다음을 통해 공유


2 - Dependency Injection

patterns & practices Developer Center

On this page: Download:
Introduction | Factories, Service Locators, and Dependency Injection | Factory Patterns - The Factory Method Pattern, Simple Factory Pattern, Abstract Factory Pattern | Service Locator Pattern | Dependency Injection | Object Composition | Object Lifetime | Types of Injection | Property Setter Injection | Method Call Injection | When You Shouldn’t Use Dependency Injection | Summary

Download code

Download PDF File

Order Paperback

Introduction

Chapter 1 outlines how you can address some of the most common requirements in enterprise applications by adopting a loosely coupled design to minimize the dependencies between the different parts of your application. However, if a class does not directly instantiate the other objects that it needs, some other class or component must take on this responsibility. In this chapter, you'll see some alternative patterns that you can use to manage how objects are instantiated in your application before focusing specifically on dependency injection as the mechanism to use in enterprise applications.

Factories, Service Locators, and Dependency Injection

Factories, service locators, and dependency injection are all approaches you can take to move the responsibility for instantiating and managing objects on behalf of other client objects. In this section, you'll see how you can use them with the same example you saw in the previous chapter. You'll also see the pros and cons of the different approaches and see why dependency injection can be particularly useful in enterprise applications.

Factory Patterns

There are three common factory patterns. The Factory Method and Abstract Factory patterns from "Design Patterns: Elements of Reusable Object-Oriented Software" by Gamma, Erich, Richard Helm, Ralph Johnson, and John Vlissides. Addison Wesley Professional, 1994., and the Simple Factory pattern.

The Factory Method Pattern

The following code samples show how you could apply the factory method pattern to the example shown in the previous chapter. The first code sample shows how you could use a factory method to return an instance of the TenantStore class to use in the ManagementController class. In this example, the CreateTenantStore method is the factory method that creates the TenantStore instance and the Index method uses this instance as part of its logic.

public class ManagementController : Controller
{
  protected ITenantStore tenantStore;

  public ManagementController()
  {
    this.tenantStore = CreateTenantStore();
  }

  protected virtual ITenantStore CreateTenantStore()
  {
    var storageAccount = AppConfiguration
      .GetStorageAccount("DataConnectionString");
    var tenantBlobContainer = new EntitiesContainer<Tenant>
      (storageAccount, "Tenants");
    var logosBlobContainer = new FilesContainer
      (storageAccount, "Logos",
      "image/jpeg");
    return new TenantStore(tenantBlobContainer,
                            logosBlobContainer);
  }

  public ActionResult Index()
  {
    var model = new TenantPageViewData<IEnumerable<string>>
      (this.tenantStore.GetTenantNames())
    {
      Title = "Subscribers"
    };
    return this.View(model);
  }

  ...
}

Using this approach does not remove the dependencies the ManagementController has on the TenantStore class, nor the FilesContainer and EntitiesContainer classes. However, it is now possible to replace the underlying storage mechanism without changing the existing ManagementController class as the following code sample shows.

public class SQLManagementController : ManagementController
{
  protected override ITenantStore CreateTenantStore()
  {
    var storageAccount = ApplicationConfiguration
      .GetStorageAccount("DataConnectionString");
    var tenantSQLTable = ...
    var logosSQLTable = ....
    return new SQLTenantStore(tenantSQLTable, logosSQLTable);
  }

  ...
}
Dn178469.note(en-us,PandP.30).gifCarlos says:
Carlos The factory method pattern enables you to modify the behavior of a class without modifying the class itself by using inheritance.

The application can use the SQLManagementController class to use a SQL-based store without you needing to make any changes to the original ManagementController class. This approach results in a flexible and extensible design and implements the open/closed principle described in the previous chapter. However, it does not result in a maintainable solution because all the client classes that use the TenantStore class are still responsible for instantiating TenantStore instances correctly and consistently.

It is also still difficult to test the ManagementController class because it depends on the TenantStore type, which in turn is tied to specific storage types (FilesContainer and EntitiesContainer). One approach to testing would be to create a MockManagementController type that derives from ManagementController and that uses a mock storage implementation to return test data: in other words you must create two mock types to manage the testing.

Note

In this example, there is an additional complication because of the way thatASP.NETMVC locates controllers and views based on a naming convention:you must also update the MVC routes to ensure that MVC uses the new SQLManagementController class.

Simple Factory Pattern

While the factory method pattern does not remove the dependencies from the high-level client class, such as the ManagementController class, on the low-level class, you can achieve this with the simple factory pattern. In this example, you can see that a new factory class named TenantStoreFactory is now responsible for creating the TenantStore instance on behalf of the ManagementController class.

