سؤال

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.

هل كانت مفيدة؟

المحلول

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.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top