次の方法で共有


State Your Dependency Intent

There are several different ways to implement Dependency Injection (DI), and Martin Fowler describes four of them in his excellent article on IoC/DI. In this article, the first three approaches (Constructor, Property, and Interface Injection) are mainly described as a background for introducing the Service Locator pattern. In Fowler's example, a generic Service Locator is a registry (basically an in-memory table), but these days you most commonly see it implemented as a factory.

Consider this simple interface:

 public interface IMyInterface
 {
     void DoStuff();
 }

Here's a class that uses a Service Locator to get an instance of IMyInterface:

 public class ImplicitConsumer
 {
     public void PerformOperation()
     {
         IMyInterface mi = ServiceFactory.Create<IMyInterface>();
         mi.DoStuff();
     }
 }

The ServiceFactory class has a static method that returns an instance of the requested interface. Although not explicitly shown, the ImplicitConsumer class has a default constructor, since no constructor is defined and the C# compiler then automatically creates one.

Now, imagine yourself in a situation where you need to consume an instance of the ImplicitConsumer class and call its PerformOperation method. Also imagine that you have just been handed the class in a binary form, with documentation, but without source code. In this scenario, you would probably write code like this:

 ImplicitConsumer ic = new ImplicitConsumer();
 ic.PerformOperation();

Writing the first line is very straightforward, since there's only one way to create a new instance of the class. Next, with the ic instance, IntelliSense will quickly help you to find and invoke the PerformOperation method, and that's it: The code compiles and you are happy.

At run-time, however, this code is going to fail, since the Service Locator has not been configured. At this point, you may resort to the documentation, and if you are lucky, the documentation will tell you that the PerformOperation method expects the Service Locator to be configured to return an instance of IMyInterface. If you aren't so lucky, you will have to fire up Reflector to figure out what to do.

Depending on the Service Locator's implementation, this configuration may be done in the configuration file or in code. Here's how it might look in code:

 ServiceFactory.Preset<IMyInterface>(new StubMyInterface());
  
 ImplicitConsumer ic = new ImplicitConsumer();
 ic.PerformOperation();

Here we have what looks like two pieces of totally unrelated code, yet they are very closely related at run-time. If you came by this code without prior knowledge, you'd probably mistake the first line of code for a piece of lava flow and delete it. If you were the author of those three lines, you might attempt to protect yourself from this risk by applying a comment to the first line, but that would be an apology.

In my opinion, an API should always strive to steer developers in the right direction. With respect to DI, the API should clearly state its intent to consume a particular dependency. Constructor Injection does this very explicitly:

 public class ExplicitConsumer
 {
     private IMyInterface mi_;
  
     public ExplicitConsumer(IMyInterface mi)
     {
         if (mi == null)
         {
             throw new ArgumentNullException("mi");
         }
         this.mi_ = mi;
     }
  
     public void PerformOperation()
     {
         this.mi_.DoStuff();
     }
 }

With this implementation, any developer is forced to do the right thing. There's only a single constructor, and IntelliSense will show that it expects an instance of IMyInterface. While a developer could pass null as a parameter, he or she would do so in spite of the knowledge that was just communicated by IntelliSense (and there would still be a run-time error). There would be no need to read the documentation or fire up Reflector, because the class makes it very clear that it needs a working instance of IMyInterface to perform its work, and as a developer, you must supply it:

 ExplicitConsumer ec = 
     new ExplicitConsumer(new StubMyInterface());
 ec.PerformOperation();

The only drawback of Constructor Injection that I have ever been able to identify is the need to initialize all dependencies at once if you have a complex hierarchy of dependencies, as described in my former post. If lazy initialization is a necessity, you can use Provider Injection, which is a variation of Constructor Injection. Although this is currently my favorite DI strategy, it's less well-known and more difficult to explain.

In any case, the main point is that if a component expects to consume a dependency which will be supplied at run-time, it should clearly state that intent through its API, instead of relying on out-of-band discovery mechanisms. Although the API may end up looking more complex, it's ensuring that mistakes are much harder to make. Even if there's a slightly more pronounced learning curve to get started with an API that uses Constructor Injection, it's easier to use in the long run. A developer with no prior knowledge of the component will sooner be able to produce code that compiles with a component relying on a Service Locator, but he or she will sooner be able to produce code that works with Constructor Injection.