public class ManagementController : Controller
  {
    private readonly ITenantStore tenantStore;

    public ManagementController()
    {
      var tenantStoreFactory = new TenantStoreFactory();
      this.tenantStore = tenantStoreFactory.CreateTenantStore();
    }

    public ActionResult Index()
    {
      var model = new TenantPageViewData<IEnumerable<string>>
        (this.tenantStore.GetTenantNames())
      {
        Title = "Subscribers"
      };
      return this.View(model);
    }

  ...
}
Dn178469.note(en-us,PandP.30).gifCarlos says:
Carlos The simple factory pattern removes the direct dependency of the ManagementController class on a specific store implementation. Instead of including the code needed to build a TenantStore instance directly, the controller class now relies on the TenantStoreFactory class to create the instance on its behalf.

This approach removes much of the complexity from the high-level ManagementController class, although in this example the ManagementController class is still responsible for selecting the specific type of tenant store to use. You could easily move this logic into the factory class that could read a configuration setting to determine whether to create a BlobTenantStore instance or a SQLTenantStoreInstance. Making the factory class responsible for selecting the specific type to create makes it easier to apply a consistent approach throughout the application.

Abstract Factory Pattern

One of the problems that can arise from using the simple factory pattern in a large application is that it can be difficult to maintain consistency. For example, the application may include multiple store classes such as SurveyStore, LogoStore, and ReportStore classes in addition to the TenantStore class you've seen in the examples so far. You may have a requirement to use a particular type of storage for all of the stores. Therefore, you could implement a BlobStoreFactory abstract factory class that can create multiple blob-based stores, and a SQLStoreFactory abstract factory class that can create multiple SQL based stores.

Dn178469.note(en-us,PandP.30).gifJana says:
Jana The abstract factory pattern is useful if you have a requirement to create families of related objects in a consistent way.

The abstract factory pattern is described in "Design Patterns: Elements of Reusable Object-Oriented Software" by Gamma, et al.

Service Locator Pattern

Using a service locator provides another variation to this general approach of using another class to create objects on your behalf. You can think of a service locator as a registry that you can look up an instance of an object or service that another class in your application created and registered with the service locator. The service locator might support querying for objects by a string key or by interface type. Often, in contrast to the factory patterns where the factory creates the object but gives responsibility for managing its lifetime to the client class, the service locator is responsible for managing the lifetime of the object and simply returns a reference to the client. Also, factories are typically responsible for creating instances of specific types or families of types as in the case of the abstract factory pattern, while a service locator may be capable of returning a reference to an object of any type in the application.

Note

The section “Object Lifetime” later in this chapter discusses object lifetimes in more detail.

Any classes that retrieve object references or service references from the service locator will have a dependency on the service locator itself.

Dn178469.note(en-us,PandP.30).gifJana says:
Jana When using a service locator, every class will have a dependency on your service locator. This is not the case with dependency injection.

For a description of the service locator pattern, see the section “Using a Service Locator” in the article “Inversion of Control Containers and the Dependency Injection pattern” by Martin Fowler.

For a discussion of why the service locator may be considered an anti-pattern, see the blog post “Service Locator is an Anti-Pattern” by Mark Seeman.

For a shared interface for service location that application and framework developers can reference, see the Common Service Locator library . The library provides an abstraction over dependency injection containers and service locators. Using the library allows an application to indirectly access the capabilities without relying on hard references.

Dependency Injection

A common feature of the all the factory patterns and the service locator pattern, is that it is still the high-level client object's responsibility to resolve its own dependencies by requesting the specific instances of the types that it needs. They each adopt a pull model of varying degrees of sophistication, assigning various responsibilities to the factory or service locator. The pull model also means that the high-level client class has a dependency on the class that is responsible for creating or locating the object it wants to use. This also means that the dependencies of the high-level client classes are hidden inside of those classes rather specified in a single location, making them harder to test.

Figure 1 shows the dependencies in the simple factory pattern where the factory instantiates a TenantStore object on behalf of the ManagementController class.

Figure 1 - Dependencies in the factory pattern

Figure 1 - Dependencies in the factory pattern

Dependency injection takes the opposite approach, adopting a push model in place of the pull model. Inversion of Control is a term that’s often used to describe this push model and dependency injection is one specific implementation of the inversion of control technique.

Martin Fowler states: “With service locator the application class asks for it explicitly by a message to the locator. With injection there is no explicit request, the service appears in the application class—hence the inversion of control.” (Inversion of Control Containers and the Dependency Injection pattern.)

