SOLID: принцип единственности ответственности

в 9:43, , рубрики: .net, C#, design patterns, solid, ооп, Проектирование и рефакторинг

В этой статье мы попробуем описать один из известных принципов объектно-ориентированного программирования, входящий в аббревиатуру не менее известного понятия SOLID. На английском языке он носит название Single Reponsibility, что в переводе на русский означает Единственность Ответственности.

Этот принцип гласит:

Класс должен иметь только одну причину для изменения

Для начала попробуем определить понятие Ответственность. Любой программный компонент имеет некоторые причины, почему он был написан. Их можно назвать требованиями. Обеспечение следования реализованной логики налагаемым на компонент требованиям назовем ответственностью компонента. Если требования меняются, меняется и логика компонента, а следовательно и его ответственность. Таким образом, первоначальная формулировка принципа эквивалентна тому, что класс должен иметь только одну ответственность, одно назначение. Тогда и причина для его изменения будет одна.
Для начала приведем пример нарушения принципа и посмотрим, какие последствия это может иметь. Рассмотрим класс, который может рассчитывать площадь прямоугольника, а также выводить его на графический интерфейс. Таким образом, класс совмещает в себе две ответственности (следовательно и две глобальных причины для изменения), которые можно определить так:

  1. Класс должен уметь вычислять площадь прямоугольника по двум его сторонам;
  2. Класс должен уметь рисовать прямоугольник.

Ниже приведен пример кода:

#using UI;
class RectangleManager
{
  private double _x;
  private double _y;

  public RectangleManager(double x, double y)
  {
    _x = x;
    _y = y;
   // Initialize UI
  }

  public double Square()
  {
     return _x*_y;
  }

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

Следует обратить внимание, что в приведенном выше коде для рисования используются сторонние графические компоненты, реализованные в пространстве имен UI.

Пусть имеются две клиентские программы, которые использует данный класс. Одна из них просто выполняет некоторые вычисления, а вторая реализует пользовательский интерфейс.

Program 1:

#using UI;
void Main()
{
  var rectangle= new RectangleManager(x, y);
  double square = rectangle.Square();
  if (square < 20) 
  {
    // Do something;
  }
}

Program 2:

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

Этот дизайн имеет следующие недостатки:

  • Program 1 вынуждена зависеть от внешних UI компонентов (директива #using UI), несмотря на то, что ей этого не нужно. Эта зависимость обусловлена логикой, реализованной в методе Draw. В итоге это увеличивает время компиляции, добавляет возможные проблемы работы программы на машинах клиентов, где просто может быть не установлено таких UI компонентов;
  • в случае изменения логики рисования следует заново тестировать весь RectangleManager компонент, иначе есть вероятность поломки логики вычисления площади и, следовательно, Program1.

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

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

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

В конечном счете это решение удовлетворяет принципу Единственности Ответственности. В коде это выглядит так:

public class Rectangle
{
  private double _x;
  private double _y;

  public Rectangle(double x, double y)
  {
    _x = x;
    _y = y;
  }

  public double Square()
  {
     return _x*_y;
  }
}

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

С учетом проделанных изменений, код клиентских программ примет следующий вид:

Program 1:

void Main()
{
  var rectangle= new Rectangle(x, y);
  double square = rectangle.Square();
  if (square < 20)
  {
     // Do something 
  }
}

Program 2:

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

Отсюда видно, что Program 1 уже не зависит от графических компонентов. Кроме того, в результате следования принципу ненужные зависимости исчезли, код стал более структурированным и надежным.

В большинстве случаев принцип Единственности Ответственности помогает снизить связность компонентов, делает код более читабельным, упрощает написание юнит тестов. Но всегда нужно помнить о том, что это всего лишь общая рекомендация, и решение по его применению следует принимать исходя из конкретной ситуации.

Автор: szolotarev

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js