Как стать автором
Обновить

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

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

В оригинальном определении этот принцип гласит:

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

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

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

Ниже приведен пример кода:
#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
  }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В большинстве случаев принцип Единственности Ответственности помогает снизить связность компонентов, делает код более читабельным, упрощает написание юнит тестов. Но всегда нужно помнить о том, что это всего лишь общая рекомендация, и решение по его применению следует принимать исходя из конкретной ситуации. Разделение ответственности должно быть осознанным. Вот несколько примеров, когда этого делать не стоит:
  1. Разбиение существующего класса может привести к тому, что клиентский код банальным образом поломается. Заметить это на этапе разработки и тестирования бывает трудно, если логика недостаточно покрыта качественными юнит тестами и/или по причине плохого мануального/авто тестирования. Иногда такая поломка может стоить компании денег, репутации и т.п.;
  2. Разделение ответственностей просто не нужно, так как клиентский код и разработчиков компонента все устраивает (при этом они знают о существовании принципа). Требования практически не меняются. Причем это относится как к существующим классам, так и к еще не созданным, а находящимся на этапе проектирования;
  3. В других случаях, когда пользы от разделения меньше, чем вреда от нее.

Однако знание и понимание принципа должно улучшить кругозор разработчика, что позволит ему эффективнее проектировать и сопровождать создаваемые решения.
Теги:
Хабы:
+4
Комментарии80

Публикации

Изменить настройки темы

Истории

Работа

.NET разработчик
72 вакансии

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн