Controlling your dependencies

Dependencies take many forms. You’re dependent on your car (shame on you!) to get to work. Your pet depends on you to feed it. You depend on your employer paying you for your work, and your employer depends on you to actually produce the work.

I’m going to talk about Dependency Injection, and I’ll be using Ruby for most of the examples. First though, we’ll cover what a dependency is.

Dependencies take many forms. You’re dependent on your car (shame on you!) to get to work. Your pet depends on you to feed it. You depend on your employer paying you for your work, and your employer depends on you to actually produce the work.

You may not think much about these in daily life, but every thing that you rely on reduces your control over your situation. For those commuters among us, if you take a train to get to work you’re probably already aware that you have no control over whether it arrives on time; without that control you’re indirectly out-of-control of what time you arrive to work.

Dependencies are unavoidable. To stop depending on the trains you could take your car, but that’s just shifting the buck; you work from home, but then you’re reliant on your broadband and electricity being available. They’re unavoidable, it’s how you manage them that’s important.

Sometimes you need to think at a higher-level than where your dependency is affecting you. You may not be able to control the trains, but you can control your need for the trains. Substitution is best method for maintaining control. Being able to substitute your mode of transportation for another without consequence would seriously improve your situation.

Dependencies in your code

Let’s play spot the dependencies.

class ChatSession
  attr_accessor :session_id

  def initialize
    @server = ChatServer.new
  end

  def get_latest_messages
    messages = @server.get_messages session_id, :since => @last_request_time
    @last_request_time = DateTime.now
   messages
  end
end

There’s a couple in there, can you spot them? The main one is that we’re depending on a ChatServer instance. We’re also depending on DateTime, but we’ll leave that one alone for now. By instantiating the ChatServer in the constructor, we’ve lost control of how our class works.

There’s a core tenet of Object Oriented Programming called the Open/Closed Principal. This states that modules should be open for extension, but closed for modification. Put simply, this means you should be able to modify the behavior of your classes without changing their code.

If we decided that our chat client was going to be distributed rather than centralized, then we’d have to modify this class. We’d have to modify all classes that instantiate a ChatServer. This design is brittle, because a single design change can ripple out across your entire code-base requiring many small changes.

How exactly do you close a class for modification, but still allow extension? I’ll show you.

def initialize(server_class)
  @server = server_class.new
end

We’re now passing in the class of server into our constructor, which is a technique called Constructor Injection; we’d create it like so: ChatSession.new(ChatServer)ChatSession is now independent of server implementation details and can operate with any server; we can now potentially swap out to use a DistributedChatServer or more practically, substitute the server in a test to verify our class more easily.

Those of you who’ve used dependency injection in a static language (such as C# or Java) may be wondering why we’re passing the Type in, instead of an actual instance. We’re doing this because it’s more flexible when combined with the dynamic nature of Ruby. For instance you could throw in a respond_to? call to check if the type is capable of doing something special, and if not fall back on the regular constructor; we couldn’t do this if we’d only received an instance.

While overloading the constructor like this can be very useful, it does lead us into producing more code when we instantiate classes. The more arguments you add, the less it scales. Statically typed languages favor using a Dependency Injection framework to alleviate this problem (I’ll cover them soon), but as we’re currently talking in Ruby we’re going to keep things simple.

def initialize(server_class=ChatServer)
  @server = server_class.new
end

We’ve altered our constructor is now using the default parameter support from Ruby to supply the most common scenario to our instance. Most of the time we’re going to use the standardChatServer so it makes sense to default to that and only supply a different one for those special cases. This gives us back our ability to use a plain new. We now satisfy the Open/Closed Principal, because we can now modify the class’s behavior by supplying a different server instance without having to modify any of it’s code.

Q: We’d still need to modify every class that creates a default ChatServer if we wanted to swap it out entirely, wouldn’t we?

A: Correct, this design is mainly intended for allowing easier occasional substitution (present in unit tests mainly). If you wanted full independence from any references to concrete implementations in your dependents, then you could use a variety of other injection patterns, or simply not supply the default to achieve this. Some examples are using a factory method with subclassing, a property to set on construction, or a type supplied directly to the method that uses it. Ruby is dynamic, do whatever works for you.

It’s fairly accepted in Ruby circles that there isn’t the need for frameworks to do Dependency Injection, as the language is flexible enough to let you do it easily enough yourself; Jamis Buck covers this position nicely in his LEGOs, Play-Doh, and Programming blog post; however, not all languages have this ability, and it’s these that generally utilise frameworks to help with dependency injection.

In static language Dependency Injection frameworks are what tie everything together. They allow you to specify your dependencies via constructors and have them auto-magically injected at run-time. If you have your container configured to pick up all your available classes on startup, then extending a class is as simple as changing the constructor, everything else happens automatically.

Our Ruby example would be configured using StructureMap (a .Net dependency injection framework) like so:

public class ServerRegistry : Registry
{
  public ServerRegistry()
  {
    Scan(x =>
    {
      x.TheCallingAssembly();
      x.WithDefaultConventions();
    });

    ForRequestedType<IChatServer>()
      .TheDefaultIsConcreteType<CenteralizedChatServer>();
  }
}

The Registry is StructureMap’s convention for configuring the container, and it’s Scan method auto-discovers all injectable types, and the ForRequestedType method-chain configures our application to use the CenteralizedChatServer as the default instance for any consumers of an IChatServer. It’s that last part that we’d be able to switch on an external variable to use a DistributedChatServer if needed.

There are dependency injection frameworks for almost every language, even for those that don’t really need them. .Net has StructureMapWindsor, and Ninject (as well as several more); Java has SpringGoogle Guice, and others; php has Symfony and Crafty; and even though Ruby and Python don’t really need them, they have Needle and Snake Guice respectively.

I know what you’re thinking, “sure this sounds nice, but isn’t it just more work? How does it actually make my life easier?”.

The fewer dependencies that your classes have, the fewer reasons they have to change; the fewer reasons they have to change, the less work you have to do; the less work you have to do, the more time you can spend doing more fun things (whether they’re more interesting programming tasks, or life outside the screen). That would be nice, wouldn’t it?

Quick Thanks

I’d just like to give James Gregory a huge thanks for writing this post.  James is the project lead for Fluent NHibernate and is the founder of Proof of Concept Ltd.

, ,

No comments yet.

Leave a Reply

Leave your opinion here. Please be nice. Your Email address will be kept private.