This is part 4 of my ongoing Design Principles series.
When designing the classes in your model, it is important that they each have a specific role to play. Cohesion means joining together to form a united whole. Classes and methods should be highly cohesive. They should have a single function. Methods on a class should be single function and together the methods on a class should all work toward accomplishing the same purpose. Adding an add method to a string class is not being cohesive. Having a “utility” class isn’t either. Classes (or methods) that try to play more than one role are harder to use, harder to test, and harder to change. The reason is side effects. If a class does many things, there is a high likelihood that using one part will affect how another part works.
It is hard to understand the use model of a multifunctional class. A class with a purpose has an obvious place in a design. A class without a purpose implies less of its use pattern and is therefore easier to mis-use. Classes with multiple roles to play also have more complex interfaces. Each role subtly influences the design of the other roles and may make the use model more complex than necessary. Simple, self-documenting use models are going to result in the cleanest code with the fewest bugs.
It is important in testing to make sure that one method call on a class doesn’t impact other method calls. If the number of methods on a class gets large, the combinatorial explosion forced on test becomes unmanageable. Likewise, every time a change is made, all methods on a class must be tested to make sure that they didn’t break. The more methods–and the more disparate the methods–the more burdensome this becomes for test. That means longer test passes, less new exploratory testing, and a poorer product.
Most importantly, complex multi-use classes will be difficult to change. The code underneath will be interconnected and a change that should affect only one role may end up triggering a cascade of changes that impact all of the other roles. Tradeoffs may be required. What should be a simple change may mean untangling various pieces of internal state. It is hard to be comfortable with a change to a complex class. Unit tests will ease the discomfort, but there is so much that could potentially break that one can never be sure something subtle didn’t just change for the worse.
How can a poor design be recognized? Here are some questions to consider:
- Is the name generic? If it doesn’t imply a single role, the class may be trying to do too much.
- Conversely, can this class be given a simple name? If naming is difficult, the class is too complex.
- Classes with a large number of methods often are trying to do more than one thing. Consider breaking them up.
- Classes should have only one reason to change.
- Methods that take a lot of options are generally poorly designed. If a method checks a state and then branches right at the beginning, it should be more than one method.
DoSomething(enum Type, int Data)
type1: do something
type2: do something else
Is better written:
do something else