With dependency injection, another class is responsible for injecting (pushing) the dependencies into the high-level client classes, such as the ManagementController class, at runtime. The following code sample shows what the high-level ManagementController class looks like if you decide to use dependency injection.

public class ManagementController : Controller
{
  private readonly ITenantStore tenantStore;

  public ManagementController(ITenantStore tenantStore)
  {
    this.tenantStore = tenantStore;
  }

  public ActionResult Index()
  {
    var model = new TenantPageViewData<IEnumerable<string>>
        (this.tenantStore.GetTenantNames())
    {
      Title = "Subscribers"
    };
    return this.View(model);
  }

  ...
}

As you can see in this sample, the ManagementController constructor receives an ITenantStore instance as a parameter, injected by some other class. The only dependency in the ManagementContoller class is on the interface type. This is better because it doesn't have any knowledge of the class or component that is responsible for instantiating the ITenantStore object.

In Figure 2, the class that is responsible for instantiating the TenantStore object and inserting it into the ManagementController class is called the DependencyInjectionContainer class.

Figure 2 - Dependencies when using dependency injection

Figure 2 - Dependencies when using dependency injection

Note

Chapter 3, “Dependency Injection with Unity,” will describe in more detail what happens in the DependencyInjectionContainer class.

The key difference between the Figure 1 and Figure 2 is the direction of the dependency from the ManagementController class. In Figure 2, the only dependency the ManagementController class has is on the ITenantStore interface.

Dn178469.note(en-us,PandP.30).gifJana says:
Jana In Figure 2, the DependencyInjectionContainer class may manage the dependencies of multiple high level client classes such as the ManagementController class on multiple service classes such as the TenantStore class.
You can use either a dependency injection container or implement dependency injection manually using factories. As you’ll see in the next chapter, using a container is easier and provides additional capabilities such as lifetime management, interception, and registration by convention.

Object Composition

So far in this chapter, you have seen how dependency injection can simplify classes such as the ManagementController class and minimize the number of dependencies between classes in your application. The previous chapter explained some of the benefits of this approach, such as maintainability and testability, and showed how this approach relates to the SOLID principles of object-oriented programming. You will now see how this might work in practice: in particular, how and where you might use dependency injection in your own applications.

If you adopt the dependency injection approach, you will have many classes in your application that require some other class or component to pass the necessary dependencies into their constructors or methods as parameters or as property values before you can use them. This implies that your application requires a class or component that is responsible for instantiating all the required objects and passing them into the correct constructors, methods, and properties: your application must know how to compose its object graph before it can perform any work. This must happen very early in the application's lifecycle: for example, in the Main method of a console application, in the Global.asax in a web application, in a role's OnStart method in a Windows Azure application, or in the initialization code for a test method.

Dn178469.note(en-us,PandP.30).gifJana says:
Jana Typically, you should place all the code tells the application how to build its object graph in a single location; this is known as the Composition Root pattern. This makes it much easier to maintain and update the application.

Object Lifetime

You should determine when to create the objects in your application based on criteria such as which object is responsible for managing the state, is the object shared, and how long the object will live for. Creating an object always takes a finite amount of time that is determined by the object’s size and complexity, and once you have created an object, it occupies some of your system’s memory.

Dn178469.note(en-us,PandP.30).gifJana says:
Jana Whichever way you create an object, there is always a trade-off between performance and resource utilization when you decide where to instantiate it.

In the example, you've seen in this chapter, there is a single ManagementController client class that uses an implementation of the ITenantStore interface. In a real application, there may be many other client classes that all need ITenantStore instances. Depending on the specific requirements and structure of your application, you might want each client class to have its own ITenantStore object, or have all the client classes share the same ITenantStore instance, or for different groups of client classes each have their own ITenantStore instance.

If every client object has its own ITenantStore instance, then the ITenantStore instance can be garbage collected along with the client object. If multiple client objects share an ITenantStore instance, then the class or component that instantiates the shared ITenantStore object must responsible for tidying it up when all the clients are finished with it.

Types of Injection

Typically, when you instantiate an object you invoke a class constructor and pass any values that the object needs as parameters to the constructor. In the example that you saw earlier in this chapter, the constructor in the ManagementController class expects to receive an object that implements the ITenantStore interface. This is an example of constructor injection and is the type of injection you will use most often. There are other types of injection such as property setter injection and method call injection, but they are less commonly used.

Property Setter Injection

As an alternative or in addition to passing a parameter to a constructor, you may want to set a property value when you instantiate an object in your application. The following code sample shows part of a class named AzureTable in an application that uses property injection to set the value of the ReadWriteStrategy property when it instantiates AzureTable object.

