CSC/ECE 517 Fall 2010/ch5 5e mf

From Expertiza_Wiki
Jump to navigation Jump to search

The Dependency Injection Pattern

An explanation by way of example

The dependency injection pattern is a design pattern for fully decoupling a class from the creation of other classes upon which it depends. In this sense, it is similar to the factory and service locator patterns which are also concerned with abstracting object creation away from the dependent class. The utility of and differences between these patterns is subtle and difficult to explain without a lot of jargon. So the concepts will be demonstrated through example. Suppose, that Syd has a strategy for making statements about objects illustrated in the following Ruby code.

# A strategy for remarking on things.
class Syd
  def remark
    if @thing.got_it?
      puts "I've got a #{@thing}."
    else
      puts "I know a #{@thing}."
    end
  end
end

With this method, Syd can make a statement about any object thing that implements the methods got_it? and to_s. The Bike class shown below is an example.

# An example of a thing upon which to remark.
class Bike
  def to_s
    "bike"
  end
  def got_it?
    return true
  end
end

Direct instantiation

In order for Syd to use his strategy, he must get ahold of a thing. The simplest way to do this is to instantiate a thing directly.

# An example of direct object instantiation.
class Syd
  def initialize
    @thing = Bike.new
  end
end

However, now all that Syd can talk about is his bike. This is because, although Syd has a strategy for talking about a wide variety of things, Syd depends on the concrete class Bike for thing creation.

The factory pattern

A common technique for encapsulating object instantiation is the factory pattern. The following example uses a factory to allow Syd to talk about an array of things. In this case, thing creation is random, but a more complex factory might be configurable through constructor arguments or some external resource like an XML file.

# An example of the factory pattern.
class ThingFactory
  def initialize
    # An array of thing classes.
    @things = [Bike, Cloak, Mouse, GingerbreadMen, Room]
  end
  def create_random_thing
    # Pick a random thing and create an instance of it.
    @things.sort_by { rand }[0].new
  end
end

$thing_factory = ThingFactory.new

class Syd
  def initialize
    # Use the factory to create a random thing
    @thing = $thing_factory.create_random_thing
  end
end

One major drawback of using the factory pattern is that each abstraction needs its own factory. In this case, the only abstraction is thing, but if there were many abstractions this could lead to a lot of unnecessary code duplication, since factories are often similar to one another. This may result in code that is difficult to maintain. Consider the case where object instantiation is configured by some outside resource like an XML file. If each factory must independently obtain information from this resource, then every time that the format of the XML file changes, every factory implementation must be modified as well.

The service locator pattern

The service locator pattern addresses the problem of factory proliferation by centralizing object instantiation. In a sense, the service locator consolidates a collection of factories into a single repository from which other objects may request dependencies. However, the role of the service locator is somewhat different from the factory. Whereas factories are specifically concerned with object creation, the service locator is concerned with supplying dependencies to objects. Consider several classes which all depend on a common abstraction. Whereas a factory would typically create an instance of the abstraction for each of the classes, the service locator might create a single instance and supply it to all of the classes.

The example problem is not complex enough to truly benefit from a service locator, but the following example code demonstrates how a service locator can facilitate code reuse. The class_eval method is used to add methods procedurally for each of the abstractions ("services") that the service locator provides. If the method of random object creation needs to be modified at some point in the future, there is only a single definition that needs to be changed.

# An example of the service locator pattern.
class ServiceLocator
  def initialize
    # Several arrays of abstractions
    @things = [Bike, Cloak, Mouse, GingerbreadMen, Room]
    @charms = [Heart, Star, Horseshoe, Clover]
    @triffles = [Tiramisu, StrawberryShortcake, TipsyLaird]
  end
end
# Add a get_random method for each abstraction
[:thing, :charm, :trifle].each do |abstraction|
  ServiceLocator.class_eval "def get_random_#{abstraction}
      @#{abstraction}s.sort_by { rand }[0].new
    end"
end

$service_locator = ServiceLocator.new

class Syd
  def initialize
    # Use the service locator to get a random thing
    @thing = $service_locator.get_random_thing
  end
end

However, the factory and service locator patterns share a common disadvantage. Namely, a class which uses a factory or a service locator necessarily depends on the factory or service locator. This has potentially serious implications for code maintainability. A class which uses a service locator must know the name and calling conventions of the service locator in order to use it. If the name or calling conventions of the service locator are modified, all classes which use the service locator must also be modified. The same is true of factories.

The dependency injection pattern

Dependency injection eliminates the dependency between a class and the mechanism used to supply its dependencies. Rather than make an object responsible for acquiring its own dependencies (directly, through factories, or through a service locator), dependency injection places the responsibility on the code that uses the object to provide the dependencies that the object needs. This is usually accomplished either by passing dependencies as arguments to the object's constructor or to a setter method. The following code example accepts both forms of dependency injection.

# An example of the dependency injection pattern.
class Syd
  attr_writer :thing
  def initialize(thing)
    # Constructor injection.
    @thing = thing
  end
end

Dependency injection is an example of the inversion of control principle. Rather than giving a class control of the creation of its own dependencies, control is abstracted out to higher-level code (an "assembler") which creates the dependencies and provides them to the class.

This method of decoupling a class from its dependencies makes the dependency injection pattern fundamentally different from the factory and service locator patterns. Whereas the factory and service locator patterns encapsulate object instantiation so as to hide the concrete class from the dependent object, the dependency injection pattern pulls the act of object instantiation outside of the dependent object through inversion of control.

Dependency injection and duck typing

It is worth noting that the example code for the dependency injection pattern is much simpler than for the factory and service locator patterns. This is partly because the logic of object instantiation has been pulled outside the example, but it is also because dependency injection fits in well in duck-typed languages like Ruby.

Duck typing means that any object may be passed as an argument to a method and, so long as it possesses the necessary methods and attributes, the program will execute properly. In the previous code example, there is no interface or abstract base class from which objects inherit. Any object which implements the got_it? and to_s methods may be used as a thing. This makes it easy to create new classes and modify existing classes to be used as things.

Dependency injection complements duck typing nicely, because any object with the methods necessary to meet the needs of the dependent class may be injected. Objects which were never explicitly designed to satisfy a dependency can be used to meet it with little or no modification. In contrast, a factory or service locator would have to be modified to support supplying the new class of object.

Real applications of dependency injection

Software frameworks

Dependency injection is a key feature of many software frameworks. Spring is a notable example. A software framework is essentially an application skeleton with a well-specified application programming interface (API) intended to serve as an extensible base for developing a fully-fleshed out application. Since library methods are called by an application, the application controls the flow of execution. This means that typically a significant amount of an application developed using libraries must be implemented before the application will be capable of doing anything useful.

In contrast, a software framework is already a functional piece of software before the application developers writes any code. Application developers override or specialize parts of the framework in order to extend its functionality. This is possible because frameworks follow the design principle of inversion of control. Rather than allow the application code to dictate the path of execution, the framework controls it.

Since software frameworks employ inversion of control, dependency injection is a natural fit. If a framework specifies an appropriate set of conventions to application developers, the framework can infer the dependencies of application code and inject them into it. Which classes the framework will use to fulfill the application dependencies and how it will initialize them is typically controllable through some form of configuration file. However, many frameworks follow a convention over configuration philosophy and will infer parameters which are not specified by the developer.

PicoContainer

PicoContainer is another example of a real world application of dependency injection. PicoContainer provides a container class to which one may add arbitrary Java classes. If the constructors of some of these classes depend on interfaces implemented by other classes, PicoContainer will infer these dependencies and use instantiations of objects which implement the required interfaces in order to instantiate other objects. Consider the following example code take from the PicoContainer website.

# Source: http://www.picocontainer.org/introduction.html

public interface Peelable {
  void peel();
}

public class Apple implements Peelable {
  public void peel() {
  }
}

public class Peeler implements Startable {  
  private final Peelable peelable;

  public Peeler(Peelable peelable) {
    this.peelable = peelable;
  }

  public void start() {
    peelable.peel();
  }
  
  public void stop() {
  }
}

public class Juicer {
  private final Peelable peelable;
  private final Peeler peeler;

  public Juicer(Peelable peelable, Peeler peeler) {
    this.peelable = peelable;
    this.peeler = peeler;
  }
}

The classes given above may be added to an instance of PicoContainer.

# Source: http://www.picocontainer.org/introduction.html

MutablePicoContainer pico = new DefaultPicoContainer();  
pico.addComponent(Apple.class);
pico.addComponent(Juicer.class);
pico.addComponent(Peeler.class);

PicoContainer infers how to instantiate these classes from their constructors. The instantiations may then be retrieved from the PicoContainer.

# Source: http://www.picocontainer.org/introduction.html

Juicer juicer = (Juicer) pico.getComponent(Juicer.class);

The class definitions given above are interdependent and these dependencies may be satisfied through construction injection. By requiring developers to write classes which follow this relatively simple convention (constructor injection of interfaces), PicoContainer is able to resolve the dependencies automatically for the developer. Under the hood, PicoContainer is doing something roughly like the following code.

# Source: http://www.picocontainer.org/introduction.html

Peelable peelable = new Apple();
Peeler peeler = new Peeler(peelable);
Juicer juicer = new Juicer(peelable, peeler);
return juicer;

This process would be much more complicated if not impossible using the factory or service locator pattern, since the class dependencies would be hidden inside the classes rather than exposed in their constructors.

References