В этой статье мы попробуем описать один из известных принципов объектно-ориентированного программирования, входящий в аббревиатуру не менее известного понятия 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. В других случаях, когда пользы от разделения меньше, чем вреда от нее.

Однако знание и понимание принципа должно улучшить кругозор разработчика, что позволит ему эффективнее проектировать и сопровождать создаваемые решения.
Поделиться с друзьями
-->

Комментарии (74)


  1. mwizard
    14.05.2017 12:51
    +2

    Понадобилось усилие, чтобы понять, что "square" — это не квадрат, это "площадь" в переводе с рунглиша.


    1. szolotarev
      14.05.2017 13:07
      -4

      Умение понимать чужой код это отдельный скил, хотя согласен, что слово может иметь два значения в английском языке.


      1. ookami_kb
        14.05.2017 13:48
        +7

        Насколько я помню, square может переводиться как "площадь", только если это Красная площадь. Площадь фигуры – это area.


        1. szolotarev
          14.05.2017 14:02
          +1

          Точно, спасибо :-) Поправил.


          1. OTCOcity
            15.05.2017 11:54

            Не поправилось в первом примере — там по-прежнему:

            public double Square()
            


            1. szolotarev
              15.05.2017 11:54

              Поправил, спасибо.


      1. samizdam
        15.05.2017 09:49
        +3

        Скилл — писать так, что бы было понятно.


    1. Deosis
      15.05.2017 06:09

      А вас не смущает, что программы пишут на бревне?


      1. mwizard
        15.05.2017 10:01
        +2

        Нет, не смущает, потому что log в данном случае имеет греческое происхождение от ?????, "говорю". А вот "square" в значении "площадь фигуры" — это банальная безграмотность и надмозг, сродни "I count that you..." = "Я считаю, что ты..."


  1. MadridianFox
    14.05.2017 13:05
    +1

    Вопрос на засыпку: x и y, которые логичнее было бы назвать w и h, у вас приватные. Как presenter узнает ширину и высоту прямоугольника, чтобы его нарисовать?


    1. szolotarev
      14.05.2017 13:13

      Это хороший вопрос :-) Переименовать можно, но это имхо субъективно. Можно вынести эти поля в паблик проперти, учту замечание. Хотя сами понимаете, к принципу единственности ответственности это сильно не относится. Это просто бага. Но все равно спасибо!


      1. MadridianFox
        14.05.2017 13:57
        +1

        В том и дело что относится. Принцип у вас нарушен)
        Класс Rectangle хранит данные прямоугольника и позволяет вычислить его площадь. А может при рисовании не надо использовать площадь? А может для вычисления площади надо подтянуть ещё одну зависимость (ну вдруг мы считаем площадь прямоугольника в многомерном искривлённом пространстве, для чего нужна отдельная библиотека)?
        А делая поля публичными, вы просто увеличиваете зависимость между классами. Даже если вы сделаете геттеры, зависимость смягчится, но сохранится. Должен ли класс, который рисует, зависеть от класса. который вычисляет ненужное ему значение? Или он должен зависеть от более атомарного, лучше даже интерфейса, который олицетворяет собой только прямоугольник и ничего лишнего?


        1. szolotarev
          14.05.2017 14:12

          Есть в этом замечании доля истины имхо. Лучше конечно ввести модель прямоугольника безо всяких там методов, а методы собственно реализовать в других классах — рисовальщике и вычисляльщике. Но в этом примере, чтобы не уходить слишком далеко, можно определить ответственность класса Rectangle как предоставление геометрических данных о фигуре — длин сторон, площади. Тогда все должно «проканать». Думаю ответственность может содержать логически объединенные группы требований. Например, одна ответственность может быть у класса, реализующего 4 CRUD операции для работы с данными, даже если кто-то и будет использовать только одну. Думаю такое вполне допустимо и разумно. Поправлю статью, более четко определив ответственности, спасибо.


  1. izzholtik
    14.05.2017 13:09
    +2

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

    Кто вообще это сформулировал таким образом? Смотрите стабильную версию статьи на вики, последние правки какой-то наркоман делал.


    1. szolotarev
      14.05.2017 13:21
      +1

      Я опирался на оригинальный труд Agile Principles, Patterns and Practices in C#, где определение дано так:
      A class should have only one reason to change.

      Но для меня лично формулировка с ответственностью/обязанностью более понятна, так что спасибо за ссылку. Это все об одном и том же в конечном счете.


  1. totuin
    14.05.2017 14:34
    +1

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


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

    А теперь вопрос знатокам: А зачем в такой ситуации вообще нужны классы?


    1. szolotarev
      14.05.2017 14:43
      +1

      Товарищи, я просто перевел оригинальное определение принципа, не смотрите вы в русскую википедию. Вот что написано на англоязычной:
      Robert C. Martin expresses the principle as, «A class should have only one reason to change.»[1]

      Согласен, что понимается это хуже, чем если говорить в терминах ответственности. Автор видимо имел в виду глобальную причину для изменения, которая может содержать более мелкие подпричины. Я хоть как-то попытался привести это к понятию единственности ответственности. Если у кого-то получится лучше, хорошо.


      1. rsi
        14.05.2017 17:58
        +1

        Эта книга переведена и продаётся в лабиринте. Собственно я Ее читал неделю назад. Ваше стремление написать статью похвально, но она выглядит похожим на вольный, сильно сжатый пересказ главы из этой книги.


        1. szolotarev
          14.05.2017 18:03
          -1

          Не у всех есть возможность купить книгу, целью данной статьи является добавить больше доступной информации по этой теме. Поэтому Ваше мнение очень важно для нас, что называется :-) А также полезно обсудить мысли, высказанные в этой книге. К примеру, само определение принципа, как оказалось, вызывает много диспутов и вопросов.


          1. rsi
            14.05.2017 18:09
            +1

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


            1. szolotarev
              15.05.2017 11:47

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


              1. rsi
                15.05.2017 11:48

                Ну так я об этом и написал.


                1. szolotarev
                  15.05.2017 11:52

                  Согласен, что статью надо дорабатывать, возможно слишком мало информации, слишком мало рассуждений :-) Спасибо за коммент.


          1. rsi
            14.05.2017 18:19

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


            1. szolotarev
              15.05.2017 12:06

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

              Постараюсь учесть.


    1. vanxant
      14.05.2017 14:58
      -1

      А зачем в такой ситуации вообще нужны классы?

      А как без классов работать с <название модного IoC контейнера>?


    1. MadridianFox
      14.05.2017 15:00
      -1

      Кстати, примерно об этом же говорит и Егор Бугаенко — класс должен содержать минимум методов, не больше 4-5, а в идеале вообще 1.


      1. totuin
        14.05.2017 15:16
        +3

        Вообще то классы, да и вообще ООП было придумано как ещё один способ структурирования кода. Каждый класс представляет собой как бы папочку методов и значений, свойственных сущности которую описывает данный класс. Имеет ли смысл делать папку всего с одним вложенным листочком?

        Проблемма современных програмистов, что они разучились думать самостоятельно, а предпочитают тупо следовать патернам, принципам и книгам типа «Clear Code», которые им забили в голову во время обучения.Почитать такие книжки конечно полезно, посмотреть как другие работают, что применяют, возможно найти там несколько полезных примеров. Но вот использовать их как библию и 10 заповедей — зло. Сегодня моден принцип единственной ответственности, и мы клеймим всех кто ему не подчиняется, завтра взойдёт другая звезда, предложит принцип максимальной ответственности, и мы будем дружно клеймить Егора Бугаенко. Прежде всего надо научится думать своей головой, и понять что все эти принципы придумывали конкретные люди с конкретным (своим собственным) образом мышления, и для конкретного котекста.


        1. szolotarev
          14.05.2017 15:51
          +1

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


        1. MadridianFox
          14.05.2017 16:39
          +1

          Вообще то классы, да и вообще ООП было придумано как ещё один способ структурирования кода.

          Не совсем. Парадигмы программирования, такие как ФП и ООП придуманы для работы с кодом на более высоком уровне абстракции. А для структурирования придумали современные управляющие конструкции (if,while,for..) и модульность.
          Имеет ли смысл делать папку всего с одним вложенным листочком?

          Имеет. Объект в ООП, как и функция в ФП, это способ динамического связывания кода.


      1. dmitryanufriev
        14.05.2017 15:19

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

        Лучше конечно ввести модель прямоугольника безо всяких там методов, а методы собственно реализовать в других классах — рисовальщике и вычисляльщике.
        .
        И в этом контексте да, зачем классы? Это хороший процедурный, структурированный код, который можно реализовать хоть на C, хоть на Pascal. Кстати, SRP это про ООП или вообще про организацию кода?


        1. szolotarev
          14.05.2017 15:40

          Насколько я понял, первоначально его относили к классам. Наверное этому принципу можно следовать и в других случаях, если это делает код менее жестким, хрупким, мобильным. Почему нет? То что класс Rectangle содержит данные и процедуры, так это было заложено в основу ООП, когда ни о каком stateless еще и не слышали. Так что не вижу причин ужасных так не делать. А вот упрямое следование принципам может привести к нехорошим последствиям.


          1. dmitryanufriev
            14.05.2017 15:55
            +1

            А если рассмотреть такой пример. Вот, допустим, мы в коде используем классическое наследование. И для класса Rectangle вводим класс-наследник, расширяющий поведение базовового класса за счет добавления метода Draw. Один класс, один метод Draw, ответственность определена — отрисовка. Назовем его, предположим, DrawableRectangle. Этот дизайн соответствует SRP?


            1. szolotarev
              14.05.2017 16:09

              Это будет тяжеловесный ректангл. Если вы его передадите в программу, не работающую с UI, может все упасть. Тут нарушение принципа Лисков, которое явилось следствием слишком большой ответственности. Я бы так не делал. К тому же рисовать можно на разных платформах. Если надо рисовать, можно сделать какой-нибудь абстрактный рисовальщик (шаблон проектирования бридж). Но это явно не ректангл тоже.


              1. retran
                15.05.2017 10:04

                Дочерний класс изменяет контракт базового класса изменяется от наследования? Нет. Его можно использовать вместо базового? Да. LSP не нарушен.


                1. szolotarev
                  15.05.2017 11:49

                  Дочерний класс можно написать так, что будет невозможно его использовать в Program 1, так как нужно будет «подгребать» UI-ные зависимости. Это может нарушить LSP. А может и не нарушить. Все зависит от того, как был сделан потомок.


                  1. retran
                    15.05.2017 15:25

                    Можете привести пример наследования с добавлением одного метода и без изменения родительского контракта, который сломает LSP?


        1. MadridianFox
          14.05.2017 16:45
          -2

          Процедурный подход не выдерживает конкуренции с функциональным и объектно-ориентированным, разве что в случаях где важна производительность (я про программы написанные на си).
          И вы немного не правы. не «Классы-процедуры», а объекты, которые ни в коем случае не процедуры, т.к. объекты можно динамически связывать, в отличие от процедур.


          1. vanxant
            14.05.2017 18:24

            Процедурный подход не выдерживает конкуренции с функциональным и объектно-ориентированным

            Смелое утверждение. А можно цифры какие-нибудь в качестве пруфа, вот это всё?
            И сразу вопрос — почему тогда стандартная библиотека Си++, который вроде как «Си с ООП», вообще ничего от этого ООП использует, кроме синтаксического сахара в виде вызова методов через точку? Ни наследования, ни полиморфизма, ничего такого? И почему тогда труЪ-ООП конкурирующие либы от Microsoft, Borland, etc с треском провалились по факту?


            1. MadridianFox
              14.05.2017 18:48

              А кто сказал что С++ это идеал ООП? Это как раз то что вы и сказали — Си с ООП.


    1. onyxmaster
      14.05.2017 16:12

      Поздравляю, вы задали правильный вопрос :)
      Теперь можно пойти почитать http://blog.ploeh.dk/2017/01/27/from-dependency-injection-to-dependency-rejection/ :)


  1. vanxant
    14.05.2017 14:42

    Упрощая пример, вы вылили с водичкой и ребёнка. Ваша вьюха (RectanglePresenter) никак не связана с моделью (Rectangle). Ну т.е. вы передаёте прямоугольник в метод Draw (кстати, почему туда, а не в конструктор?), но никак не используете. Таким образом, оценить стоимость следования принципу единственной ответственности невозможно (а она ненулевая).

    Предлагается добавить в Rectangle несколько методов

    /* @return bool 
    true, если текущий прямоугольник полностью покрывает otherRectangle) */
    bool Contains (otherRectangle)  { ... };
    
    /* @return bool 
    true, если текущий прямоугольник хотя бы частично пересекается с otherRectangle) */
    bool Intersects (otherRectangle)  { ... };
    
    /* @return Rectangle 
    возвращает пересечение текущего прямоугольника с otherRectangle */
    Rectangle Intersect (otherRectangle)  { ... };
    
    /* масштабирует текущий прямоугольник в Alpha раз */
    void Scale(double Alpha) { ... };
    


    Ну и немного усложнить остальные классы, например:
    1. Вьюхе в метод Draw передаётся прямоугольник текущей видимой области экрана; отрисовка производится, только если текущий прямоугольник хоть немного попадает в видимую область. Если таки попадает, то рисуется не всё, а только попадающая часть.
    2. В коде вызывается Scale модели, после чего должна обновиться вьюха.

    И вот уже для такой задачи привести варианты решения с SRP и, для сравнения, с одним классом.


    1. szolotarev
      14.05.2017 14:53

      Я бы лучше поменял пример на какой-нибудь другой, чем так все усложнять. На самом деле, как я уже говорил, я старался максимально соответствовать примеру, приведенному в оригинальном англоязычном труде под названием Agile Principles, Patterns and Practices in C#, который написал господин Robert C. Martin. Может он и слишком прост, но думаю кое-что там все-таки дает возможность увидеть преимущества разделения логики по классам.
      То что RectanglePresenter неявно использует передаваемый ему в методе (почему бы и не в методе) объект Rectangle, это да, для простоты. Все это скрыто за комментарием // Draw the figure on UI.


      1. vanxant
        14.05.2017 15:09
        +1

        почему бы и не в методе

        Ух ты. А зачем вы тогда создаёте экземпляр класса RectanglePresenter? Гадите в память, нагружаете сборщик мусора, жрёте процессор в конце концов? Почему не использовать статический метод?

        Поймите меня правильно, вы прочитали модную книжку и пришли на форум специалистов продавать серебряную пулю. Пуля неплохая, не спорю. Но вас просят назвать цену. Не по акции типа «первая доза бесплатно», а в чуть более реальной жизни.


        1. szolotarev
          14.05.2017 15:19

          Зависимость можно передавать в метод, проперти, конструктор. Автор модной книжки уже даже и не помню как ее передавал. При чем тут это? Это просто демонстрация, это мог быть просто псевдо код. Статические методы, сборка мусора, нагрузка процессора — это тема другой дискуссии.


          1. vanxant
            14.05.2017 16:00

            Статические методы, сборка мусора, нагрузка процессора — это тема другой дискуссии

            Я спросил вас про цену продаваемой вами серебряной пули, а вы отвечаете, что это тема другой дискуссии. Ну ок, яснопонятно.


            1. szolotarev
              14.05.2017 16:20

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


              1. totuin
                14.05.2017 16:28

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


                1. szolotarev
                  14.05.2017 17:57

                  Эта величина зависит от ситуации. Поэтому и не рекомендуется всегда следовать паттернам. В конкретном случае примера из этой статьи цена должна превышать убытки априори.


    1. ZurgInq
      14.05.2017 15:32

      Может я не прав, но как я вижу:
      * Прямоугольник передаётся в метод, а не конструктор, потому, что через конструктор в дальнейшем будут передаваться зависимости.
      * Добавление методов Contains, Intersects и подобных явно противоречит принципу единой ответственности. В привидённом примере Rectangle является ValueObject, и должен описывать только своё поведение. Методы Contains и подобные лучше вынести в нечто под названием CollisionDetector. Но метод Scale вполне может быть у Rectanagle, т.к. только меняет его свойства, но и здесь можно выделить отдельный класс.


      1. vanxant
        14.05.2017 15:37

        1. Т.е. по вашему, вьюха от модели не зависит? Если это фабричный рисователь прямоугольников, то давайте его так и назовём. А также дадим ему соответствующий интерфейс (чтобы можно было передавать в метод Draw сразу пачку прямоугольников, а не строго по 1).

        2. Ну тогда, согласитесь, и вычисление площади нужно выносить в отдельный класс.


        1. ZurgInq
          14.05.2017 15:44

          1. Да, вьюха не зависит от модели. От того, что можно будет рисовать пачку прямоугольников, а не один, суть не изменится.
          2. Соглашусь, что в идеале можно\нужно вынести. На практике, мы можем получить бесконечное дробление.


          1. szolotarev
            14.05.2017 15:48

            Не надо выносить, прочитайте ответственность Rectangle и вы увидите:
            Класс Rectangle, ответственный за вычисление площади и предоставление значений длин сторон прямоугольника.

            Period.


            1. vanxant
              14.05.2017 15:51

              Может я не умею считать до двух, но вы перечислили две ответственности, лежащих на одном вашем классе Rectangle.


              1. szolotarev
                14.05.2017 15:54

                Нет, это все один контракт компонента Rectangle и одна ответственность, пусть и не элементарная.


            1. totuin
              14.05.2017 15:57
              +2

              Ну вы сами себе противоречите.

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


              Изменилось требование по выводу длин сторон из сатиметров в метры — Причина 1
              Изменилось требование вывода площади из квадратных миллиметров в квадратные километры — Причина 2

              Причем эти изменения могут произойти в разное время.
              Делаем ещё два класса? Один выводит только длинны сторон, второй только площадь на основе переданного им прямоугольника?


              1. szolotarev
                14.05.2017 16:12
                -1

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


  1. vanxant
    14.05.2017 15:19
    -3

    И ещё один вопрос автору, просто чтобы оценить его уровень понимания предмета разговора.
    А почему бы, собственно, не унаследовать RectanglePresenter от Rectangle? Это вполне себе укладывается в принцип единственной ответственности, вы можете реюзать Rectangle самостоятельно (например, в Program 1). При этом лишнего кода придётся писать ОЧЕНЬ сильно меньше, и работать будет быстрее.


    1. szolotarev
      14.05.2017 15:24
      +3

      Потому что RectanglePresenter это не ректангл при всем уважении. Не надо смешивать. О Принципе Лискоф я тут не говорю, говорю про банальное Is A. Наследованию предпочитаю аггрегацию в этом случае.


      1. vanxant
        14.05.2017 15:33

        RectanglePresenter это не ректангл при всем уважении

        Вполне себе Rectangle, прямоугольник на экране. У которого вполне себе есть та же площадь, в пикселях.
        Не надо смешивать

        Я знаю, что не надо, но объясните почему? Чем это грозит?
        О Принципе Лискоф

        А при чём здесь принцип Лисков? При таком наследовании он не нарушается, кстати. Если у вас какая-то либа ожидает Rectangle, вы вполне можете подсунуть ей RectanglePresenter, и всё будет работать как ожидалось.
        Наследованию предпочитаю аггрегацию в этом случае

        Простите, в вашем коде нет агрегации.


        1. szolotarev
          14.05.2017 15:46
          +1

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


    1. ZurgInq
      14.05.2017 15:36
      +1

      RectanglePresenter судя по комментарию в коде «Draw the figure on UI» рисует фигуру где то ещё. Он не является фигурой, а лишь отвечает за то, где и как её отрисовать. В память, на экран, куда-то ещё. И не обязательно именно конкретный Rectangle.


      1. szolotarev
        14.05.2017 15:51
        +1

        Exaclty :-)


  1. andreyverbin
    14.05.2017 16:36

    SRP более вреден чем полезен так как определения "ответственности" нет. Изменение ОС это причина для изменения? А смена архитектуры процессора? Что если прямоугольники нужны не с double, а int внутри для ускорения рассчетов? А если мы хотим SIMD? И так далее и тому подобное.


    Из всего SOLID только принцип подстановки Барбары Лисков заслуживает внимания, так как у него имеется внятная формулировка. Остальное полезно для ознакомления, применение на практике наталкивается на произвол из-за размытых формулировок.


    1. szolotarev
      14.05.2017 16:41
      +1

      Думаю ответственность — это контракт компонента, то есть набор предоставляемых им операций. Это коррелирует с принципом разделения интерфейсов. На каждый компонент должны быть написаны юниты, которые обозначают требования к компоненту. Single Reason to change — это существенные изменения в первоначальных требованиях, меняющие ожидания от компонента. Вот надо научиться правильно этот контракт определять видимо. Про остальные принципы я тоже планирую написать, если меня вконец не забанят :-)


  1. IL_Agent
    14.05.2017 17:57
    +1

    Столько уже копий сломано да статей понаписано про srp и прямоугольники с квадратами… Какой смысл в этой? Поспорить в комментах?


    1. szolotarev
      14.05.2017 18:23

      А какой смысл Вы вкладываете в свои статьи? Целью этой является обсуждение проблемы, я каждый существенный комментарий попытаюсь адресовать, чтобы ищущие люди в интернете могли дополнить свои знания по этой теме.


      1. vanxant
        14.05.2017 18:35
        +2

        Ну а мы задаём неудобные вопросы, чтобы те самые «ищущие люди» имели более одного мнения и хотя бы слышали, что есть какие-то подводные камни.


        1. szolotarev
          14.05.2017 19:32

          Exactly :-)


  1. samizdam
    14.05.2017 23:10
    +1

    >> Единственность Ответственности

    Ох… Надмозг какой-то. Не лучше ли «единственная ответственность»?


    1. BigD
      15.05.2017 09:42
      +1

      Единая ответственность, а не единственная.


      1. samizdam
        15.05.2017 09:59

        Согласен!


  1. BOM
    15.05.2017 14:26
    +1

    Мне не нравится пример, где специально приводится настолько разная ответственность двух методов класса — один вычисляет площадь, а другой рисует. В реальности тут и там встречаются случаи, когда грань между двумя ответственностями настолько тонкая, что вопрос ее разделения является скорее философским, нежели каким-то прикладным. И зачастую бывает так, что для одной и той же сущности приходится писать два разных класса, потому что single responsibility, хотя это кажется странным, сущность одна, а классов два (или больше). Как кошка в двух экземплярах, один экземпляр умеет eat(), другой poop() и обоим в конструкторе передается DigestiveSystem DSystem = new DigestiveSystem


    1. szolotarev
      15.05.2017 14:28

      Если привести такой неоднозначный пример, комментов было бы раза в два больше :-) Можно просто в конце статьи привести более подробное рассуждение на тему критериев применимости принципа, в которое включить Вашу мысль.