Rethink code cohesion

Emergent Design: time to relate

The methods reverseCharacters() and isMightyMouse() are easy to name because they each do a single, identifiable thing. It also helps with debugging. If the characters are not reversing properly, I know exactly where to look to find the bug, because the responsibility for doing this is clearly assigned to the properly named method, and only that method.

Cohesion of perspective level

Another aspect of cohesion you should be aware of is the level of perspective at which a method operates. Put simply, methods tend to accomplish their functionality by either of the following:

  • Code logic directly in the method
  • Calling other methods

The preceding process() method has a little bit of logic in it, but it is purely for sequencing the other methods and organizing the results of their actions. Mostly, process() calls reverseCharacters() and isMightyMouse(), where the actual work is done. This aggregation of behavior that process() does is at a level of perspective we call specification.

Levels of perspective

In UML Distilled: A Brief Guide to the Standard Object Modeling Language, Martin Fowler refers to the levels of perspective that Steve Cook and John Daniels identified in their book Designing Object Systems. The three types of perspective are conceptual, specification, and implementation.

  • The conceptual perspective deals with the system objects, which ideally represent entities in the problem domain I am writing for. If I am designing an application for a bank, these might be account, transaction, statement, customer, and the like.
  • Specification means the public methods that form the interface of each object, but also the private methods to which they are delegated (see the example code on this page). So, process(), being the only public method of Application, is decidedly a specification-level issue, but from the point of view of process(), so are reverseCharacters() and isMightyMouse(). This is because process() is concerned with what they do (how they are called, what they return), but not how they do it.
  • Implementation is concerned with the code that does the actual work. Nice that we still have that, eh?

The reverseCharacters() and isMightyMouse() methods are implementation-level methods; they have the code that does the dirty work.

It would be overstating things to suggest that I always write methods that are purely at one level of perspective or another-even here, process() has a little bit of logic in it, not just a series of method calls. But my goal is to be as cohesive as possible in terms of levels of perspective, mostly because it makes the code easier to read and understand.

It would not be overstating things to suggest that I always strive to write methods that are cohesive in the general sense, that they contain code that is all about the same issue or purpose. When I find poorly cohesive methods, I am going to change them, every time, because I know that method cohesion is a principle that will help me create maintainable code, and that is something I want for myself, let alone my team and my customer.

Class cohesion

Classes themselves also need to be cohesive. The readability, maintenance, and clarity issues that, in part, drive the need for method cohesion also motivate class cohesion.

In addition, we know we want our classes to define objects that are responsible for themselves, that are well-understood entities in the problem domain. A typical mistake that developers who are new to object orientation make is to define their classes in terms of the software itself, rather than in terms of the problem being solved. For instance, consider the following code:


public class BankingSystem {
  // No "method guts" are provided; this is just a 
  // conceptual example
  public void addCustomer(String cName, String cAddress, 
                          String accountNumber, 
                          double balance) {}
  public void removeCustomer(String accountNumber) {}
  public double creditAccount(String accountNumber, 
                              double creditAmount) {}
  public double debitAccount(String accountNumber, 
                             double debitAmount) {}
  public boolean checkSufficientFunds(String accountNumber, 
                                      double checkAmount) {}
  public void sendStatement(String accountNumber) {}
  public boolean qualifiesForFreeToaster(String accountNumber){}
  public boolean transferFunds(String fromAccount, 
                               String toAccount, 
                               double transferAmount) {}
}

It is easy to imagine the thought process that leads to code like this: "I am writing a banking system, and so I will name the class for what it is, a BankingSystem. What does a banking system do? Well, I need to be able to add and remove customers, manage their accounts by adding to them and withdrawing from them," and so on.

Sponsored: Driving business with continuous operational intelligence