public class AzureTable<T> : ...
{
  public AzureTable(StorageAccount account)
  : this(account, typeof(T).Name)
  {
  }

  ...

  public IAzureTableRWStrategy ReadWriteStrategy 
    { get; set; }
  
  ...
}

Notice that the constructors are not responsible for setting the read/write strategy and that the type of the ReadWriteStrategy property is an interface type. You can use property setter injection to provide an instance of the IAzureTableRWStrategy type when your dependency injection container constructs an instance of AzureTable<T>.

You should only use property setter injection if the class has a usable default value for the property. While you cannot forget to call a constructor, you can forget to set a property such as the ReadWriteStrategy property in the example above.

Dn178469.note(en-us,PandP.30).gifJana says:
Jana You should use property setter injection when the dependency is optional. However don’t use property setter injection as a technique to avoid polluting your constructor with multiple dependencies; too many dependencies might be an indicator of poor design because it is placing too much responsibility in a single class. See the single responsibility principle discussed in Chapter 1.
However, dependencies are rarely optional when you are building a LOB application. If you do have an optional dependency, consider using constructor injection and injecting an empty implementation (the Null Object Pattern.)

Method Call Injection

In a similar way to using property setter injection, you might want to invoke a method when the application instantiates an object to perform some initialization that is not convenient to perform in a constructor. The following code sample shows part of a class named MessageQueue in an application that uses method injection to initialize the object.

public class MessageQueue<T> : ...
{
  ...

  public MessageQueue(StorageAccount account)
  : this(account, typeof(T).Name.ToLowerInvariant())
  {
  }

  public MessageQueue(StorageAccount account,
                     string queueName)
  {
    ...
  }

  public void Initialize(TimeSpan visibilityTimeout,
                    IRetryPolicyFactory retryPolicyFactory)
  {
    ...
  }
  
  ...
}

In this example, the Initialize method has one concrete parameter type and one interface parameter type. You can use method injection to provide an instance of the IRetryPolicyFactory type when your dependency injection container constructs an instance of MessageQueue<T>.

Method call injection is useful when you want to provide some additional information about the context that the object is being used in that can’t be passed in as a constructor parameter.

Dn178469.note(en-us,PandP.30).gifJana says:
Jana Both property setter and method injection may be useful when you need to support legacy code that uses properties and methods to configure instances.

When You Shouldn’t Use Dependency Injection

Dependency injection is not a silver bullet. There are reasons for not using it in your application, some of which are summarized in this section.

  • Dependency injection can be overkill in a small application, introducing additional complexity and requirements that are not appropriate or useful.
  • In a large application, it can make it harder to understand the code and what is going on because things happen in other places that you can’t immediately see, and yet they can fundamentally affect the bit of code you are trying to read. There are also the practical difficulties of browsing code like trying to find out what a typical implementation of the ITenantStore interface actually does. This is particularly relevant to junior developers and developers who are new to the code base or new to dependency injection.
  • You need to carefully consider if and how to introduce dependency injection into a legacy application that was not built with inversion of control in mind. Dependency injection promotes a specific style of layering and decoupling in a system that may pose challenges if you try to adapt an existing application, especially with an inexperienced team.
  • Dependency injection is far less important in functional as opposed to object-oriented programming. Functional programming is becoming a more common approach when testability, fault recovery, and parallelism are key requirements.
  • Type registration and resolving do incur a runtime penalty: very negligible for resolving, but more so for registration. However, the registration should only happen once.
Dn178469.note(en-us,PandP.30).gifJana says:
Jana Programming languages shape the way we think and the way we code. For a good exploration of the topic of dependency injection when the functional programming model is applied, see the article “Dependency Injection Without the Gymnastics” by Tony Morris.

Note

According to Mark Seeman, using dependency injection “can be dangerous for your career because it may increase your overall knowledge of good API design. Once you learn how proper loosely coupled code can look like, it may turn out that you will have to decline lots of job offers because you would otherwise have to work with tightly coupled legacy apps.”
What are the downsides to using Dependency Injection? On StackOverflow.

Summary

In this chapter, you've seen how dependency injection differs from patterns such as the factory patterns and the service locator pattern by adopting a push model, whereby some other class or component is responsible for instantiating the dependencies and injecting them into your object’s constructor, properties, or methods. This other class or component is now responsible for composing the application by building the complete object graph, and in some cases it will also be responsible for managing the lifetime of the objects that it creates. In the next chapter, you'll see how you can use the Unity container to manage the instantiation of dependent objects and their lifetime.

Next Topic | Previous Topic | Home | Community