3

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?

4
  • "meaning I have failed to achieve not changing the original method." I'm not sure that's the case. The original method is sitting there as an unmodified Abstraction class. Commented Feb 6, 2018 at 17:11
  • That said, if your issue is only wanting to change the implementation used for a limited number of consumers, you can add an IAbstractionV2 : IAbstraction interface for your AbstractionV2 class to implement, then inject AbstractionV2 through that. Commented Feb 6, 2018 at 17:16
  • Do you only ever have one implementation of IAbstraction in use globally? It looks like nothing ever depends on Abstraction.method1 directly so you can simply remove it?
    – Lee
    Commented Feb 6, 2018 at 17:17
  • Do note that the OCP does not forbid changing existing code, but the idea is to prevent sweeping changes. So once you see that a change causes many classes to change, that is an indication that OCP is violated.
    – Steven
    Commented Feb 7, 2018 at 8:14

3 Answers 3

3

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
2

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))
2
  • If I may, I would like to update this answer to add an example of how to do this with Simple Injector. You might be surprised how easy this actually is (hint: it's done using RegisterConditional).
    – Steven
    Commented Feb 7, 2018 at 8:08
  • @Steven - sure, go for it. I started poking around looking for it but never got too far. Commented Feb 7, 2018 at 13:31
2

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.