Separated Interface vs. Strategy
In this topic, I will discuss a common dilemma in software design, particularly when designing relationships and interactions between different software layers and components.
Here is the problem: let's say that we have UI layer interacting with Domain layer. In particular, Client class (member of UI layer) has a dependency on Service class (member of Domain layer). Visually, this looks as follows:
We need to make Client class unit-testable. In order to achieve this, we need to abstract away Client's dependencies, and among them (as you might guess) dependency on Service class as well.
Solution to the given problem is to have an interface representing Service, which we would refer from the Client class. However, this can be achieved by doing one of the two:
- Extract IService interface from Service class, make Service implement IService, and make Client depend on IService. Outcomes: IService becomes separated interface, which belongs to Domain layer.
- Define IService interface that exposes behavior needed by Client class, make Client depend on IService, and have a concrete implementation of IService adapt it to Service class. Outcome: IService becomes strategy interface, which belongs to UI layer.
While the first approach (separated interface) might seem better, it is the wrong one. First of all, you just decided to extract an interface from Service, which means you didn't really have a good reason to do so until this problem revealed. Next, you are increasing the number of published interfaces by Domain layer, thus creating more maintenance and stability issues - the less the number of the published interfaces, the better the stability of the software layer.
Here is the diagram of the correct approach (followed by few advantages of it):
First, Client is limited to the knowledge of IService interface, which potentially is thinner than all the capabilities exposed by Service. Next, we just made Service (and thus Domain's capabilities) replaceable without referencing Domain's published interfaces. And finally, Domain's functionality is not dictated by UI; instead, UI adapts to the Domain's capabilities by translating them into UI's needs through DomainService adapter class.
One more benefit - obviously, Client class became unit-testable - which we wanted to achieve from the beginning.