OOP, cohesion, coupling and heuristics

Introduction

In the OOP world, we do not need big, God like, classes that do a bit of everything. On the contrary, we need small classes that have specific roles and responsibilities. At the same time, each class has to be as independent as reasonably possible from the others, no matter if they belong in the same module or not.

In the OOP world a module is a package or a namespace, depending on the programming language you are using. The S.O.L.I.D principle that is focused on creating highly cohesive classes is the Single Responsibility Principle.

Think of your codebase as a multinational company, like LexCorp or Wayne Enterprises. You would not assign too many roles to a specific person, but you would not let them go rogue too. It is not that wise. There is always a hierarchy, a structure, a system of reasoning of who does what and why! It is the same in the OOP world. Always reason on how many classes you need and why, and which the role of each class. Try to avoid to be extremely verbal, ergo, do not create classes that have little or no role in your codebase!

Cohesion indicates the classes relationships within the module, while on the other hand coupling indicates the relationships among modules. The concepts of cohesion and coupling are not new at all. They were invented by Larry Constantine, and you can read more about them in the Structured Design book.

Finally, the OO Design Heuristics are a set of guidelines for good object-oriented designs. As Bjarne Stroustrup (father of the C++ programming language) correctly pointed out many years ago, just making a design “object oriented” doesn’t always mean that the design is good.

Cohesion

According to the definition provided by the University of Carolina Computer Science department, cohesion is “The measure of strength of the association of elements within a module. Modules whose elements are strongly and genuinely related to each other are desired. A module should be highly cohesive.” Or in simpler words, cohesion portrays the true responsibilities of a module.

The higher the cohesiveness, the better the OO design. If the module, by its functionalities, satisfies the specifications that all of them belong in a specific context and has a clear vision and purpose, then the module can be considered a high cohesion one. In other words, we can say that we have a module with high cohesion, if the module’s classes methods have much in common, and perform a small number of related activities, on related data(sets). The main benefits of high cohesion are the low module complexity, the higher module maintainability and reusability.

On the other hand, if the module is like an open bar buffet, that you can get a bit of everything, and its purpose is unclear and without a vision, then the module is of low cohesion. Low cohesion modules are difficult to reuse, to test and to maintain. Cohesion and coupling are inversely related concepts. High cohesion goes hand in hand with loose coupling, and vice versa. Cohesive modules focus on a specific set of related tasks with low dependency among them.

Let’s see an example of low and high cohesion. First, we have a low cohesion example. As you can see, the class Employee carries a lot of different irrelevant behaviors, behaviors that are not in common.

namespace Entrance;

public class Employee 
{
	public Guid Id {get; init;}
	public void OpenEntranceDoor() {}
	public void CloseEntranceDoor() {}
	public void CleanEntranceFloor() {}
	public void CleanEntranceWindows() {}
	public void HostEntranceArea() {}
	public void SecureEntranceArea() {}
}

Now let’s see how a high cohesion example:

namespace Entrance;

public class Employee 
{
	public Guid Id {get; init;}
}

public class Concierge: Employee 
{
	public void OpenEntranceDoor() {}
	public void CloseEntranceDoor() {}
}

public class Janitor: Employee 
{
	public void CleanEntranceFloor() {}
	public void CleanEntranceWindows() {}
}

public class Secretary: Employee 
{
	public void HostEntranceArea() {}
}

public class SecurityOfficer: Employee 
{
	public void SecureEntranceArea() {}
}

If we kept maintaining the low cohesion, God like, Employee class, of the first example, the maintenance process would be a hell. The complexity of each method is different, the size of each method is different, and also Git would complain a lot due to conflicts, because all the programmers would be forced to edit it! Plus, the reusability level of this class would be zero, if not below.

In the high cohesion example, assuming that the module’s name is “Entrance”, each of the classes has a specific role that is related to the “Entrance” concept. They all work on and interact with the “Entrance”, but each class has a specific set of responsibilities, based on its role. So, we have increased maintainability, all the classes are reusable and each one of them stays out of the other’s feet.

