C# Dependency injection side effect (two step init

2019-04-15 22:22发布

This question already has an answer here:

I'm working on a project in which my constructors contain - only - behavioral dependencies. i.e. I never pass values / state.

Example:

class ProductProcessor : IProductProcessor
{
   public double SomeMethod(){ ... }
}
class PackageProcessor
{
   private readonly IProductProcessor _productProcessor;
   private double _taxRate;

   public PackageProcessor(IProductProcessor productProcessor)
   {
        _productProcessor = productProcessor;
   }

   public Initialize(double taxRate)
   {
       _taxRate = taxRate;
       return this;
   }

   public double ProcessPackage()
   {
       return _taxRate * _productProcessor.SomeMethod();
   }

}

In order to pass state, it was decided to include a second step (a call to Initialize).

I know we can configure this as a named parameter in the IoC Container config class, however, we did not like the idea of creating "new namedParameter(paramvalue)'s" in the configuration file as it makes it unnecessarily unreadable and creates a future maintenance pain spot.

I've seen this pattern in more than one place.

Question: I read some consider this two step initialization an anti-pattern. If that is the consensus, wouldn't this imply a limitation / weakness of sorts in the approach of dependency injection via a IoC container?

Edit: After looking into Mark Seeman's suggestion:

and the answers to this one, I have a few comments: Initialize/Apply : Agree on it being an anti pattern / smell. Yacoub Massad: I agree IoC containers are a problem when it comes to primitive dependencies. Manual (poor man's) DI, as described here sounds great for smaller or architecturally stable systems but I think it could become very hard to maintain a number of manually configured composition roots.

Options: 1)Factories as dependencies (when run time resolution is required) 2) Separate stateful object from pure services as described here.

(1): This is what I had been doing but I realized that there is a potential to incur into a another anti-pattern: the service locator. (2): My preference for my particular case is this one about this one as I can cleanly separate both types. Pure services are a no brainer - IoC Container, whereas stateful object resolution will depend on whether they have primitive dependencies or not.

Every time I've 'had' to use dependency injection, it has been used in a dogmatic way, generally under the orders of a supervisor bent on applying DI with IoC container at any cost.

2条回答
一夜七次
2楼-- · 2019-04-15 22:47

I read some consider this two step initialization an anti-pattern

The Initialize method leads to Temporal Coupling. Calling it an anti-pattern might be too strict, but it sure is a Design Smell.

How to provide this value to the component depends on what type of value it is. There are two flavors: configuration values and runtime values:

  • Configuration Values: If it is a constant/configuration value that won't change during the lifetime of the component, the value should be injected into the constructor directly.

  • Runtime values: In case the value changes during runtime (such as request specific values), the value should not be provided during initialization (neither through the constructor nor using some Initialize method). Initializing components with runtime data actually IS an anti-pattern.

I partly agree with @YacoubMassad about the configuration of primitive dependencies using DI containers. The APIs provided by containers do not enable setting those values in a maintainable way when using auto-wiring. I think this is mainly caused by limitations in C# and .NET. I struggled a long time with such API while designing and developing Simple Injector, but decided to leave out such API completely, because I didn't find a way to define an API that was both intuitive and lead to code that was easy maintainable for the user. Because of this I usually advise developers to extract the primive types into Parameter Objects and instead register and inject the Parameter Object into the consuming type. In other words, a TaxRate property can be wrapped in a ProductServiceSettings class and this Parameter Object can be injected into ProductProcessor.

But as I said, I only partly agree with Yacoub. Although it is more practical to compose some of your objects by hand (a.k.a. Pure DI), he implies that this means you should abandon DI containers completely. IMO that is too strongly put. In most of the applications I write, I batch-register about 98% of my types using the container, and I hand-wire the other two 2%, because auto-wiring them is too complex. This gives in the context of my applications the best overall result. Of course, you're mileage may vary. Not every application really benefits from using a DI container, and I don't use a container myself in all the application I write. But what I always do however, is apply the Dependency Injection pattern and the SOLID principles.

查看更多
不美不萌又怎样
3楼-- · 2019-04-15 22:47

The taxRate in your example is a Primitive Dependency. And primitive dependencies should be injected normally in the constructor like the other dependencies. Here is how the constructor would look like:

public PackageProcessor(IProductProcessor productProcessor, double taxRate)
{
    _productProcessor = productProcessor;
    _taxRate = taxRate;
}

The fact that DI containers do not nicely/easily support primitive dependency is a problem/weakness of DI containers in my opinion.

In my opinion, it is better to use Pure DI for object composition instead of a DI container. One reason is that it supports easier injection of primitive dependencies. See this article also for another reason.

Using the Initialize method has some problems. It makes the construction of an object more complex by requiring the invocation of the Initialize method. Also, a programmer might forget to call the Initialize method, which leaves your object in an invalid state. This also means that the taxRate in this example is a hidden dependency. Programmers wouldn't know that your class depends on such primitive dependency by simply looking into the constructor.

Another problem with the Initialize method is that it might be called twice with different values. Constructors on the other hand, ensure that dependencies do not change. You would need to create a special boolean variable (e.g. isInitialized) to detect if the Initialize method has been called already. This just complicates things.

查看更多
登录 后发表回答