This manual work makes me think that something is wrong with my design.
Probably there is. It is difficult to be specific with the examples you provided, but often this problem occurs for one of the following reasons:
- You use base classes to implement end-to-end tasks (such as logging, transaction processing, security checks, validation, etc.) instead of decorators.
- You use base classes to implement collaborative behavior instead of placing this behavior in separate components that can be introduced.
Base classes are often overused to add cross-cutting issues to the implementation. In this case, the base class will soon become an object of God : a class that does too much and knows too much. It violates the principle of single responsibility (SRP), causing it to change frequently, become complex and make testing difficult.
The solution to this problem is to remove the base class together and use multiple decorators instead of the same base class. You must write a decorator for each question. Thus, each decorator is small and focused and has only one reason for change (one responsibility). Using a project based on common interfaces , you can create common decorators that can wrap a whole set of types that are related to architecture. For example, one decorator that can wrap all implementation implementations in your system or one decorator that wraps all requests in the system with a certain return type.
Base classes are often overused to contain a set of unrelated functions that are reused by multiple implementations. Instead of putting all this logic into a base class (turning a base class into a service nightmare), this functionality should be extracted into several services, the implementation of which may be dependent.
When you start reorganizing such a project, you will often see that implementations begin to get a lot of dependencies, an anti-pattern called an excessive insertion of the constructor. Exaggerating the constructor is often a sign that the class violates SRP (which makes it difficult and difficult to test). But moving logic from a base class to a dependency did not complicate the implementation task, and in fact, a model with a base class had the same problems, but with the difference that the dependencies were removed.
When you look closely at code in implementations, you often see some kind of repeating code pattern. Similarly, multiple implementations use the same set of dependencies. This is a signal of lack of abstraction. This code and its dependencies can be extracted to the aggregation service. Aggregate services reduce the number of dependencies required for implementation, and wrap the overall behavior.
Using Aggregate services looks like a wrapper that @SimonWhitehead talks about in the comments, but note that aggregated services are an abstraction of both dependencies and behavior. If you create a โcontainerโ of dependencies and expose these dependencies through public properties for the implementation to use them, you do not reduce the number of dependencies on which the implementation depends, and you do not reduce the complexity of this class, and you do not make the implementation easier to test. On the other hand, aggregated services reduce the number of dependencies and the complexity of the class, which simplifies understanding and testing.
Subject to these rules, in most cases it is not required to have a base class.