11.6. Dependency Injection Principle

The dependency injection principle (DIP), sometimes also called dependency inversion, states that if two classes depend on each other but their implementations may change, it would be better for them to both depend on a separate abstract interface that is “injected” between them.

Suppose RottenPotatoes now adds email marketing—interested moviegoers can receive emails with discounts on their favorite movies. RottenPotatoes integrates with the external email marketing service MailerMonkey to do this job:

 1class EmailList
 2    attr_reader :mailer
 3    delegate :send_email, :to => :mailer
 4    def initialize
 5        @mailer = MailerMonkey.new
 6    end
 7end
 8# in RottenPotatoes EmailListController:
 9def advertise_discount_for_movie
10    moviegoers = Moviegoer.interested_in params[:movie_id]
11    EmailList.new.send_email_to moviegoers
12end

Suppose the feature is so successful that you decide to extend the mechanism so that moviegoers who are on the Amiko social network can opt to have these emails forwarded to their Amiko friends as well, using the new \(\text{Amiko}\) gem that wraps Amiko’s RESTful API for friend lists, posting on walls, messaging, and so on. There are two problems, however.

First, EmailList#initialize has a hardcoded dependency on \(\text{MailerMonkey}\), but now we will sometimes need to use \(\text{Amiko}\) instead. This runtime variation is the problem solved by dependency injection—since we won’t know until runtime which type of mailer we’ll need, we modify EmailList#initialize so we can “inject” the correct value at runtime:

 1class EmailList
 2    attr_reader :mailer
 3    delegate :send_email, :to => :mailer
 4    def initialize(mailer_type)
 5        @mailer = mailer_type.new
 6    end
 7end
 8# in RottenPotatoes EmailListController:
 9def advertise_discount_for_movie
10    moviegoers = Moviegoer.interested_in params[:movie_id]
11    mailer = if Config.has_amiko? then Amiko else MailerMonkey end
12    EmailList.new(mailer).send_email_to moviegoers
13end

You can think of DIP as injecting an additional seam between two classes, and indeed, in statically compiled languages DIP helps with testability. This benefit is less apparent in Ruby, since as we’ve seen we can create seams almost anywhere we want at runtime using mocking or stubbing in conjunction with Ruby’s dynamic language features.

The second problem is that \(\text{Amiko}\) exposes a different and more complex API than the simple send_email method provided by \(\text{MailerMonkey}\) (to which EmailList#send_email delegates in line 3), yet our controller method is already set up to call send_email on the mailer object. The Adapter pattern can help us here: it’s designed to convert an existing API into one that’s compatible with an existing caller. In this case, we can define a new class \(\text{AmikoAdapter}\) that converts the more complex Amiko API into the simpler one that our controller expects, by providing the same send_email method that \(\text{MailerMonkey}\) provides:

 1class AmikoAdapter
 2    def initialize ; @amiko = Amiko.new(...) ; end
 3    def send_email
 4        @amiko.authenticate(...)
 5        @amiko.send_message(...)
 6    end
 7end
 8# Change the controller method to use the adapter:
 9def advertise_discount_for_movie
10    moviegoers = Moviegoer.interested_in params[:movie_id]
11    mailer = if Config.has_amiko? then AmikoAdapter else MailerMonkey end
12    EmailList.new(mailer).send_email_to moviegoers
13end

When the Adapter pattern not only converts an existing API but also simplifies it—for example, the \(\text{Amiko}\) gem also provides many other Amiko functions unrelated to email, but AmikoAdapter only “adapts” the email-specific part of that API—it is sometimes called the Façade pattern.

Lastly, even in cases where the email strategy is known when the app starts up, what if we want to disable email sending altogether from time to time? Figure 11.19 (top) shows a naive approach: we have moved the logic for determining which emailer to use into a new \(\text{Config}\) class, but we still have to “condition out” the email-sending logic in the controller method if email is disabled. But if there are other places in the app where a similar check must be performed, the same condition logic would have to be replicated there (shotgun surgery). A better alternative is the Null Object pattern, in which we create a “dummy” object that has all the same behaviors as a real object but doesn’t do anything when those behaviors are called. Figure 11.19 (bottom) applies the Null Object pattern to this example, avoiding the proliferation of conditionals throughout the code.

 1class Config
 2    def self.email_enabled? ; ... ; end
 3    def self.emailer ; if has_amiko? then Amiko else MailerMonkey end ; end
 4end
 5def advertise_discount_for_movie
 6    if Config.email_enabled?
 7        moviegoers = Moviegoer.interested_in(params[:movie_id])
 8        EmailList.new(Config.emailer).send_email_to(moviegoers)
 9    end
10end
 1class Config
 2    def self.emailer
 3        if email_disabled? then NullMailer else
 4            if has_amiko? then AmikoAdapter else MailerMonkey end
 5        end
 6    end
 7end
 8class NullMailer
 9    def initialize ; end
10    def send_email ; true ; end
11end
12def advertise_discount_for_movie
13    moviegoers = Moviegoer.interested_in(params[:movie_id])
14    EmailList.new(Config.emailer).send_email_to(moviegoers)
15end
Figure 11.19: Top: a naive way to disable a behavior is to “condition it out” wherever it occurs. Bottom: the Null Object pattern eliminates the conditionals by providing “dummy” methods that are safe to call but don’t do anything.

Figure 11.20 shows the UML class diagrams corresponding to the various versions of our DIP example.

An interesting relative of the Adapter and Façade patterns is the Proxy pattern, in which one object “stands in” for another that has the same API. The client talks to the proxy instead of the original object; the proxy may forward some requests directly to the original object (that is, delegate them) but may take other actions on different requests, perhaps for reasons of performance or efficiency.

Two classic examples of this pattern are found in ActiveRecord itself. First, the object returned by ActiveRecord’s all, where and find-based methods quacks like a collection, but it’s actually a proxy object that doesn’t even do the query until you force the issue by asking for one of the collection’s elements. That is why you can build up complex queries with multiple wheres without paying the cost of doing the query each time. The second is when you use ActiveRecord’s associations (Section 5.4: the result of evaluating @movie.reviews quacks like an enumerable collection, but it’s actually a proxy object that responds to all the collection methods (size, <<, and so on), without querying the database except when it has to. Another example of a use for the proxy pattern would be for sending email while disconnected from the Internet. If the real Internet-based email service is accessed via a send_email method, a proxy object could provide a send_email method that just stores an email on the local disk until the next time the computer is connected to the Internet. This proxy shields the client (email GUI) from having to change its behavior when the user isn’t connected.

11.20
Figure 11.20: Left: Without dependency injection, EmailList depends directly on MailerMonkey. Center: With dependency injection, @mailer can be set at runtime to use any of MailerMonkey, NullMailer (which implements the Null Object pattern to disable email), or AmikoAdapter (which implements the Adapter/Façade pattern over Amiko), all of which have the same API. Right: In statically typed languages, the abstract superclass GenericMailer formalizes the fact that all three mailers have compatible APIs, but in Ruby this superclass is often omitted if it consists entirely of abstract methods (as is the case here), since abstract methods and classes aren’t part of the language.

Self-Check 11.6.1. Why does proper use of DIP have higher impact in statically typed languages?

In such languages, you cannot create a runtime seam to override a “hardwired” behavior as you can in dynamic languages like Ruby, so the seam must be provided in advance by injecting the dependency.