Comments 80
Понадобилось усилие, чтобы понять, что "square" — это не квадрат, это "площадь" в переводе с рунглиша.
А вас не смущает, что программы пишут на бревне?
Класс Rectangle хранит данные прямоугольника и позволяет вычислить его площадь. А может при рисовании не надо использовать площадь? А может для вычисления площади надо подтянуть ещё одну зависимость (ну вдруг мы считаем площадь прямоугольника в многомерном искривлённом пространстве, для чего нужна отдельная библиотека)?
А делая поля публичными, вы просто увеличиваете зависимость между классами. Даже если вы сделаете геттеры, зависимость смягчится, но сохранится. Должен ли класс, который рисует, зависеть от класса. который вычисляет ненужное ему значение? Или он должен зависеть от более атомарного, лучше даже интерфейса, который олицетворяет собой только прямоугольник и ничего лишнего?
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]
Согласен, что понимается это хуже, чем если говорить в терминах ответственности. Автор видимо имел в виду глобальную причину для изменения, которая может содержать более мелкие подпричины. Я хоть как-то попытался привести это к понятию единственности ответственности. Если у кого-то получится лучше, хорошо.
Эта книга переведена и продаётся в лабиринте. Собственно я Ее читал неделю назад. Ваше стремление написать статью похвально, но она выглядит похожим на вольный, сильно сжатый пересказ главы из этой книги.
Ну это потому что у вас в статье тема не раскрыта) в книге говорится что причина для изменения определяется в контексте программы. Т.е. Если в вашей программе расчёт площади и отрисовка прямоугольника идут бок о бок и никогда не меняются отдельно друг от друга, то собственно это означает что у них одна причина для изменения и класс не нарушает принципа.
А зачем в такой ситуации вообще нужны классы?
А как без классов работать с <название модного IoC контейнера>?
Проблемма современных програмистов, что они разучились думать самостоятельно, а предпочитают тупо следовать патернам, принципам и книгам типа «Clear Code», которые им забили в голову во время обучения.Почитать такие книжки конечно полезно, посмотреть как другие работают, что применяют, возможно найти там несколько полезных примеров. Но вот использовать их как библию и 10 заповедей — зло. Сегодня моден принцип единственной ответственности, и мы клеймим всех кто ему не подчиняется, завтра взойдёт другая звезда, предложит принцип максимальной ответственности, и мы будем дружно клеймить Егора Бугаенко. Прежде всего надо научится думать своей головой, и понять что все эти принципы придумывали конкретные люди с конкретным (своим собственным) образом мышления, и для конкретного котекста.
Но всегда нужно помнить о том, что это всего лишь общая рекомендация, и решение по его применению следует принимать исходя из конкретной ситуации.
Вообще то классы, да и вообще ООП было придумано как ещё один способ структурирования кода.
Не совсем. Парадигмы программирования, такие как ФП и ООП придуманы для работы с кодом на более высоком уровне абстракции. А для структурирования придумали современные управляющие конструкции (if,while,for..) и модульность.
Имеет ли смысл делать папку всего с одним вложенным листочком?
Имеет. Объект в ООП, как и функция в ФП, это способ динамического связывания кода.
Лучше конечно ввести модель прямоугольника безо всяких там методов, а методы собственно реализовать в других классах — рисовальщике и вычисляльщике..
И в этом контексте да, зачем классы? Это хороший процедурный, структурированный код, который можно реализовать хоть на C, хоть на Pascal. Кстати, SRP это про ООП или вообще про организацию кода?
И вы немного не правы. не «Классы-процедуры», а объекты, которые ни в коем случае не процедуры, т.к. объекты можно динамически связывать, в отличие от процедур.
Процедурный подход не выдерживает конкуренции с функциональным и объектно-ориентированным
Смелое утверждение. А можно цифры какие-нибудь в качестве пруфа, вот это всё?
И сразу вопрос — почему тогда стандартная библиотека Си++, который вроде как «Си с ООП», вообще ничего от этого ООП использует, кроме синтаксического сахара в виде вызова методов через точку? Ни наследования, ни полиморфизма, ничего такого? И почему тогда труЪ-ООП конкурирующие либы от Microsoft, Borland, etc с треском провалились по факту?
Поздравляю, вы задали правильный вопрос :)
Теперь можно пойти почитать http://blog.ploeh.dk/2017/01/27/from-dependency-injection-to-dependency-rejection/ :)
Предлагается добавить в 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 и, для сравнения, с одним классом.
То что RectanglePresenter неявно использует передаваемый ему в методе (почему бы и не в методе) объект Rectangle, это да, для простоты. Все это скрыто за комментарием // Draw the figure on UI.
почему бы и не в методе
Ух ты. А зачем вы тогда создаёте экземпляр класса RectanglePresenter? Гадите в память, нагружаете сборщик мусора, жрёте процессор в конце концов? Почему не использовать статический метод?
Поймите меня правильно, вы прочитали модную книжку и пришли на форум специалистов продавать серебряную пулю. Пуля неплохая, не спорю. Но вас просят назвать цену. Не по акции типа «первая доза бесплатно», а в чуть более реальной жизни.
Статические методы, сборка мусора, нагрузка процессора — это тема другой дискуссии
Я спросил вас про цену продаваемой вами серебряной пули, а вы отвечаете, что это тема другой дискуссии. Ну ок, яснопонятно.
* Прямоугольник передаётся в метод, а не конструктор, потому, что через конструктор в дальнейшем будут передаваться зависимости.
* Добавление методов Contains, Intersects и подобных явно противоречит принципу единой ответственности. В привидённом примере Rectangle является ValueObject, и должен описывать только своё поведение. Методы Contains и подобные лучше вынести в нечто под названием CollisionDetector. Но метод Scale вполне может быть у Rectanagle, т.к. только меняет его свойства, но и здесь можно выделить отдельный класс.
2. Ну тогда, согласитесь, и вычисление площади нужно выносить в отдельный класс.
2. Соглашусь, что в идеале можно\нужно вынести. На практике, мы можем получить бесконечное дробление.
Класс Rectangle, ответственный за вычисление площади и предоставление значений длин сторон прямоугольника.
Period.
Класс должен иметь только одну причину для изменения
Изменилось требование по выводу длин сторон из сатиметров в метры — Причина 1
Изменилось требование вывода площади из квадратных миллиметров в квадратные километры — Причина 2
Причем эти изменения могут произойти в разное время.
Делаем ещё два класса? Один выводит только длинны сторон, второй только площадь на основе переданного им прямоугольника?
А почему бы, собственно, не унаследовать RectanglePresenter от Rectangle? Это вполне себе укладывается в принцип единственной ответственности, вы можете реюзать Rectangle самостоятельно (например, в Program 1). При этом лишнего кода придётся писать ОЧЕНЬ сильно меньше, и работать будет быстрее.
RectanglePresenter это не ректангл при всем уважении
Вполне себе Rectangle, прямоугольник на экране. У которого вполне себе есть та же площадь, в пикселях.
Не надо смешивать
Я знаю, что не надо, но объясните почему? Чем это грозит?
О Принципе Лискоф
А при чём здесь принцип Лисков? При таком наследовании он не нарушается, кстати. Если у вас какая-то либа ожидает Rectangle, вы вполне можете подсунуть ей RectanglePresenter, и всё будет работать как ожидалось.
Наследованию предпочитаю аггрегацию в этом случае
Простите, в вашем коде нет агрегации.
SRP более вреден чем полезен так как определения "ответственности" нет. Изменение ОС это причина для изменения? А смена архитектуры процессора? Что если прямоугольники нужны не с double, а int внутри для ускорения рассчетов? А если мы хотим SIMD? И так далее и тому подобное.
Из всего SOLID только принцип подстановки Барбары Лисков заслуживает внимания, так как у него имеется внятная формулировка. Остальное полезно для ознакомления, применение на практике наталкивается на произвол из-за размытых формулировок.
Ох… Надмозг какой-то. Не лучше ли «единственная ответственность»?
Вот насчет «Класс должен иметь только одну причину для изменения» — но если допустим у 2х отчетов (бизнес сущностей любых других) есть общая логика и её вынести в отдельный общий класс то он будет иметь 2 причины для изменения — 1 изменилась логика 1го отчета, и 2я — изменилась логика 2го отчета. Как быть с этой дилемой? Вынесение функционала в общий класс, вроде это конечно логично, но я каждый раз мучаюсь вопросом.
Класс должен иметь только одну причину для изменения
С этой формулировкой возникло очень много путаницы, поэтому в дальнейшем Мартин разъяснял, что имеется в виду: "причина" изменения – это люди, стейкхолдеры. В вашем примере причина изменения одна и та же, если это, например, два финансовых отчета. Ключевой момент – если это действительно общая логика, а не переиспользование кода для разных отчетов, которые в данный момент имеют похожее поведение.
Не разработчики, а люди (точнее даже, роли, физически это может быть один человек) со стороны бизнеса, которые могут запросить изменения, или которые отвечают за то, что делает этот метод.
Хороший пример в статье Мартина:
public class Employee {
public Money calculatePay();
public void save();
public String reportHours();
}
Эмпирическое правило для определения, нарушается ли SRP: кого уволят, если в этом классе что-то сломается?
Сломается calculatePay()
– уволят финансового директора, потому что правильно считать зарплату – это его ответственность.
Сломается save()
и дропнется база – увольняем технического директора.
Сломается reportHours()
– уволим операционного директора, отчеты – это его работа.
Очевидно, что здесь принцип нарушается, потому что за класс "отвечают" сразу три стейкхолдера.
Понятно, что в реальной жизни достанется одному и тому же программисту, но сам Мартин и говорит, что это просто гипербола, чтобы проще было понять принцип.
SRP часто сводят к формулировке "класс должен делать одну вещь", но это неправильно. Это гораздо более высокоуровневый принцип.
SOLID: принцип единственности ответственности