Cohesion types

Coincidental cohesion. Coincidental cohesion is when classes are grouped together but without serving common tasks. The only common ground among those classes is that they have been grouped together. The most common example is the utilities classes.

Communicational / Informal cohesion. Communicational cohesion is when parts of a module are grouped because they operate on the same data. There are cases where communicational cohesion is the highest level of cohesion that can be attained under the circumstances. For example: a module which operates on the same dataset.

Functional cohesion. When specific classes are grouped in a module where all of them contribute to a very specific and narrow set of tasks. Functional cohesion is considered the most desirable type of cohesion for a software module, but it may not be achievable.

Logical cohesion. Logical cohesion is when classes are grouped because they are logically categorized to do the same thing even though they are different by nature. For example: grouping all mouse and keyboard input handling routines.

Procedural cohesion. Procedural cohesion is when parts of a module are grouped because they always follow a certain sequence of execution. For example: a function which checks file permissions and then opens the file.

Sequential cohesion. Sequential cohesion is when parts of a module are grouped because the output from one part is the input to another part like an assembly line. For example: a function which reads data from a file and processes the data.

Temporal cohesion. Temporal cohesion is when parts of a module are grouped by when they are processed – the parts at a particular time in program execution. For example: A function which is called after catching an exception which closes open files, creates an error log, and notifies the user.

Coupling, and the “Law of Demeter”

Based on Wikipedia, “Coupling is the degree of interdependence between software modules; a measure of how closely connected two routines or modules are; the strength of the relationships between modules.”.

High coupling is an undesired behavior. What usually happens is that a change, even a minor one, in one of the modules usually affects and leads to changes in other modules too. Additionally, some modules might require more time to execute their functions due to the increased dependencies. Finally, reusability or testability are low because any dependent modules must be included too. Coupling increases between two classes A and B if:

  • Class B extends Class A
  • Class A has an attribute of Class B type.
  • Class A calls methods of an object of Class B.
  • Class A has a method that references Class B (return type or parameter).

When one module communicates with others through clear and consistent interfaces and is unaware of the internal implementation of the other modules, the desired amount of low coupling is attained. The following properties must be taken into account when calculating the coupling of our code base:

Degree. It refers to how many links there are between each module and the others. This degree count should be as low as possible. For instance, the degree should be modest and coupling would be loose if the module needs to link to other modules through a limited number of parameters or narrow interfaces.

Ease. It is determined by how clear the connections between the module in hand and the other ones are. With coupling, we want the connections to be easy to make without the requirement to comprehend how the other modules are implemented.

Flexibility. How easily we can replace one module with another and the changes of the module in hand will be minimum to none. We want the other modules to be easily replaceable.

OOP coupling cases

Dynamic coupling. The goal of this type of coupling is to provide a run-time evaluation of a software system. It has been argued that static coupling metrics lose precision when dealing with an intensive use of dynamic binding or inheritance. In the attempt to solve this issue, dynamic coupling measures have been taken into account.

Logical coupling. Logical coupling exploits the release history of a software system to find change patterns among modules or classes. For example: Entities that are likely to be changed or sequences of changes (a change in a class A is always followed by a change in a class B).

Semantic coupling. This kind of coupling considers the conceptual similarities between software entities using, for example, comments and identifiers and relying on techniques.

Subclass coupling. Describes the relationship between a child and its parent. The child is connected to its parent, but the parent is not connected to the child.

Temporal coupling. When two actions are bundled together into one module just because they happen to occur at the same time.

The Law of Demeter

Demeter was the ancient Greek goddess of Agriculture, sister of Zeus. As described in the book “The pragmatic programmer”, by Andrew Hunt and David Thomas, the name “Law of Demeter” was chosen because the style rule was discovered while working on the Demeter Project.

