SOLID: the principle of uniqueness of responsibility

In this article we will try to describe one of the well-known principles of object-oriented programming, which is included in the abbreviation of the equally well-known concept of SOLID. In English, it is called Single Reponsibility, which in Russian means Uniqueness of Responsibility.

In the original definition, this principle states: A

class must have only one reason for changing

First, let's try to define the concept of Responsibility and try to connect this concept in the above wording. Any software component has some reason why it was written. They can be called requirements. Ensuring that the implemented logic follows the requirements imposed on the component is called component liability. If the requirements change, the logic of the component changes, and therefore its responsibility. Thus, the initial formulation of the principle is equivalent to the fact that the class should have only one responsibility, one purpose. Then there will be one reason for its change.
To begin with, we give an example of a violation of the principle and see what consequences this may have. Consider a class that can calculate the area of ​​a rectangle, and also display it on a graphical interface. Thus, a class combines two responsibilities (therefore, two global reasons for change), which can be defined as follows:

  1. The class must be able to calculate the area of ​​the rectangle on its two sides;
  2. The class should be able to draw a rectangle.

The following is sample code:
#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
  }
}

It should be noted that in the above code, third-party graphic components implemented in the UI namespace are used for drawing.

Suppose there are two client programs that use this class. One of them just does some calculations, and the second implements a 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 disadvantages:

  • Program 1 is forced to depend on external UI components (directive #using UI), despite the fact that it does not need it. This dependence is due to the logic implemented in the Draw method. As a result, this increases compilation time, adds possible problems of the program on client machines, where such UI components may simply not be installed;

  • в случае изменения логики рисования следует заново тестировать весь RectangleManager компонент, иначе есть вероятность поломки логики вычисления площади и, следовательно, Program1.

В данном случае налицо признаки плохого дизайна, в частности Хрупкости (легко поломать при внесении изменений вследствие высокой связности), а также относительной Неподвижности (возможные трудности использования класса в Program 1 из-за ненужной зависимости от UI).

Проблему можно решить, разделив исходный компонент RectangleManager на следующие части:

  1. Класс Rectangle, ответственный за вычисление площади и предоставление значений длин сторон прямоугольника;
  2. Класс RectanglePresenter, реализующий рисование прямоугольника.

Please note that the responsibility of the Rectangle class is complex, that is, it contains both requirements for providing the lengths of the sides and for calculating the area. Thus, we can say that responsibility reflects the contract of the component, that is, the set of its operations (methods). This contract itself is determined by the potential needs of customers. In our case, this is the provision of the geometric parameters of the rectangle. In code, it looks like this:

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
  }
}

Subject to the changes made, the code of the client programs will take the following form:

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);
}

This shows that Program 1 is no longer dependent on graphical components. In addition, following the principle of unnecessary dependencies disappeared, the code became more structured and reliable.

In most cases, the principle of Uniqueness of Responsibility helps to reduce the connectivity of components, makes the code more readable, and simplifies the writing of unit tests. But you always need to remember that this is just a general recommendation, and a decision on its application should be made based on the specific situation. The division of responsibility should be conscious. Here are some examples when you should not do this:
  1. Splitting an existing class can cause the client code to crash in a trivial way. It can be difficult to notice this at the development and testing stage if the logic is not sufficiently covered by high-quality unit tests and / or due to poor manual / auto testing. Sometimes such a breakdown can cost a company money, reputation, etc .;
  2. Separation of responsibilities is simply not necessary, as client code and component developers are happy with everything (and they are aware of the existence of the principle). The requirements are practically unchanged. And this applies both to existing classes, and to those that have not yet been created, but are at the design stage;
  3. In other cases, when the benefits of separation are less than the harm from it.

However, knowledge and understanding of the principle should improve the horizons of the developer, which will allow him to more efficiently design and maintain the created solutions.

Also popular now: