Dependency Inject with caution, please.

Posted by Niels Buus on november 13, 2013
English, Rails, Ruby, Software development

In contemporary programming you often stumble upon this pattern called ‘Dependency Injection’. Proponents will emphasize looser coupling and easier testing, when advocating it’s use. In this commentary, I will examine the concept and explain why it should be used with restraint. In my example, I will refer to an imaginary supporter of the Dependency Injection pattern and I will name him Pat. I will use Ruby in my examples, but the reasoning behind can be applied in most programming languages. Let’s first look at some code.

require 'open-uri'

class PostalCodeLookup 
  def self.find_postal_name(zip_code)
    response = open("http://postal-code-webservice.org/denmark/#{zip_code}.json")
    JSON.parse(response)["name"]
  end
end

This code consists of a class called PostalCodeLookup with a single class method which, given a postal code, can return the name of the corresponding city for that postal code, using an imaginary JSON webservice. The context of the app is a simple timetracking application written in Rails and handcrafted for a local Danish company that offers consultancy services. The PostalCodeLookup class will be used in three controllers. First in ConsultantController where it is used to fill out the private contact details for each consultant working in the company. Secondly in the ConsultancySessionController, where it is used to optionally record the physical location of a consultancy session. Finally, it is used in the CustomerController, where it’s used to fill out the contact details of a customer, so the system can export serialized invoices for the accounting department.

While the code may look rather innocent, I can sense Pat looking sadly at this code. He points at the code and exclaims “It has dependencies! It will be hard to test!”. Pat may be right, it does indeed have dependencies. I mean, this code wouldn’t work unless run in an environment that offers the open-uri library. Furthermore, it has the service url hardcoded in. It expects the response to be JSON and it even grabs a JSON parser from the global namespace.

So let’s try to enumerate the dependencies in play here. I will consider knowledge of external circumstances, classes or methods as dependencies. While some may argue that invoking a method on an external class constitutes as much as three dependencies – knowledge of the class name, knowledge of the class method and knowledge of the method signature, I am going to group it as one dependency for simplicity.

This means that each controller has a dependency on the PostalCodeLookup class, so that’s three dependencies. Furthermore, PostalCodeLookup knows a lot of stuff. It knows about:

  • open-uri
  • The URL of the external webservice
  • Which country we are operating in
  • The serialization format used
  • Where to get a parser for that format and how to use the parser
  • How to extract the postal name from the response data structure

That’s six additional dependencies bringing us to a total of nine. Pat is not keen on that. He wants the class to know less and conceives an alternative implementation:

