Pull to refresh

Comments 80

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

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

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

Не поправилось в первом примере — там по-прежнему:
public double Square()

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

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

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

Вопрос на засыпку: x и y, которые логичнее было бы назвать w и h, у вас приватные. Как presenter узнает ширину и высоту прямоугольника, чтобы его нарисовать?
Это хороший вопрос :-) Переименовать можно, но это имхо субъективно. Можно вынести эти поля в паблик проперти, учту замечание. Хотя сами понимаете, к принципу единственности ответственности это сильно не относится. Это просто бага. Но все равно спасибо!
В том и дело что относится. Принцип у вас нарушен)
Класс Rectangle хранит данные прямоугольника и позволяет вычислить его площадь. А может при рисовании не надо использовать площадь? А может для вычисления площади надо подтянуть ещё одну зависимость (ну вдруг мы считаем площадь прямоугольника в многомерном искривлённом пространстве, для чего нужна отдельная библиотека)?
А делая поля публичными, вы просто увеличиваете зависимость между классами. Даже если вы сделаете геттеры, зависимость смягчится, но сохранится. Должен ли класс, который рисует, зависеть от класса. который вычисляет ненужное ему значение? Или он должен зависеть от более атомарного, лучше даже интерфейса, который олицетворяет собой только прямоугольник и ничего лишнего?
Есть в этом замечании доля истины имхо. Лучше конечно ввести модель прямоугольника безо всяких там методов, а методы собственно реализовать в других классах — рисовальщике и вычисляльщике. Но в этом примере, чтобы не уходить слишком далеко, можно определить ответственность класса Rectangle как предоставление геометрических данных о фигуре — длин сторон, площади. Тогда все должно «проканать». Думаю ответственность может содержать логически объединенные группы требований. Например, одна ответственность может быть у класса, реализующего 4 CRUD операции для работы с данными, даже если кто-то и будет использовать только одну. Думаю такое вполне допустимо и разумно. Поправлю статью, более четко определив ответственности, спасибо.
UFO just landed and posted this here
Я опирался на оригинальный труд Agile Principles, Patterns and Practices in C#, где определение дано так:
A class should have only one reason to change.

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


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

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

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

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

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

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

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

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

А как без классов работать с <название модного IoC контейнера>?
Кстати, примерно об этом же говорит и Егор Бугаенко — класс должен содержать минимум методов, не больше 4-5, а в идеале вообще 1.
Вообще то классы, да и вообще ООП было придумано как ещё один способ структурирования кода. Каждый класс представляет собой как бы папочку методов и значений, свойственных сущности которую описывает данный класс. Имеет ли смысл делать папку всего с одним вложенным листочком?

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

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

Имеет. Объект в ООП, как и функция в ФП, это способ динамического связывания кода.
Наверное, если опираться на то, что говорит yegor256, то в статье вообще неудачный пример, на мой взгляд. Фактически, получается, что будет модель данных (DTO), которая содержит ширину и высоту и классы-процедуры, которые с этой моделью будут работать. О чем, собственно, автор и говорит в одном из комментариев выше
Лучше конечно ввести модель прямоугольника безо всяких там методов, а методы собственно реализовать в других классах — рисовальщике и вычисляльщике.
.
И в этом контексте да, зачем классы? Это хороший процедурный, структурированный код, который можно реализовать хоть на C, хоть на Pascal. Кстати, SRP это про ООП или вообще про организацию кода?
Насколько я понял, первоначально его относили к классам. Наверное этому принципу можно следовать и в других случаях, если это делает код менее жестким, хрупким, мобильным. Почему нет? То что класс Rectangle содержит данные и процедуры, так это было заложено в основу ООП, когда ни о каком stateless еще и не слышали. Так что не вижу причин ужасных так не делать. А вот упрямое следование принципам может привести к нехорошим последствиям.
А если рассмотреть такой пример. Вот, допустим, мы в коде используем классическое наследование. И для класса Rectangle вводим класс-наследник, расширяющий поведение базовового класса за счет добавления метода Draw. Один класс, один метод Draw, ответственность определена — отрисовка. Назовем его, предположим, DrawableRectangle. Этот дизайн соответствует SRP?
Это будет тяжеловесный ректангл. Если вы его передадите в программу, не работающую с UI, может все упасть. Тут нарушение принципа Лисков, которое явилось следствием слишком большой ответственности. Я бы так не делал. К тому же рисовать можно на разных платформах. Если надо рисовать, можно сделать какой-нибудь абстрактный рисовальщик (шаблон проектирования бридж). Но это явно не ректангл тоже.
Дочерний класс изменяет контракт базового класса изменяется от наследования? Нет. Его можно использовать вместо базового? Да. LSP не нарушен.
Дочерний класс можно написать так, что будет невозможно его использовать в Program 1, так как нужно будет «подгребать» UI-ные зависимости. Это может нарушить LSP. А может и не нарушить. Все зависит от того, как был сделан потомок.
Можете привести пример наследования с добавлением одного метода и без изменения родительского контракта, который сломает LSP?
Процедурный подход не выдерживает конкуренции с функциональным и объектно-ориентированным, разве что в случаях где важна производительность (я про программы написанные на си).
И вы немного не правы. не «Классы-процедуры», а объекты, которые ни в коем случае не процедуры, т.к. объекты можно динамически связывать, в отличие от процедур.
Процедурный подход не выдерживает конкуренции с функциональным и объектно-ориентированным

