How does the SOLID open/closed principle fit in with Dependency Injection and dependency inversion

后端 未结 3 1613
夕颜
夕颜 2021-02-06 17:09

I am starting to apply SOLID principles, and am finding them slightly contradictory. My issue is as follows:

My understanding of dependency inversion principle is that

相关标签:
3条回答
  • 2021-02-06 17:23

    Modules become closed to modification once they are referenced by other modules. What becomes closed is the public API, the interface. Behavior can be changed via polymorphic substitution (implementing the interface in a new class and injecting it). Your IoC container can inject this new implementation. This ability to polymorphically substitute is the 'Open to extension' part. So, DIP and Open/Closed work together nicely.

    See Wikipedia:"During the 1990s, the open/closed principle became popularly redefined to refer to the use of abstracted interfaces..."

    0 讨论(0)
  • 2021-02-06 17:45

    Addressing the exact problem you mentioned:

    You have classes that depend on IAbstraction and you've registered an implementation with the container:

    container.Register<IAbstraction, Abstraction>();
    

    But you're concerned that if you change it to this:

    container.Register<IAbstraction, AbstractionV2>();
    

    then every class that depends on IAbstraction will get AbstractionV2.

    You shouldn't need to choose one or the other. Most DI containers provide ways that you can register more than one implementation for the same interface, and then specify which classes get which implementations. In your scenario where only one class needs the new implementation of IAbstraction you might make the existing implementation the default, and then just specify that one particular class gets a different implementation.

    I couldn't find an easy way to do this with SimpleInjector. Here's an example using Windsor:

    var container = new WindsorContainer();
    container.Register(
        Component.For<ISaysHello, SaysHelloInSpanish>().IsDefault(),
        Component.For<ISaysHello, SaysHelloInEnglish>().Named("English"),
        Component.For<ISaysSomething, SaysSomething>()
            .DependsOn(Dependency.OnComponent(typeof(ISaysHello),"English")));
    

    Every class that depends on ISaysHello will get SaysHelloInSpanish except for SaysSomething. That one class gets SaysHelloInEnglish.

    UPDATE:

    The Simple Injector equivalent is the following:

    var container = new Container();
    
    container.Register<ISaysSomething, SaysSomething>();
    
    container.RegisterConditional<ISayHello, SaysHelloInEnglish>(
        c => c.Consumer.ImplementationType == typeof(SaysSomething));
    
    container.RegisterConditional<ISayHello, SaysHelloInSpanish>(
        c => c.Consumer.ImplementationType != typeof(SaysSomething))
    
    0 讨论(0)
  • 2021-02-06 17:46

    TL;DR

    • When we say that code is "available for extension" that doesn't automatically mean that we inherit from it or add new methods to existing interfaces. Inheritance is only one way to "extend" behavior.
    • When we apply the Dependency Inversion Principle we don't depend directly on other concrete classes, so we don't need to change those implementations if we need them to do something different. And classes that depend on abstractions are extensible because substituting implementations of abstractions gets new behavior from existing classes without modifying them.

    (I'm half inclined to delete the rest because it says the same thing in lots more words.)


    Examining this sentence may help to shed some light on the question:

    and then at time T, method1 now needs to add " ExtraInfo" onto its returned value.

    This may sound like it's splitting hairs, but a method never needs to return anything. Methods aren't like people who have something to say and need to say it. The "need" rests with the caller of the method. The caller needs what the method returns.

    If the caller was passing int example and receiving example.ToString(), but now it needs to receive example.ToString() + " ExtraInfo", then it is the need of the caller that has changed, not the need of the method being called.

    If the need of the caller has changed, does it follow that the needs of all callers have changed? If you change what the method returns to meet the needs of one caller, other callers might be adversely affected. That's why you might create something new that meets the need of one particular caller while leaving the existing method or class unchanged. In that sense the existing code is "closed" while at the same time its behavior is open to extension.

    Also, extending existing code doesn't necessarily mean modifying a class, adding a method to an interface, or inheriting. It just means that it incorporates the existing code while providing something extra.

    Let's go back to the class you started with.

    public Class Abstraction : IAbstraction
    {
         public virtual string method1(int example)
         {
             return example.toString();
         }
    }
    

    Now you have a need for a class that includes the functionality of this class but does something different. It could look like this. (In this example it looks like overkill, but in real-world example it wouldn't.)

    public class SomethingDifferent : IAbstraction
    {
         private readonly IAbstraction _inner;
    
         public SomethingDifferent(IAbstraction inner)
         {
             _inner = inner;
         }
    
         public string method1(int example)
         {
             return _inner.method1 + " ExtraInfo";
         }
    }
    

    In this case the new class happens to implement the same interface, so now you've got two implementations of the same interface. But it doesn't need to. It could be this:

    public class SomethingDifferent
    {
         private readonly IAbstraction _inner;
    
         public SomethingDifferent(IAbstraction inner)
         {
             _inner = inner;
         }
    
         public string DoMyOwnThing(int example)
         {
             return _inner.method1 + " ExtraInfo";
         }
    }
    

    You could also "extend" the behavior of the original class through inheritance:

    public Class AbstractionTwo : Abstraction
    {
         public overrride string method1(int example)
         {
             return base.method1(example) + " ExtraInfo";
         }
    }
    

    All of these examples extend existing code without modifying it. In practice at times it may be beneficial to add existing properties and methods to new classes, but even then we'd like to avoid modifying the parts that are already doing their jobs. And if we're writing simple classes with single responsibilities then we're less likely to find ourselves throwing the kitchen sink into an existing class.


    What does that have to do with the Dependency Inversion Principle, or depending on abstractions? Nothing directly, but applying the Dependency Inversion Principle can help us to apply the Open/Closed Principle.

    Where practical, the abstractions that our classes depend on should be designed for the use of those classes. We're not just taking whatever interface someone else has created and sticking it into our central classes. We're designing the interface that meets our needs and then adapting other classes to fulfill those needs.

    For example, suppose Abstraction and IAbstraction are in your class library, I happen to need something that formats numbers a certain way, and your class looks like it does what I need. I'm not just going to inject IAbstraction into my class. I'm going to write an interface that does what I want:

    public interface IFormatsNumbersTheWayIWant
    {
        string FormatNumber(int number);
    }
    

    Then I'm going to write an implementation of that interface that uses your class, like:

    public class YourAbstractionNumberFormatter : IFormatsNumbersTheWayIWant
    {
        public string FormatNumber(int number)
        {
            return new Abstraction().method1 + " my string";
        }
    }
    

    (Or it could depend on IAbstraction using constructor injection, whatever.)

    If I wasn't applying the Dependency Inversion principle and I depended directly on Abstraction then I'd have to figure out how to change your class to do what I need. But because I'm depending on an abstraction that I created to meet my needs, automatically I'm thinking of how to incorporate the behavior of your class, not change it. And once I do that, I obviously wouldn't want the behavior of your class to change unexpectedly.

    I could also depend on your interface - IAbstraction - and create my own implementation. But creating my own also helps me adhere to the Interface Segregation Principle. The interface I depend on was created for me, so it won't have anything I don't need. Yours might have other stuff I don't need, or you could add more in later.

    Realistically we're at times just going to use abstractions that were given to us, like IDataReader. But hopefully that's later when we're writing specific implementation details. When it comes to the primary behaviors of the application (if you're doing DDD, the "domain") it's better to define the interfaces our classes will depend on and then adapt outside classes to them.

    Finally, classes that depend on abstractions are also more extensible because we can substitute their dependencies - in effect altering (extending) their behavior without any change to the classes themselves. We can extend them instead of modifying them.

    0 讨论(0)
提交回复
热议问题