class PostalCodeLookup
  attr_reader :http_client, :url, :country, :format, :parser, :node_path

  def initialize(http_client, url, country, format, parser, node_path)
    @http_client = http_client
    @url = url
    @country = country
    @format = format
    @parser = parser
    @node_path = node_path
  end

  def find_postal_name(zip_code)
    response = http_client.get("#{url}/#{country}/#{zip_code}.#{format}"
    parsed_response = parser.parse(response)
    node_path.each do |node_path_segment|
      parsed_response = parsed_response[node_path_segment]
    end
    parsed_response
  end

end

Pat is happier about this approach. Look how this class has no knowledge of any classes in the global namespace. Pat thinks it makes the code easier to understand, more flexible and easier to test. He argues:

  • It’s easier to understand, because you no longer have to be concerned with actual classes. Instead of understanding the JSON class or open-uri, it is abstracted away and all you need to know is that PostalCodeLookup will receive objects that respond to methods such as get or parse.
  • It’s more flexible, because in my version:
    • You are not restricted to looking up Danish postal codes. In fact, you can look up just about any postal code.
    • You can use any domain name, so if there are multiple domains hosting this webservice, you can pick the one that fits the particular use case.
    • Furthermore, you are not limited to using a particular http library which may be deprecated or unavailable in a future environment, instead you just supply any http client object that responds to get.
    • You are not limited to a particular serialization format. JSON may be the format of today, but who knows if it will sink into oblivion like SOAP/XML? Perhaps we’ll all use BSON, YAML or CSV next year. Keep the class flexible by injecting a parser object.
    • You can’t be sure that the postal name will always reside in the root of the response at an attribute called “name”, so let’s abstract that away and have the service accept an array of attribute keys, so the postal name can be hidden in any arbitrary nested data structure.
  • It’s easier to test, because there are no dependencies to actual classes, so in a spec you can supply stubs through the constructor and easily set expectations on any interaction with these injected dependencies.

 

While Pat is right about this, he’s forgetting something: The whole picture.

While PostalCodeLookup no longer has external dependencies and can be configured in numerous ways through the constructor, he has inflicted pain on the consumers of the class.

Before Pat intervened, the three controllers could use the PostalCodeLookup class by calling PostalCodeLookup.find_postal_name(zip_code), like:

PostalCodeLookup.find_postal_name(2300)
=> "København S"

Using Pats implementation, it suddenly takes quite a bit of configuration to get started. First, we must write an http wrapper which encapsulates the open-uri library and supports the interface that PostalCodeLookup expects.

require 'open-uri'

class OpenUriWrapper
  def self.get(url)
    open(url).read
  end
end

So now we are ready to use the PostalCodeLookup class by calling:

postal_code_finder = PostalCodeLookup.new(OpenUriWrapper, 'http://postal-code-webservice.org', 'denmark', 'json', JSON, ['name'])

The controllers are not happy about this. They are littered with knowledge of things they care little about only to satisfy a selfish PostalCodeLookup class that wants to live in a carefree world. I voice my concern to Pat by asking “Doesn’t it concern you that three controllers now have knowledge of things irrelevant to them?”. Pat thinks and replies “No, it makes the code more flexible and more explicit.”

Anyway. Thanks to Pat, PostalCodeLookup has been freed from six dependencies, although we’ve introduced 18 new dependencies and will introduce 6 more for every class that wants to use PostalCodeLookup.

But what about his arguments?

Readability

Pat thinks removing class references improves readability. He believes you only need to understand the duck.

I very much disagree. When a duck is passed in, the interface is implicit. While Java developers are good at creating interfaces and having them ruthlessly enforced by a compiler, a dynamic language such as Ruby makes the interface more blurry. There rarely exists a formally defined interface class, so the duck really is the sum of methods used in the context at any given point in time. This makes refactoring difficult, because while it may be as simple as using your IDE to figure out what methods the JSON class responds to, it requires a more thorough investigation to figure out which parsers are injected in different contexts and what methods these parsers may have in common.

Flexibility

He rightfully points out that the class becomes very adaptable, because it can be instantiated in countless ways. However, there is no need. Looking at the initializer signature it accepts

http_client, url, country, format, parser, node_path

open-uri is baked into Ruby 1.9/2.0, so I can’t think of a reason to not use it. And we don’t have multiple services. The application is developed for a local Danish company. The response is always JSON and the response datastruture always contain a ‘name’ key. In other words. There is absolutely no need for all this configuration, when the class can work flawlessly with sensible defaults. In the future, when one or more of these constants becomes variable, you can extract them for injection. But I think it’s a good idea to postpone that until the need arises, because often you’ll find that it doesn’t.

It sounds like dependency injection is always bad?

Well, there are situations in which the pattern is useful. But don’t inject just for the sake of it. I should admit that I have used the least elegant kind of dependency injection in these examples, which is called manual injection, specifically constructor injection. Alternative implementations exist in which a service supplies dependencies, so our controllers are not littered with information. In such a setup, the service often use a configuration source that can be altered at runtime, so in the event the URL to the webservice changes, the system administrator can fix it by editing an environment variable or YAML configuration file and reload the configuration, rather than calling the developers in to update the PostalCodeLookup class. A sensible example of this practice can be found in ActiveRecord, where database configuration is loaded from a YAML file during startup, rather than hardcoded into the ActiveRecord models themselves.

This makes sense because every Rails application has it’s own unique database credentials, rather than some shared set from 37signals. It also makes sense, because you’ll want to use different databases, depending on whether you are running in development or production. But, since we are not querying different postal code webservices and there only exists one deployment of our timetracking app, there is little point in extracting URL, serialization format or country details from the PostalCodeLookup class.

The URL could be extracted, but the only situation in which I could imagine dumping the webservice, would be during a test of the consuming controllers. However, it would be way too tedious to set up a local dummy JSON webservice, instead of just stubbing the find_postal_name method:

PostalCodeLookup.stub(:find_postal_name).and_return("Valby")

Finally, injecting the JSON parser is also pointless as Ruby has a built-in JSON parser in the standard library and as such as be relied upon to be present.

So, in conclusion:

  • Don’t inject just because people tell you to.
  • Don’t elevate constants to global availability until you see they are used multiple times.
  • Don’t extract behavior for injection, until you actually have multiple implementations of that behavior and in such a case, consider creating multiple classes using inheritance or mixins rather than behavioral injection.
  • If you DO inject, make sure the interface is clear.
  • Moving a dependency from a class to it’s consumer, only reduces coupling, if there are no consumers. And if the class has no consumers, you better delete it. Fast.
  • If there’s only a single consumer of the class, moving the dependency reduces readability and complicates the method signature of the class’s constructor.
  • If there are multiple consumers of a class, moving the dependency increases coupling propertional to the number of consumer classes.

What are your thoughts on this topic? Feel free to leave a comment.

{lang: 'da'}

Tags:

Endnu ingen kommentarer.

Skriv en kommentar

WP_Big_City

Du skal være logget ind for at skrive en kommentar.