Comments

  • Anonymous
    June 02, 2007
    You're absolutely right. However, when I employ a certain design principle (http://udidahan.weblogs.us/2007/05/12/first-principle-of-design-refined/), I don't find that kind of problem occurring, even with setter injection. If no one can get at the concrete class without going through the "service locator", you're home free. At least, that's what's been working for me.

  • Anonymous
    June 02, 2007
    The comment has been removed

  • Anonymous
    June 03, 2007
    I am working on a rather simple DI issue. Constructor DI in my mind is the best way "state dependency intent". The problem I have with setter DI is that it is a setter, and could be overwritten, sure there are mesures that can be taken but in the end even I may accidentially call the setter and create some hard to track and dangerous issues. I understand the statement that interfaces define behavior indepenent of contruction, and even Fowler boils it down to tradeoffs. I view this like life before generics with no covariant return types, some ugly code in that era, and the new overload keyword was downright dangerous.  In this era, I was told to use the new keyword to work around the lack of covariance. Now we have generics and code is elegant, clean, and feels right. What does the BCL need to allow us to express DI in an elegant and safe manner? Great posts, and good discussion.

  • Anonymous
    June 03, 2007
    Hi Mike Thank you for your comment. What about Constructor Injection do you think is not elegant and safe?

  • Anonymous
    June 04, 2007
    That's exacly what I was saying, Constuctor Injection(CDI) is both "elegant and save".   The problem is implementing CDI when using an interface, looking back to the first question about the inability to explicitly expess DI intent on an interface, you can't.   If you are decoupling layers, you use an interface, and CDI is not supported.  You have to use Setter DI which is less elegant and intuitive. Setter DI can also be dangerous as it can be set twice which is not the intent of DI. When I read Fowlers paper, and put DI in practice in an interface, it reminds me of the new overload keyword prior to generics to work around the lack of covariance in the BCL. Thanks for the discussion...

  • Anonymous
    June 04, 2007
    Oh, I think I now see what you are saying: If you really, truly want to model a dependency constraint into a contract, you can only do so by Property Injection, because you can model a property on the interface, but not a constructor. I still think it's a Really Bad Idea™ to model dependency chains in contracts, but that aside, this brings up an interesting point of Krzysztof Cwalina's: Interfaces don't define contracts. If, on the other hand, you were to model your contracts as classes, you could have an Initialize method that takes any dependencies as parameters. Then, any virtual members of the class would be protected hook methods that public clients would only be able to invoke via public safeguards that check whether the instance has been properly initialized. You see this pattern in many places in the BCS, particularly in the design-time framework. Why you would want to do this with a dependency hierarchy still eludes me, though :)

  • Anonymous
    June 05, 2007
    Mark, In multi-threaded environments, it often makes sense to create a new "service" object, rather than have a single object be thread safe. Also, when using these same principles for the interaction between the Service Layer and the Domain Model, decoupling the class creation enables you to "inject" other behaviors into the creation process. An example of this scenario can be found here: http://udidahan.weblogs.us/2007/04/23/fetching-strategy-design/ On the issue of setters and CDI with interfaces, interfaces should not expose these setters. The client code shouldn't be aware of the dependencies of its service object. The DI framework would wire that up behind the scenes - regardless of whether you used constructors or setters. On Krzysztof's post, interfaces don't NECESSARILY define a contract, but they CAN. That's where the value lies. Mike D, Check out my post  (http://udidahan.weblogs.us/2007/05/12/first-principle-of-design-refined/), to see how clients, services, and interfaces work together.

  • Anonymous
    June 06, 2007
    Hi Udi Thank you for your comments. Yes, I agree that multi-threading tend to make our lives more difficult, and that we should strive to use a service per thread, instead of developing thread-safe services (which is harder). In both WCF and ASP.NET, this is pretty easy to ensure, since you can just create your service hierarchy at each request, since requests are (typically) handled by a new object instance (of Page, or your service, or whatever). There will obviously be a performance price to pay, but some of that can be alleviated be creating a pool of service hierarchies. In any case, it's a good point. The ability to inject behavior between consumer and service can be implemented with CDI using the Decorator pattern, but I agree that using a configurable Pipeline as part of a Service Locator is a bit cleaner. I totally agree with your point on Property Injection and setters, and I was thinking the same thing several times while writing the other comments, but never got around to explicitly state it :)