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.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
where
s 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.
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.