svs.io

← Back to blog

Extreme Decoupling FTW

After spending years writing fat, ugly classes, I suppose it is inevitable that the pendulum swings the other way and we head towards ‘wat? you got a class for that..?!?!?!?’ territory. For the moment though, the wins are huge and keep coming.

Dependency Injection is just a big word for explicitly passing in stuff that your object is going to depend on. Recently, I’ve been working on an app that automatically goes and make reservations using certain APIs. Testing it is always problematic because one has to constantly stub out the actual API call with something that doesn’t contractually oblige you to several thousand rupees of payments :-) Also, the frontend is being worked on by a separate team and I don’t want the dev/staging server to make actual bookings during development. With DI, dealing with situations like this is easy.

While building the app, I decided that nothing was going to talk to anything without talking to something else first. Here’s a rough sketch of the architecture.

class TripBooker
# Given a trip and a list of vendors, this class makes bookings for all the vendors
def initialize(trip, vendors = :all)
@trip = trip
@vendors = VendorSelector.new(vendors).all
@credentials = CredentialSelector.get_available_credentials
end
def book!(vendors = nil)
{
:results => ((Array(vendors) if vendors) || @vendors).map do |vendor|
book_with_vendor!(vendor)
end
}
end
private
def book_with_vendor!(vendor)
VendorBooker.new(@trip, vendor, @credentials.send(vendor)).book!
end
end
view raw trip_booker.rb hosted with ❤ by GitHub

The TripBooker first calls out to a VendorSelector service which provides a list of vendors based on any filtering rules that might apply. Then, each vendor is passed into a VendorBooker service that does the booking with that vendor. It also calls out to the CredentialSelector service to choose an appropriate set of credentials for the calls. What does the VendorBooker look like?

class VendorBooker
# This class is responsible for calling the appropriate translation mechanism to convert a CRM Trip
# into the appropriate format to book
#
# It also translates the response into a CRM Booking object to persist to the database
def initialize(trip, vendor, credentials)
@trip = trip
@vendor = vendor
@credentials = credentials
end
def book!
booking_response_translator.new(customer.book!(vendor_booking, @credentials)).translate
end
private
def customer
Kernel.module_eval("#{vendor}Customer").new(@trip.customer)
end
def vendor_booking
Kernel.const_get("#{vendor}Booking").new(@trip).booking
end
def booking_response_translator
Kernel.const_get("#{vendor}BookingResponse")
end
def vendor
@vendor.to_s.camelize
end
end

Oooh….more indirection. The VendorBooker creates objects of class FooCustomer and FooBooking and uses the class FooBookingResponse to parse the results from the booking. Internally, FooBooking calls the wrapper class to the API with the appropriate parameters. FooBooking is the translator class that translates from a generic Booking object into one that fits with Vendors::Foo’s idea of what a booking is. The API wrapper class Vendors::Foo has no idea about anything that just happened above. It merely accepts some arguments to the constructor and makes appropriate calls to the foo.com API.

So, what does four levels of indirection get you? Easy pluggability. As I mentioned, I’d had to stub out the calls to the foo API during testing and I was about to deploy the app on to a staging server so our frontend team could write code against it, but I didn’t want to actually make bookings. So, I decided that I would have a different setup for development, staging and testing. Basically, during testing, I make VendorSelector return [:dummy] as the vendor. Then the class DummyBooking and DummyBookingResponse returns a dummy booking without making any API calls.

By being explicit about the dependencies at every step of the way and making sure each class only does one thing, we’ve made life super easy when we need to introduce new behaviour in particular situations.

This is just one of the ways in which DI helps massively. Here’s a flowcharty diagram