Written By: Steve Zagieboylo, Senior Architect at Calavista
This is the fifth part in the series, “Technical Debt Patterns.”
Some important central class has several not-really-related sets of responsibilities, making it huge.
Robert C. Martin coined the phrase Single-Responsibility Principle (SRP) as one of his core guidelines in object-oriented design. This states that “every module, class or function in a computer program should have responsibility over a single part of that program’s functionality, which it should encapsulate.” This does not mean that every class should have only a single internal value, but that every class should have a single core purpose, one thematic element over which it is responsible.
Often you’ll start with the best of intentions, where you consider one class, perhaps an abstract base class, to encapsulate some significant concept which may be implemented in concrete subclasses. In the course of development, you realize that there is some functionality that is common to these base classes that might be unrelated, or only marginally related to the core theme of the class, but it is a convenient place to put the added functionality. Perhaps related to persistence of this set of classes, or a validation check (which seems related after all, it is the validation of the representation of the core theme). Over time, this secondary responsibility turns out to need a couple of functions, not just the one, and suddenly the class has several major responsibilities, seemingly grown from nowhere.
One symptom that can lead you to consider this pattern is that a particular class file is getting a lot of merge conflicts. It seems that frequently several people are editing the same file, so there are frequently overlaps. An alternative symptom is that people have avoided this issue by clearly delineating, in comments, the code for one responsibility vs. the other (or others). This is a clear sign that your class has outgrown its purpose and is failing the SRP.
The cost to living with this transgression ranges from high (when there are lots of merge conflicts) to low (where the code is pretty stable and this core class sees very little action). However, it is rarely worth living with it, because the approaches to fix the problem are generally pretty easy.
Solution 1: Simple Separation Within the Structure
This approach is really just a formalization of the segmented code structure described as a symptom. You separate the class into two classes and an interface (or two classes and two interfaces, if you want to be even more pure). First, create an interface which represents the functionality you want to separate out. Your IDE should have easy tools to do this, where you just pick the methods to pull out. Second, separate the class into two classes, one extending the other, where one has only the methods and supporting properties for the interface, which should leave the other with only the methods and supporting properties for the other major responsibility of the class.
Now you can move to the clients of this class, areas of the code that are calling it. Hopefully, most of them only think of the class in one of its aspects, so you can change their reference to it either to be the interface or the new, cleaner class. This part of the exercise will possibly illuminate for you where the muddled thinking on the original class has led to some sloppy code. Or maybe it will point out to you that this over-burdening hasn’t really been costing you much, in terms of a clean architecture.
Solution 2: Delegation
Another approach to fixing this problem is to move one responsibility completely out of the class hierarchy into its own, completely independent class or hierarchy of classes. Then you make the original class delegate all that work to the new, referenced class. You still separate out the secondary theme into a new interface, but rather than this being just a sugar-coating over the fact that you’re still doing all the implementation in a single concrete class (having inherited some of it, but it’s all still there), you actually move the implementation into a completely separate class. Clients of this class now need to make a separate call to get to the delegate before calling whatever method they wanted to perform.
This approach makes a lot of sense when you realize that you would really like to abstract out some of this second theme of responsibilities, and maybe that becomes a hierarchy of classes all on its own, orthogonal to the hierarchy that the core theme represents.