The Law of Demeter principle (or the Principle of Least Knowledge) is a rule of thumb for designing object-oriented systems that helps us reduce any dependencies. “Only talk to your friends” is the motto. It was first proposed at Northeastern University in the fall of 1987 by Ian Holland and popularized in books by Booch, Budd, Coleman, Larman, Page-Jones, Rumbaugh and others. A more general formulation is the following: Each unit should have only limited knowledge about any other units that are “closely” related to the current unit.

The main motivation that led to the formulation of the Law of Demeter is to control information overload. It is easier to keep a limited set of items in short-term memory and it is easier to keep them in memory if they are closely related.

In OOP terms, the principle states that an object at any moment in the codebase must not know of the internal implementation details of any objects it manipulates. Let’s say that we have Object and Method of that object. Based on the Law of Demeter the Method can only invoke any other methods that belong to the following objects:

  1. Object itself.
  2. Method’s parameters.
  3. Object’s fields.
  4. Any objects created within Method.
  5. A global variable, accessible by Object, in the scope of Method.

Later we will discuss full C# examples explaining the rules above, but before that we will discuss why the method chaining of different objects and methods is bad, and the train wreck problem.

Method chaining and the train wreck problem

The following diagram illustrates how the Law of Demeter is violated:

ClassA is accessing ClassC through method chaining. If we had objectA of ClassA, objectB of ClassB, and objectC of ClassC the code would look like the following:

objectA.objectB.objectC.Method();

The statement above to access objectC.Method() isn’t long, but based on the Law of Demeter we could reduce the method chaining to something like this:

objectA.OutputOfMethodC();

A rule of thumb that helps but identify if the Law of Demeter is applied or not is the number of dots that are used. Specifically, an object should avoid invoking methods of an object returned by another object or method. In the world of the OOP programming languages, the law can be stated simply as “use only one dot“. That means that objectA.objectB.objectC.Method(); or objectA.objectB.ReturnObjectC().Method(); break the law. An excellent analogy is the dog analogy. If we wanted the object Dog to walk, we would call the the method Walk() of the Dog object, ergo Dog.Walk(). We would not command the dog’s legs to walk directly, so we would not have a statement like Dog.Legs.Walk(); or Dog.Legs().Walk();

ATTENTION! The Law of Demeter isn’t against methods chaining on the same object, so LINQ for example respects the Law of Demeter. For our non-C# friends, the following code respects the Law of Demeter too, because all the methods are applied on the same object:

objectA.OutputOfMethodC().Lowercase().Uppercase();

The train wreck problem. Why the methods chaining of different objects is bad? Forget the Law of Demeter for a moment. In OOP languages unfortunately we have a feature that is a drawback for the programmer and the code itself. And this drawback is null. I will not discuss the latest versions of C# that try to fight it with Nullable types. This is out of the scope of this article.

Let’s check the two following lines of code

objectA.objectB.objectC.objectD.Method()

or

objectA.MethodReturnsObjectB().MethodResult().AnotherResult()

Do you know which part of this chain is null? No, you don’t. Nobody does. And if you have a NullReferenceException the rest of the chain will not be executed. In order to avoid the train wreck problem, in OOP one possible solution is to adopt the Null Object Pattern, or other similar patterns that come from the Functional Programming world, like Monads for example.

Law Of Demeter example

Let’s discuss a full example that violates the Law of Demeter. It was aforementioned, an object must not know any internal details of the object it manipulates.

The following example is the famous “The Paperboy and the Wallet” one, from the David Bock’s paper about the Demeter practice. In this example, the paperboy gets a payment from a customer who has a wallet. So, we have three classes: Customer, Wallet and Paperboy.

public class Customer
{ 
    public Wallet Wallet {get; set;}
}
public class Wallet
{
    public float Value {get; set;}
 
    public void AddMoney(float amount)
    {
        Value += amount;
    }
 
    public void SubtractMoney(float amount)
    {
        Value -= amount;
    }
}
public class Paperboy
{
    public void SellPaper(Customer customer)
    {
        var payment = 2.0f;
        var wallet = customer.Wallet;

        if (wallet.Value >= payment)
        {
            wallet.SubtractMoney(payment);
        }
        else
        {
            // we will return here later
        }
    }
}

If this example was in the real world, it is a dangerous case. I don’t think that we give our wallets to the person that we want to pay, and that person takes the money out of our wallet. The Paperboy object knows that the customer object has a wallet and it can manipulate it, and this is a clear Single Responsibility Principle violation. And nothing prevents the paperboy object to do the following:

customer.Wallet = null;

Even worse, the customer’s wallet might be already null, so the paperboy’s object SellPaper method has to add a null check to avoid an unwanted NullReferenceException.

public void SellPaper(Customer customer)
{
    var payment = 2.0f;
    var wallet = customer.Wallet;
    if (wallet is not null)
    {
        if (wallet.Value >= payment)
        {
            wallet.SubMoney(payment);
        }
        else
        {
            // we will return here later
        }
    }
}

The PaperBoy object’s code is a bit more complex now. Extra modifications of the Wallet class will result in modifying the Paperboy class even more due to the fact that those three classes are tightly coupled. The paperboy just wants to be paid, no matter if the money comes from a wallet or another medium (e.g pocket, credit card etc.).

Let’s rewrite the code a bit now:

public class Customer
{
    private Wallet _wallet;

    public Customer()
    {
        _wallet = new Wallet(20.0f);
    }
 
    public void PayAmount(float amountToPay)
    {
	try
	{
	        if (_wallet.Value >= amountToPay)
	        {
	            _wallet.SubtractMoney(amountToPay);
	        }
	}
	catch(Exception ex)
	{
	  throw; //if the value is less than the amount of money that has to be paid, throw an exception
	}	
    }
}
public class Wallet
{
	public float Value { get; private set; }
    
	public Wallet(float initialAmount)
    {
        Value = initialAmount;
    }
 
    public void AddMoney(float amount)
    {
        Value += amount;
    }
 
    public void SubtractMoney(float amount)
    {
        Value -= amount;
    }
}
public class Paperboy
{
    public void SellPaper(Customer customer)
    {
        var payment = 2.0f;
        customer.PayAmount(payment);
	// if the amount can't be paid an exception has been thrown by the customer object
    }
}

Now, the paperboy can only access the Customer object, but not the Wallet object at all! This means that the paperboy is paid by the customer without knowing where the whether the money comes from a wallet or not. The relationship between the Paperboy class and the Wallet class has been removed, so the code in the end is less coupled.

Finally, as you can understand, the advantages of embracing the Law of Demeter are the same, more or less, as the ones of achieving low coupling among the various modules:

  • The dependencies among the codebase classes are reduced.
  • The classes can be reused with ease.
  • The classes are easier to test.
  • The classes are more maintainable and flexible to changes.

OOP heuristics

OOP heuristics are a set of guidelines for good object-oriented design. Their goal is to help us make choices on software design. We want to design “good classes“, ergo classes that encapsulate the right data (state) and should have the right behavior. We must also distribute any responsibilities across the classes correctly.

All software developers know how to code, but not all of them are equally skilled in creating good designs for applications or subsystems. As Bjarne Stroustrup, father of the C++ programming language, pointed out many years ago, just making a design “object oriented” doesn’t always mean that you are making the design “good”.

Some software developers for example, will define a single big central class (a “god class”) that is responsible for controlling all of the main system behavior. But why is this wrong? It is a disaster for evolution and maintenance: “God classes impede evolution because they achieve only a low level of procedural abstraction, so changes may affect many parts of a god class, its data containers, and its clients. By splitting a god class up into object-oriented abstractions, changes will tend to be more localized and therefore easier to implement.“. The phrase above is from “Object-Oriented Reengineering Patterns (Morgan Kaufmann, 2003)” by Serge Demeyer, Stephane Ducasse, and Oscar Nierstrasz.

Design heuristics help us to evaluate our software design alternatives early in the design process. This can save a lot of design rework later. Design heuristics can be used as part of a design review or code review process. Design reviewers should look for potential violations of the design heuristics, and they should suggest possible restructuring steps to restore a good system structure.