Смелое утверждение. А можно цифры какие-нибудь в качестве пруфа, вот это всё?
И сразу вопрос — почему тогда стандартная библиотека Си++, который вроде как «Си с ООП», вообще ничего от этого ООП использует, кроме синтаксического сахара в виде вызова методов через точку? Ни наследования, ни полиморфизма, ничего такого? И почему тогда труЪ-ООП конкурирующие либы от Microsoft, Borland, etc с треском провалились по факту?
А кто сказал что С++ это идеал ООП? Это как раз то что вы и сказали — Си с ООП.
Упрощая пример, вы вылили с водичкой и ребёнка. Ваша вьюха (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 и, для сравнения, с одним классом.
Я бы лучше поменял пример на какой-нибудь другой, чем так все усложнять. На самом деле, как я уже говорил, я старался максимально соответствовать примеру, приведенному в оригинальном англоязычном труде под названием Agile Principles, Patterns and Practices in C#, который написал господин Robert C. Martin. Может он и слишком прост, но думаю кое-что там все-таки дает возможность увидеть преимущества разделения логики по классам.
То что RectanglePresenter неявно использует передаваемый ему в методе (почему бы и не в методе) объект Rectangle, это да, для простоты. Все это скрыто за комментарием // Draw the figure on UI.
почему бы и не в методе

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

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

Я спросил вас про цену продаваемой вами серебряной пули, а вы отвечаете, что это тема другой дискуссии. Ну ок, яснопонятно.
Да нет цены, просто хочу получить полезный фидбек. Первая публикация, не судите строго.
Любое применение любого патерна призванного облегчить поддержание проекта в будущем, имеет определённую цену в момент разработки. Выражаться она может в строчках кода, в человеко часах необходимых для реализации, в сдвиге сроков выхода проекта. Вот про эту цену Вас и спрашивают. Она есть всегда, и часто не оправданно велика.
Эта величина зависит от ситуации. Поэтому и не рекомендуется всегда следовать паттернам. В конкретном случае примера из этой статьи цена должна превышать убытки априори.
Может я не прав, но как я вижу:
* Прямоугольник передаётся в метод, а не конструктор, потому, что через конструктор в дальнейшем будут передаваться зависимости.
* Добавление методов Contains, Intersects и подобных явно противоречит принципу единой ответственности. В привидённом примере Rectangle является ValueObject, и должен описывать только своё поведение. Методы Contains и подобные лучше вынести в нечто под названием CollisionDetector. Но метод Scale вполне может быть у Rectanagle, т.к. только меняет его свойства, но и здесь можно выделить отдельный класс.
1. Т.е. по вашему, вьюха от модели не зависит? Если это фабричный рисователь прямоугольников, то давайте его так и назовём. А также дадим ему соответствующий интерфейс (чтобы можно было передавать в метод Draw сразу пачку прямоугольников, а не строго по 1).

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

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

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


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

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

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

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

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

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

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


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

Думаю ответственность — это контракт компонента, то есть набор предоставляемых им операций. Это коррелирует с принципом разделения интерфейсов. На каждый компонент должны быть написаны юниты, которые обозначают требования к компоненту. Single Reason to change — это существенные изменения в первоначальных требованиях, меняющие ожидания от компонента. Вот надо научиться правильно этот контракт определять видимо. Про остальные принципы я тоже планирую написать, если меня вконец не забанят :-)
Столько уже копий сломано да статей понаписано про srp и прямоугольники с квадратами… Какой смысл в этой? Поспорить в комментах?
А какой смысл Вы вкладываете в свои статьи? Целью этой является обсуждение проблемы, я каждый существенный комментарий попытаюсь адресовать, чтобы ищущие люди в интернете могли дополнить свои знания по этой теме.
Ну а мы задаём неудобные вопросы, чтобы те самые «ищущие люди» имели более одного мнения и хотя бы слышали, что есть какие-то подводные камни.
>> Единственность Ответственности

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

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

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

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

Спасибо за объяснение, да пока, что это общая логика именно для 2х отчетов, в будущем это может изменится но не планируется, разница только во входных данных.
А то значит люди? То есть разработчики которые может изменить этот код? я не понял до конца.

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


Хороший пример в статье Мартина:


public class Employee {
  public Money calculatePay();
  public void save();
  public String reportHours();
}

Эмпирическое правило для определения, нарушается ли SRP: кого уволят, если в этом классе что-то сломается?


Сломается calculatePay() – уволят финансового директора, потому что правильно считать зарплату – это его ответственность.


Сломается save() и дропнется база – увольняем технического директора.


Сломается reportHours() – уволим операционного директора, отчеты – это его работа.


Очевидно, что здесь принцип нарушается, потому что за класс "отвечают" сразу три стейкхолдера.


Понятно, что в реальной жизни достанется одному и тому же программисту, но сам Мартин и говорит, что это просто гипербола, чтобы проще было понять принцип.


SRP часто сводят к формулировке "класс должен делать одну вещь", но это неправильно. Это гораздо более высокоуровневый принцип.

Sign up to leave a comment.

Articles