7

If I have a base class with services injected via constructor dependencies: Is it possible to declare the constructor of the subclass without using : base (params)?

public MyBaseClass
{
   private IServiceA _serviceA;
   private IServiceB _serviceB;
   private IServiceC _serviceC;

   public MyBaseClass(null, null, null)
   public MyBaseClass(IServiceA serviceA, IServiceB serviceB, IServiceC serviceC)
   {
       _serviceA = serviceA;
       _serviceB = serviceB;
       _serviceC = serviceC;
   }
}

And a subclass with some extra dependencies injected:

public MySubClassA : MyBaseClass
{
   private IServiceD _serviceD;

   public MySubClassA (null, null, null, null)
   public MySubClassA (IServiceA serviceA, IServiceB serviceB, 
                       IServiceC serviceC, IServiceD serviceD)
          : base (serviceA, serviceB, serviceC)
   {
       _serviceD = serviceD;
   }
}

The problem here is that I have multiple subclasses, only 10 or so right now, but the number will increase. Every time I need to add another dependency to the base class, I have to go through each and every subclass and manually add the dependency there as well. This manual work makes me think that there is something wrong with my design.

So is it possible to declare the constructor of MyBaseClassA without having the services required by the base class in the sub classes' constructor? eg so that the constructor of MyBaseClassA only has this much simpler code:

   public MySubClassA (null)
   public MySubClassA (IServiceD serviceD)
   {
       _serviceD = serviceD;
   }

What do I need to change in the base class so that the dependency injection occurs there and does not need to be added to the sub classes as well? I'm using LightInject IoC.

JK.
  • 21,477
  • 35
  • 135
  • 214
  • 2
    Can't your IoC container inject to properties instead of constructor? I think it would be much simpler, especially if you are deeply nested classes and lots of injection. – Simon Belanger Jul 01 '13 at 02:02
  • 2
    In addition to the other Simon's idea.. you can throw a wrapper around these dependencies (a container of dependencies, if you will) as another solution. Although I prefer the property approach. – Simon Whitehead Jul 01 '13 at 02:07
  • @SimonBelanger everything else in the app uses param injection. I think that property injection was tried in one part of the app and had some problem that I've since forgotten - I'll have to revisit that. – JK. Jul 01 '13 at 02:19
  • @SimonWhitehead yes a wrapper might work, I can try to create a dependancy model and then there will only be one param that goes to the base class, and if I need to add more dependencies I just add them to the model, and all the subclasses will not need to change – JK. Jul 01 '13 at 02:20
  • 1
    Regardless, having that much subclass is a definitely a hint to a design oversight (to answer one of your question). @SimonWhitehead option's would be simpler too, but then you bind yourself to a Service Locator. – Simon Belanger Jul 01 '13 at 02:21
  • @SimonBelanger Not really (RE: Service Locator). The wrapper can be instantiated by the container, which in turn instantiates the dependencies in the wrapper. It relies entirely on the container still. – Simon Whitehead Jul 01 '13 at 02:28
  • I actually really like the wrapper approach, it keeps construction time injection and makes it really easy to change the dependancy set. Ive always thought of param injection as a bit of a last resort – undefined Jul 01 '13 at 02:38

1 Answers1

17

This manual work makes me think that there is something wrong with my design.

There probably is. It's hard to be specific with the examples you gave, but often such problem is caused by one of the following reasons:

  • You use base classes to implement cross-cutting concerns (such as logging, transaction handling, security checks, validation, etc) instead of decorators.
  • You use base classes to implement shared behavior instead of placing that behavior in separate components that can be injected.

Base classes are often abused for adding cross-cutting concerns to implementations. In that case, the base class soon becomes a God object: a class that does too much and knows too much. It violates the Single Responsibility Principle (SRP), causing it to change often, get complex, and making it hard to test.

The solution to that problem is to remove the base class all together and use multiple decorators instead of a single base class. You should write a decorator per cross-cutting concern. This way each decorator is small and focused, and has only one reason to change (a single responsibility). By using a design that is based on generic interfaces you can create generic decorators that can wrap a whole set of types that are architecturally related. For instance, one single decorator that can wrap all use case implementations in your system, or one decorator that wrap all queries in the system that has a certain return type.

Base classes are often abuses to contain a set of unrelated functionality that is reused by multiple implementations. Instead of placing all this logic in a base class (making the base class grow into a maintenance nightmare), this functionality should be extracted into multiple services that implementations can depend on.

When you start to refactor towards such design, you'll often see that implementations start getting many dependencies, an anti-pattern which is called constructor overinjection. Constructor overinjection is often a sign that a class violates the SRP (making it complex and hard to test). But moving the logic from the base class to dependencies didn't make the implementation harder to test, and in fact the model with the base class had the same problems, but with the difference that the dependencies were tucked away.

When looking closely at the code in the implementations, you will often see some kind of recurring code pattern. Multiple implementations are using the same set of dependencies in the same way. This is a signal that an abstraction is missing. This code and its dependencies can be extracted into an aggregate service. Aggregate services reduce the amount of dependencies an implementation needs, and wraps common behavior.

Using Aggregate services looks like the 'wrapper' that @SimonWhitehead talks about in the comments, but please note that aggregate services are about abstracting both dependencies and behavior. If you create a 'container' of dependencies and expose those dependencies through public properties for implementations to use them, you are not lowering the number of dependencies an implementation depends upon and you are not lowering the complexity of such class and you're not making that implementation easier to test. Aggregate services on the other hand do lower the number of dependencies and the complexity of a class, making it easier to grasp and test.

When following these rules, there won't be a need to have a base class in most cases.

Steven
  • 166,672
  • 24
  • 332
  • 435
  • +1 to introduce cross-cutting concerns and solution using decorators. (actually, it should +2) – Fendy Jul 02 '13 at 09:46
  • Every now and again I think it would be good idea to clean up and create "god class" - Then I come across this answer, again, and realise that is a stupid idea :D – Piotr Kula Jul 03 '17 at 08:12