Written by 10:58 Computer Environment, Languages & Coding

SOLID: Single Responsibility Principle

In this article, I will try to describe one of the famous principles of the object oriented programming, that is a part of another famous term – SOLID. This principle is called Single Responsibility, and it states that:

A class should have only one reason to change.

First of all, let’s try defining the Responsibility term and link it to the statement above. Any software component has certain reasons why it was written. They can be referred to as requirements. Let’s consider the component responsibility as ensuring the adherence of the implemented logic to the requirements imposed on the component. If the requirements are being changed, the component logic and its responsibility are being changed as well. Therefore, the initial definition of the principle is identical to the fact that a class must have only one responsibility, one purpose. Therefore, there is only one reason for its change as well. For one thing, let’s take a look at an example of the principle violation, and see what consequences it may have. Let’s consider a lass that calculates the area of a rectangle and outputs it to a GUI. Thus, the class combines two responsibilities (therefore, two global reasons for change) that can be defined in the following way:

  1. The class must calculate the area of a rectangle by two sides.
  2. The class must draw the rectangle.

Here is a code example:

#using UI;
class RectangleManager
{
  public double W {get; private set;}
  public double H {get; private set;}

  public RectangleManager(double w, double h)
  {
    W = w;
    H = h;
   // Initialize UI
  }

  public double Area()
  {
     return W*H;
  }

  public void Draw()
  {
     // Draw the figure on UI
  }
}

Note that the code above uses the external graphic components implemented in the UI name space for drawing.

Suppose we have two client software programs that employ this class. One of them simply makes certain calculations, and the second one implements the user interface.

Program 1:

#using UI;
void Main()
{
  var rectangle= new RectangleManager(w, h);
  double area = rectangle.Area();
  if (area < 20) 
  {
    // Do something;
  }
}

Program 2:

#using UI;
void Main()
{
   var rectangle= new RectangleManager(w, h);
   rectangle.Draw();
}

This design has the following cons:

  • Program 1 has to depend on the external UI components (the #using UI directive), in spite of the fact that it does not need it. This dependency is caused by the logic implemented in the Draw method. As a result, it increases the compilation time, adds various performance issues on client machines, which can simply do not have such UI components installed;
  • When the logic changes, we need to test the entire RectangleManager over again. Otherwise, the crashes of the area calculation logic, and, consequently, the crash of Program1 may occur.

In this case, we have an obvious evidence of bad design. In particular, we deal with Fragility (it crashes easily when changes are made due to the high cohesion), and relative Immobility (possible difficulties of class usage in Program 1 because of the unnecessary dependence on UI).
The problem can be solved by dividing the RectangleManager source component into the following two parts:

  1. The Rectangle class responsible for the area calculation and providing length values of the rectangle sides;
  2. The RectanglePresenter class that implements drawing of the rectangle.

Note, that the responsibility of the Rectangle class is complex. That is, it contains the requirements to provision of the side lengths and to the area calculation. Therefore, we can say that the responsibility reflects the component contract, i.e. the set of its operations (methods). This contract is defined by the potential requirements of a client. In our case, this is the provision of geometric parameters of the rectangle. The code representation looks in the following way:

public class Rectangle
{
  public double W {get; private set;}
  public double H {get; private set;}

  public Rectangle(double w, double h)
  {
    W = w;
    H = h;
  }

  public double Area()
  {
     return W*H;
  }
}

public class RectanglePresenter()
{
  public RectanglePresenter()
  {
    // Initialize UI 
  }
  public void Draw(Rectangle rectangle)
  {
    // Draw the figure on UI
  }
}

Taking into account the changes made, the code of the client software programs will look in the following way:

Program 1:

void Main()
{
  var rectangle= new Rectangle(w, h);
  double area = rectangle.Area();
  if (area < 20)
  {
     // Do something 
  }
}

Program 2:

#using UI;
void Main()
{
  var rectangle = new Rectangle(w, h);
  var rectPresenter = new RectanglePresenter();
  rectPresenter.Draw(rectangle);
}

As you can see from the code, Program 1 is not dependent of the graphical components anymore. Besides, thanks to adherence to the principles, the unnecessary dependencies have been eliminated, and the code has become more structural and reliable.

In most cases, the Single Responsibility Principle helps minimizing connectivity of components, makes code more readable, and simplifies writing of unit tests. However, we need to always remember that it is just a common recommendation, and the decision for its implementation should be taken from the perspective of the specific situation. Sharing of responsibility must be conscious. Here are several examples when it is not worth it:

  • Division of an existing class may break a client code. It may be hard to notice on the stage of development and testing, if the logic is weakly covered with unit tests and/or because of bad manual/auto testing. Sometimes, such break may cost a company money and reputation etc.
  • Division of responsibilities is simply not needed, since the developers of a component are happy with everything (at that, they are aware of the principle). Requirements do not change much. It applies to the existing classes and the ones that are currently being developed;
  • In any other cases, when division does more harm than good.

Anyways, knowledge and understanding of the principle improves developer’s area of thought and allows designing and maintaining solutions being created in a more effective way.

Tags: , Last modified: September 23, 2021
Close