The heuristics should never be taken as “absolute rules” — and in fact, some of the heuristics are contradictory. Heuristics are useful for evaluating the quality of a design.

Some heuristic examples are the following:

Heuristic 2.2: Users of a class must be dependent on its public interface, but a class should not be dependent on its users.

Interpretation: The “users” of a class are the other functions and classes that call upon the class. If the class is going to be as reusable as possible, the class implementor should make only minimal assumptions about the external functions and classes that will be calling on the public operations of the class. A LinkedList class will provide insert(), remove(), search_for(), and length_of() operations the same way no matter what class is requesting these services. A Timer class can time PhoneCall or TrackMeet objects.

Heuristic 2.6: Do not clutter the public interface of a class with things that users of that class are not able to use or are not interested in using.

Example: Some class developers might write one or more public constructors for an abstract class. It is a better idea to make these constructors “protected” because they will only really be callable in the definition of the constructors of derived classes.

Heuristic 2.8: A class should capture one and only one key abstraction.

Interpretation: The process of object-oriented design is the process of discovering the important abstractions in the system and mapping them to implementable classes. The “abstractions” that the classes will implement might come from several different places: an object-oriented analysis of the specific problem the domain engineering process applied to a set of related software systems in the same problem domain the proposed “architecture” of the system low-level design abstractions that are used to help implement interfaces to hardware devices, external databases, and other subsystems The main job of class design is to make sure that each class is “just the right size”. If a class contains multiple abstractions, there will be more cross-checking required later when a class needs to be modified or extended.

Heuristic 3.2: Do not create God classes/objects in your system. Be very suspicious of an abstraction whose name contains Driver, Manager, System, or Subsystem.

Interpretation: This heuristic is an extension of the previous one. A god class is a class that is either a class that makes all of the policy decisions for a significant part of the system after collecting the necessary information from its peers (a behavioral God class), or a class that encapsulates all of the important data in the system, giving it out to any of the other classes when they ask for it (a data god class). A god class is a symptom of poor distribution of system intelligence. Behavioral god classes are often created when a designer creates a “controller” class to combine all of the policy decisions in a single place. Data god classes may result from naive conversion of an existing data-oriented system to an object-oriented structure.

God classes are a problem because they increase the effort needed to change the design later. Most designs need to expand to handle new behaviors and new usage scenarios. In a system with a god class, that God class is often involved in most of the usage scenarios that will be modified as new features are added to the system. The designers will have the burden of verifying that a large number existing scenarios are unaffected by each change to the god class.

Exceptions: This heuristic might be violated when designing an interface to legacy code:

  • You are creating a Facade class that acts as a wrapper for a large legacy subsystem. This kind of wrapper class might need to be a behavioral god class which reflects the design decision that the legacy subsystem is taking the responsibility for specific behavior and scenarios.
  • The Facade class serves an important purpose. It hides the details of the legacy implementation, and it provides the writers of the rest of the application with a simpler and stable “application programmer interface”.

Heuristic 3.3: Beware of classes that have many accessor methods defined in their public interface, many of them imply that related data and behavior are not being kept in one place.

Interpretation: An accessor method is a function in a class that gives access to one data attribute in the class. It is OK to have some accessor functions for some attributes, but don’t take things too far. If you have accessor function for every attribute, the data isn’t really encapsulated. Other classes will become strongly coupled to the internal representation of the data, which will make things harder to change and evolve. You need to decide on the “role” of each class. If the class is a “service provider”, it probably doesn’t need to offer direct access to much of its data — most other classes just want it to “do” something. If the class is a “data organizer”, then there probably is a need for accessor functions, but they might only permit access to selected internal values.

For more read the following sources:

  1. Object Oriented Design Heuristics (manclswx.com)
  2. Object-Oriented Reengineering Patterns (unibe.ch)

References

Leave a Reply

Your email address will not be published.