Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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 classes should depend on abstractions. In practice this means classes should be derived from interfaces. All fine so far.

Next my understanding of the open/closed principle is that after a certain cut off point, you should not alter the contents of a class, but should extend and override. This makes sense so far to me.

So given the above, I would end up with something like this:

public interface IAbstraction
{
    string method1(int example);
}

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

and then at time T, method1 now needs to add " ExtraInfo" onto its returned value. Rather than altering the current implementation, I would create a new class that extends Abstraction and make it do what I needed, as follows.

public Class AbstractionV2 : Abstraction 
{
   public override string method1(int example)
   {
       return example.toString() + " ExtraInfo";
   }
}

And I can see the reason for doing this is that only the code I want to call this updated method will call it, and the rest of the code will call the old method.

All makes sense to me - and I assume my understanding is correct??

However, I am also using dependency injection (simple injector), so my implementations are never through a concrete class, but instead are through my DI configuration, as follows:

container.Register<IAbstraction, Abstraction>();

The issue here is that under this setup, I can either update my DI config to be:

container.Register<IAbstraction, AbstractionV2>();

In which case all instance will now call the new method, meaning I have failed to achieve not changing the original method.

OR

I create a new interface IAbstractionV2 and implement the updated functionality there - meaning duplication of the interface declaration.

I cannot see any way around this - which leads me to wonder if dependency injection and SOLID are compatible? Or am I missing something here?

like image 999
Alex Avatar asked Oct 21 '25 04:10

Alex


2 Answers

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.

like image 66
Scott Hannen Avatar answered Oct 23 '25 20:10

Scott Hannen


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..."

like image 22
Yuli Bonner Avatar answered Oct 23 '25 18:10

Yuli Bonner



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!