Compose, Don’t Combine
Sometimes a single word can land us in hot water. It happened to me with an article on Bloated Constructors. One of the solutions I proposed was:
Bottom-Up / Combine Dependencies: Can some of the dependent services and repositories be isolated and managed together via a new high-level class? E.g. In CustomerEmailService, the IEmailTemplateXXX interfaces might be fertile ground for such a rationalisation.
The word that got me into trouble in the above sentence is ‘combine‘.
My friend Andy pointed out that we would significantly decrease ease of maintenance if IEmailTemplateRepository
, IEmailTemplateConfiguration
and IEmailTemplateService
implementations were combined into a single class EmailTemplateManager
. Of course, Andy is right—it would probably be suboptimal to consolidate these classes. Hat tip to Andy.
There are probably good reasons why the interfaces and their corresponding implementers are kept separate. Unfortunately, we can’t tell much about implementation behaviour from a mere interface.
In any case, what I meant to say is that the functionality provided by the IEmailTemplateXXX
interfaces could potentially be composed into another high-level class. It would depend on whether that class had a good, singular reason to exist, i.e., a cohesive class. Let’s say that EmailTemplateBuilder
is such a cohesive type utilising the functionality of IEmailTemplateRepository
, IEmailTemplateConfiguration
, and IEmailTemplateService
.
The constructor signature for EmailTemplateBuilder
might look like this:
public EmailTemplateBuilder(IEmailTemplateRepository emailTemplateRepository, IEmailTemplateConfiguration emailTemplateConfig, IEmailTemplateService emailTemplateService)
Here is a reproduction of our original, 10-parameter bloated constructor for CustomerEmailService
:
public CustomerEmailService(ICustomerRepository customerRepository, ICustomerService customerService, ICustomerTransactionRepository customerTransactionRepository, IEmailTemplateRepository emailTemplateRepository, IEmailTemplateConfiguration emailTemplateConfig, IEmailTemplateService, emailTemplateService, IReportingService reportingService, IConfigurationService configService, ILocalTimeProvider localTimeProvider, ILogger logger)
If EmailTemplateBuilder
implements IEmailTemplateBuilder
, and we managed to switch out IEmailTemplateBuilder
for 3 IEmailTemplateXXX
interfaces in CustomerEmailService
, then the constructor becomes less bloated, contracting to 8 parameters:
public CustomerEmailService(ICustomerRepository customerRepository, ICustomerService customerService, ICustomerTransactionRepository customerTransactionRepository, IEmailTemplateBuilder emailTemplateBuilder, IReportingService reportingService, IConfigurationService configService, ILocalTimeProvider localTimeProvider, ILogger logger)
Yes, this CustomerEmailService
constructor is still bloated. There is more work to be done. However, it’s a noticeable improvement.
By the way, the title of this article seems to suggest that composition is always the right way to go—not so. At times it might make perfect sense to merge the code of two classes. For example, a ShoppingCartManager
class and a ShoppingCartHelper
class, each performing a complementary part (but not the whole) of a shopping cart’s functionality, say, might be absorbed into a cohesive ShoppingCart
class.
Unfortunately, there are no rules about how to construct the perfect abstraction or class every time. That does not mean anything goes, and we are entirely on our own. On the contrary—we have fabulous tools like The SOLID Principles. Sometimes we combine, sometimes we compose, but as long as the code ‘makes sense’—our classes are small and cohesive—we are on a good path.
Leave a Reply
Want to join the discussion?Feel free to contribute!