Pull to refresh

Comments 198

Более того, мне даже кажется, что за анемичной моделью будущее. Все приложения, по большому счету, создаются для обработки данных. И представление разработчика о предметной области (модель) — это всего лишь одно из множества возможных представлений (моделей). Эту мысль замечательно со всех сторон показывает коллега maxstroy в цикле своих статей.
Мы никоим образом не сможем избежать хранения данных, но мы можем избежать связывания данных с правилами их обработки. Ничего не имею против ООП, но я также ничего не имею и против ФП. Просто, чем сложнее проектируемая предметная область, тем выше шанс, что придется один и тот же набор данных рассматривать с точки зрения разных моделей. И чем раньше отделить данные (то, что хранится в базе, грубо говоря) от обработчиков (то, что существует в программном коде), там меньше вероятность применить имеющуюся модель (объект = данные + поведение) в непредназначенных для этой модели (поведения) условиях.

Все (бизнес)приложения создаются для получения результатов моделирования предметной области. Обработка данных по заданным правилам — это способ, которым эти результаты получаются. Отделять данные от правил в общем случае нужды нет, равно как и использовать одни данные в разных моделях по разным правилам. Иногда это хороший вариант, но в общем случае он вводит паразитные зависимости.
Не надо переводить pattern как шаблон, С++ программисты не одобрят. Эти термины давно переводятся(читаются) как есть — паттерн и антипаттерн.
Да, пожалуй, стремясь избавиться от англицизмов я несколько перегнул палку.
Посмею заявить, что не С++ программисты не имеют ничего против слов «шаблон» и «анти-шаблон».

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

Да и людям, которые слабо знакомы с английским и только начинают познавать азы проектирования, слово «шаблон» будет более знакомо нежели «паттерн»(по крайней мере так было у многих людей с которыми мне доводилось общаться)

P.S. не подумайте что я с Вами спорю — просто хотелось изложить свою точку зрения и наблюдения.
Но известнейшая книга Гамма (приведена в статье) «Приемы объектно-ориентированного проектирования. Паттерны проектирования». Да и в самой статье то паттерны, то шаблоны употребляется.
Возможно данная книга сыграла важную роль в популяризации «паттернов». Сложно сейчас рассуждать об этом.
Но во когда смешивают «паттерны» и «шаблоны» в одном контексте — вот это я считаю плохим тоном.
Я предпочитаю использовать или английский термин или перевод, но не все вместе — считаю что от этого получается какая-то каша.
В одном интервью он сказал, что сейчас очень многое написал бы по-другому. Особенно главку про Singleton.
В плюсах, например, согласно C++ Core Guidelines очень много теперь по другому. И синглтонов надо избегать. Всё меняется-то.
На первый взгляд выглядит, что богатая модель искусственно перегружена ответственностями — оба метода слабо относятся к сущности Customer, по-моему, особенно метод покупки. А ActiveRecord многими сам по себе считается антипаттерном.

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

/me не понял, почему нельзя классы, обеспечивающие инварианты, сделать как обёртку к "анемичным"? Вроде и волки сыты, и овцы целы, и пастуху вечная память — модель, "богатая" снаружи и "анемичная" внутри.

Дык вроде автор по факту и показывает, что если богатую модель вести в SOLID, ровно то что вы пишете и выйдет)

это по сути функциональный подход, где данные и методы разделены
мне (и не только) кажется тут более будет понятен в контексте ООП такой принцип — чем меньше у метода параметров/зависимостей (в том числе неявных в виде полей и методов классов) — тем лучше
плюс в идеале у метода не должно быть ненужных параметров
пример БМПО в таком случае очевидно хуже так как каждый метод принимающий Item неявно зависит от логики purchase, а в примере АМПО такая зависимость будет явно указана через PurchaseService

Поправлю: не функциональный, а процедурный подход

Автор статьи забыл сказать с т.з. чего нечто является антипаттерном.


С т.з. ООП — это явный антипаттерн, т.к. ООП означает поведение, когда объекты обмениваются сообщениями, вызывая методы и во главу угла ставится поведение, а данные прячутся.


Анемичность же — явный признак не ООП, а его "соседа" ADT. Читаем для примера http://www.cs.utexas.edu/~wcook/papers/OOPvsADT/CookOOPvsADT90.pdf


Когда данные живут отдельно от поведения — это уже признак разорванности и не-целостности концепций. Т.е. банально "вы не умеете их готовить". Представьте организм, в котором бы кости и мышцы были отдельно, кожа сбоку, а кровеносная система вообще своя у всех. Вот это пример анемичности.


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

Все еще проще. Классическое ООП = «Rich Data Model» = антипаттерн. А «Anemic Data Model» — это способ адептов ООП сказать «не пишите на ООП, берите ФП», не признаваясь что вся концепция «объект сам с собой что-то делает» — провалилась.
Я с Вами не соглашусь.
Классическое ООП = «Rich Data Model» = антипаттерн.

Это заявление выглядит как типичное «ваше ООП отстой, а вот ФП — это круто».

Говнокодище пишут и на ООП языках и на ФП — проблема, в основном, в людях, а не в ООП или ФП.

К примеру, люди берут типичный MVC framework для Enterprise системы, засовывают всю бизнес логику в AcctiveRecord, а потом мы видим тысячи гневных отзывов о AcctiveRecord.
Но в то же время тысячи людей используют типичный MVC framework с AcctiveRecord для небольших систем и вполне себе счастливы, а заказчик получает новый фичи по рассписанию.

Так вот, не в AcctiveRecord проблема у первых, а в них самих. Каждый шаблон, методология и язык применимы для решения определенного ряда задач и если попытаться выйти за рамки применения то сразу начнутся проблемы.

Просто с засовыванием бизнес-логики в ActiveRecord ещё можно мириться в достаточно больших приложениях, он для того и создан. Но вот когда в одном методе переплетается и бизнес-логика, и логика хранения (как в примере в статье), вот тут уже тушите свет…

VolCh Вы естественно правы — я обобщенно говорил.

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

Для примера и привел ActiveRecord т.к. с ним очень часто допускают ошибки и, как Вы и сказали, начинают делать лапшу из бизнес и логики хранения. В итоге получаются классы на 1500 — 4000 тысячи строк где код на все случаи жизни с переплетением бизнес правил и логики ОРМ.
99% подобных проблем лечится даже не архитектурой а стайлгайдом.
Не приниать на код-ревью классы где в методах больше 20 строк а классы больше 200 строк. Ну не строго конечно, но превышения должны быть единичны и иметь вескую причину.
Когда они начнут их дробить на слои создавая из лапши лазанью наследования, у них хоть чуток начнут шестеренки крутиться на тему структуры. Но даже если лазанья останется, а не появится желание разделять сущности, то все равно с таким макаронным монстром жить будет проще. Разумеется если соблюдены хоть какие-то нормы именования и компановка хоть какую-то логику имеет.

Обычно, увы, ругаются не те, кто их применил, а те, кому это применение досталось в наследство. Непосредственно применяющие обычно просто не замечают проблем или, вполне сознавая их, относят их к техническому долгу. Ведь по сути ActiveRecord считается антипаттерном не потому, что совмещает две отвественности в одном классе, а потому что очень часто эти ответсвенности применяющие паттерн пихают уже в один метод, например, добавляя в мутирующий состояние метод после собственно мутации ещё и вызов save(), чтобы не вызывать его явно каждый раз. И вот, без глубокого вникания в код ты уже и не знаешь, произойдёт ли сохранение после какого-то вызова или нет, где-то происходит, а где-то нет. И это в случае "плоского" объекта уже плохо, а уж если там по цепочке объекты мутируют друг друга...

VolCh я с вами согласен.

Действителльно припекает зачастую тем, кто получает в подарок ведро лапши.

Но добавлю, что порой даже те, кто начал варить лапшу, понимают что они наделали, но винят в этом ActiveRecord, framework и т.п. (сам грешен — так делал в прошлом)

А насчет Вашего описания проблемы с использованием ActiveRecord — я об этом и думал, но не расписал более детально как Вы.

Не понимаю, как можно винить паттерн или фреймворк, если ты сам решил его применять, особенно понимая, что применяешь ты его неправильно :)

Это как раз легко.
1. Решение могло быть принято давно и не тобой, а ты теперь разгребаешь последствия нескольких лет его правильного и неправильного применения. При этом вторые обращают на себя куда больше внимания по понятным причинам, из-за чего может казаться, что они составляют 90% общего объема.
2. Решение могло быть принято тобой, но неверно понято рядом последователей. И поскольку некоторые подходы позволяют отстрелить себе ноги легче, чем другие, — есть основания «винить» (ну, точнее, признавать недостатки) фреймворки, которые в большей мере допускают неверное их использование.
добавлю к коментарию pankraty

представим что есть Junior или Middle разработчик. Ему дают задачу и он ее рализует основываясь на подходах принятых в текущем framework'е. Потом идет следующая задача, за ней следующая и в какой-то момент приложение разрастается, а подходы продолжают применяться как для более простого варианта и со временем все скатывается в помойку.

Разработчик делает вывод, что виноват framework.

В том, что его не предупредили вовремя, что пора менять фреймворк или, хотя бы, подходы?

думаю можно и так сказать.

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

Но реалии таковы, что во многих фирмах фреймворк выбран за разработчиков и хочешь не хочешь, а берешь то, что решили за тебя.
То же и с подходами.

К примеру, фирма конвеером клепает простенькие заказы на Yii2 и вдруг заходит что-то серьезное. Скорее всего начнут делать как для конвеера и наберут кучу проблем т.к. надо было отказаться сразу от Yii2, а скорее всего и от PHP. (это так, пример из пальца чтобы передать суть)

Ну не фреймворк же виноват, а тот, кто принял решение его использовать. Ну или тот, кто не принял решение перестать его использовать, когда задачи его переросли.

Валить все проблемы на глупость людей — не конструктивно. Если люди глупые — значит их учат плохо. А учат их ООП-шники во главе с Фаулерами и ДядьБобами. Сугубо гуманитарными методами, в стиле «белочка и зайка есть животные по признаку наличия четырёх лапок».

Все это происходит уже лет 20, и никаких видимых результатов не приносит. Пора уже по-тихоньку заканчивать.

Начать заканчивать можно с того, что прекратить эти километровые споры про «Anemic vs. Rich Data Model»
Не мешай нам холиварить!
А учат их ООП-шники во главе с Фаулерами и ДядьБобами. Сугубо гуманитарными методами, в стиле «белочка и зайка есть животные по признаку наличия четырёх лапок».

Я вот каждый раз, когда это слышу, спрашиваю (и ни разу не получил еще ответа): а что же гуманитарного в том, как учат "ООП-шники" вообще и Фаулер и Мартин в частности?

Отсутствие научного метода.

Оу, научный метод. А расскажите мне, пожалуйста, про научный метод (прямо вот начиная с критериев научности) применительно к разработке ПО?

Использование научных достижений и их, скажем так, созидание — суть разные вещи. Вы пользуетесь булевой логикой, что есть результат науки, но вы не создаете эту булеву логику с нуля. Вы пользуетесь ML-алгоритмами, но вы не используете математический аппарат для обоснования этих алгоритмов как рабочих. Ну и так далее. Ниже (выше) верно отметили, программирование ближе всего к инженерной (т.е. практической) деятельности.

Это ну примерно как быть Стивеном Хокингом, который придумал некую теорию, которую надо проверить на каком-нибудь ускорителе, и быть оператором этого ускорителя. Где-то посередине — те, кто построил ускоритель (создали вашу ОС, средства разработки в ней, тп).
Использование научных достижений и их, скажем так, созидание — суть разные вещи. Вы пользуетесь булевой логикой, что есть результат науки, но вы не создаете эту булеву логику с нуля.

Казалось бы, прекрасный пример того, что в разработке ПО научный метод совершенно не обязателен. Мне-то интересны, наоборот, примеры использования, причем хоть сколько-нибудь мейнстримного, "научного метода" в разработке.

Я скорее просто подвел итог вашим комментариям выше :-) Примеры научного метода в разработке я сходу не могу даже придумать — наука может быть прикладной областью в разработке, то есть областью, куда (где) применяется (прикладывается) в итоге программный продукт. Но сама его разработка — не знаю.

Как минимум, прикладное и системное программирование — не научная деятельность, а инженерная. Как и любая (хотя может и не любая) другая инженерная деятельность она базируется на достижениях науки, но напрямую научный метод не использует, принимает господствующие теории и гипотезы на веру и использует их предсказательную силу.

архитектура по принципам SOLID

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

Чтобы грамотно спроектировать систему, нужно иметь достаточный опыт проектирования систем, а так же видеть много примеров качественной архитетуры уже готовых систем. А не заучивать что означают буковки в аббривеатуре SOLID.

У принципа SOLID — 2 проблемы, первое то что его придумали университетские теоретики, которые априори будут стоять на ступеньку практиков, а второе то что большинство воспринимает такие принципы как догму, даже не пытаясь подвергнуть их критике, не говоря уже о том чтобы на своем опыте самим дойти до понимания этих принципов.

Например, с чего вы вообще решили что архитектура которая соотвествует принципам SOLID — это хорошая архитектура?
его придумали университетские теоретики

Вообще-то нет. Вполне себе практики и придумали.


об «архетиктуре» перед проектированием не заканчивался бы написанием непонятного, неподдерживаемого говнокода.

Вы говорите о big upfront design. Ваши эти "теоретики" обычно пишут в своих чисто теоритических книжках что это не очень хорошая идея, и хоть проектирование и важно, следует решать проблемы по мере их поступления а не просто так. Особенно с учетом того что принципы в духе Open/Close вообще не достижимы на практике (это как получить систему вообще без связанности, что по определению невозможно).


большинство воспринимает такие принципы как догму

Что забавно ибо сами принципы эти по определению должны учитывать контекст и поток изменений требований. То есть один и тот же код в разных контекстах может как соблюдать так и нарушать эти принципы.


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


Например, с чего вы вообще решили что архитектура которая соотвествует принципам SOLID — это хорошая архитектура?

Цель SOLID — уменьшить каскад изменений, уменьшить сложность системы. Если вы "соблюдаете SOLID" но у вас этого нет — то возможно вы их не соблюдаете на самом деле. Это вполне частое явление, как никак концепт весьма сложный.


Да и потом, есть еще GRASP, которые чуть-чуть проще для восприятия чем SOLID на мой взгляд, но в целом примерно о том же.


А не заучивать что означают буковки в аббривеатуре SOLID.

То есть вы предлагаете заучивать примеры хорошей архитектуры. Причем узнать о том хорошая она или нет мы по сути можем поработав с ней хотя бы пол года с учетом изменений требований. А это означает что первые лет 5 вообще не стоит подпускать людей к гринфилд проектам. Так выходит?


Вот еще вопрос — есть такое понятие как coupling и cohesion. Должен ли разработчик знать что это? Должен ли понимать что coupling должен быть низким а cohesion высоким и что это дает? И если да, осознаете ли вы что SRP ~= cohesion а OCP ~= coupling?

его придумали университетские теоретики

Вообще-то нет. Вполне себе практики и придумали.

Как раз таки теоретики. Я бы даже уточнил — популисты. Есть теория управляемости, которая говорит, что у человека в голове помещается от 5 до 9 сущностей, так вот SRP этому явно противоречит.
Есть теория управляемости, которая говорит, что у человека в голове помещается от 5 до 9 сущностей, так вот SRP этому явно противоречит.

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

Нет, не очевидно. Если классов будет 5 вместо 25, но функциональность будет той же, удержать их в голове будет ничуть не легче, т.к. единицей внимания все-таки является не класс сам по себе, а единица функциональности.
Кроме того, я не привязывался бы к конкретным цифрам 5-9, поскольку «удерживать внимание на N сущностей одновременно» это не то же самое, что «помнить и понимать устройство N сущностей» (второе, на мой взгляд, имеет куда большее значение в практической деятельности).
В случае большого количества мелких сущностей помнить и понимать их устройство, как ни удивильно, проще, т.к. они естественным образом группируются по подобию. Например, «это валидаторы, по одному на каждый тип сущности, они делают то-то и то-то», «а это репозитории, они устроены примерно так-то и так-то, вызываются обычно из таких-то мест», «вот эта пачка классов соответствует различным стратегиям, используются там-то, выбор стратегии зависит от того-то» и т.п. В случае больших классов с многочисленными методами и внутренними взаимосвязями когнитивная нагрузка выше, как результат, помнить и понимать их устройство — сложнее.
Часть ответа написал ниже. Категорически не согласен с вашими выводами. Абстракции на то и абстракции, чтобы можно было не знать их устройство, но вы всегда знаете где это устройство посмотреть в случае необходимости. С взаимосвязями сущностей ситуация обратная, необходимо помнить, как они между взаимодействуют. На самом деле я не призывают писать god объекты, как может показаться. Должен быть баланс между количеством сущностей и их функционалом, вот для меня нахождение этого баланса и видится искусством.

Это вопрос декомпозиции/модульности системы, как организовать поведение так, что бы с этим легко было работать. Что бы модули были логически целостной единицей (SRP тот же как раз про это), и что бы связи между отдельными модулями были минимальны (это по сути то, о чем говорит нам и information hiding и подобные концепции).


Относительно God Object — очень просто разбить модули на маленькие, гораздо сложнее разбить их так, что бы количество связей между ними было минимальным. Но анемичные модели (предмет дискуссии статьи) это как раз таки приводит к большей связанности между модулями и понижает кохижен модулей. Что в свою очередь приводит к большой когнетивной нагрузке.

SRP ничего не знает про логическую целостность. Логическая целостность это качество абстракций и инкапсуляция.
SRP ничего не знает про логическую целостность.

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

«а это репозитории, они устроены примерно так-то и так-то, вызываются обычно из таких-то мест»

а это logical cohesion и он является плохой практикой. Модули группируются по функционалу а не по подобию. В случае репозитория у вас в модулях представляющих разные элементы предметной области будет у каждого свой интерфейс репозитория. Реализации оных уже можно сгруппировать в один модуль.


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


Практика показывает что люди очень плохо понимают куда ложить ту или иную програмную сущность.

а это logical cohesion и он является плохой практикой. Модули группируются по функционалу а не по подобию.

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

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

В общем случае функциональность сущностью не является. Как пример можно привести класс String, в котором 3 тысячи строк, но я не помню чтобы кто то на него жаловался. И все точно знают где искать операции для работы со строками. Можно представить и обратную ситуацию, если вынести функциональность в отдельные сервисы, то собрать в голове будет гораздо сложнее.
В общем случае функциональность сущностью не является.

Сущностью в понимании DDD — не является. Сущностью в понимании "надо удерживать в голове" — является (как и метод, скажем).


Как пример можно привести класс String, в котором 3 тысячи строк, но я не помню чтобы кто то на него жаловался.

… как пример highly-cohesive-класса, да?


И все точно знают где искать операции для работы со строками.

Вот, например, операция получения байтового массива в заданной кодировки и наоборот (я про .net сейчас). Или вот, скажем, операции сравнения (в смысле comparison) двух строк с учетом культуры (да и вообще все операции с учетом культуры).


Можно представить и обратную ситуацию, если вынести функциональность в отдельные сервисы, то собрать в голове будет гораздо сложнее.

Можно представить, конечно. Вообще, любую операцию над кодом можно представить с негативным результатом. Но это никак не значит, что SRP противоречит принципу "от 5 до 9 сущностей".

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

Нет, не отрицаю. Как вы сделали такой вывод из моих слов?


Пример с культурой неудачный

Жизненный зато.


когда возникают две взаимодействующих сущности

Ну то есть в реальной программной жизни — в подавляющем большинстве случаев.


Каким образом, по вашему мнению, SRP говорит что не надо разбивать класс string на сервисы?

SRP — примененный разумно — не говорит, что string надо разбивать на сервисы.

Нет, не отрицаю. Как вы сделали такой вывод из моих слов?

абстрагирование и инкапсуляция позволили снизить когнитивную нагрузку, это то что нарушает SRP
SRP — примененный разумно — не говорит, что string надо разбивать на сервисы.

скорее разумный выбор не применять SRP. Что для меня показатель слабой состоятельность SRP как принципа проектирования.
абстрагирование и инкапсуляция позволили снизить когнитивную нагрузку, это то что нарушает SRP

Во-первых, я не вижу нарушения SRP. Во-вторых, я не вижу, каким образом то, что я сказал, отрицает преимущества ООП.


скорее разумный выбор не применять SRP

Почему же? В string SRP применен, string не отвечает за операции, не имеющие отншения к строкам.


Что для меня показатель слабой состоятельность SRP как принципа проектирования.

все принципы проектирования опираются на разумное применение.

В string SRP применен, string не отвечает за операции, не имеющие отншения к строкам.

вы противоречите SRP
вы противоречите SRP

Нет. Я, возможно, противоречу вашему пониманию SRP, но почему это должно меня волновать?

13к комментариев говорят об обратном.
Изменение алгоритма является основанием для изменения класса?
Изменение алгоритма является основанием для изменения класса?

Того, в котором реализован этот алгоритм — да.

А если в классе два разных алгоритма, то это две причины для изменения?
Ваш SRP это топор из сказки каша из топора. Берем СРП много здравого смысла и опыта и получаем результат. Только СРП в данном случае лишний компонент. Вам он не нужен, тому кому он нужен он не поможет.
Берем СРП много здравого смысла и опыта и получаем результат.

С программированием вообще так, вы не поверите. Без опыта и здравого смысла не работает.


Вам он не нужен

Может, я сам разберусь, что мне нужно, а что — нет?

Может, я сам разберусь, что мне нужно, а что — нет?

Вы спрашиваете у меня разрешения?

Нет, я намекаю вам, что судить, кому что нужно — опасно.

нет, все еще одна — вам нужно как-то по другому со строками работать.


То есть условная роль, которая требует изменений, все еще одна.


p.s. если для вас сложны роли и зоны ответственности и все это такой ужас вызывает — не смотрите в сторону DDD.

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

… у которого в рассуждениях есть как минимум одна фактическая ошибка (он путает dependency inversion с dependency injection).

Ну Дэн вообще клевый чувак, и он в этом докладе неплохо так набрасывает. И целью ставит не отказ от SOLID, а что бы слушатель начал задавать правильные вопросы.


По сути, можно опровергнуть все его аргументы (в особенности про LSP). Однако как по мне — он заменяет абстрактные термины (вроде единой ответственности) еще более абстрактным "пиши хороший код".


Что до "знаний будущего" — SOLID в книге Боба идет уже после рефакторинга, так что… я думаю ответ тут довольно простой.

Ну Дэн вообще клевый чувак, и он в этом докладе неплохо так набрасывает.

Я просто не люблю подобные набросы. Утомили.


Однако как по мне — он заменяет абстрактные термины (вроде единой ответственности) еще более абстрактным "пиши хороший код".

Яп-яп.

еще более абстрактным «пиши хороший код»

в отношении SRP он был достаточно конкретен «can easily do several related things»
По сути, можно опровергнуть все его аргументы

Этож не тесты на быстродействие, опровержение будет очень субъективным.
в отношении SRP он был достаточно конкретен «can easily do several related things»

а как определить что они related? Вон у людей так logical cohesion выходит и для них они related.


Этож не тесты на быстродействие, опровержение будет очень субъективным.

Так он ничего более объективного не предлагает.


Словом, дискуссия всеравно зайдет в тупик. Можете на GRASP посмотреть, они чуть менее расплывчато сформулированы.

все очень субъективно, если автор считает, что related, значит related. Если при эксплуатации дизайна выяснится, что это неудобно, значит надо будет разнести. То, что Дэн не предлагает другого формального подхода еще не делает SRP правильным. Наоборот, нет никакого формального подхода. Хорошая модель должна помещаться в голове, но SRP об этом ничего не знает. На практике я вижу, что с одной стороны тестопригодность растет и размер класса уменьшается, но с другой увеличивается количество синтетических абстракций и нарушается инкапсуляция, что не идет на пользу качеству модели.
что это неудобно, значит надо будет разнести.

но это еще более субъективно. Я знаю разработчиков которым удобно глобальный стэйт и процедуры. Просто потому что они так привыкли за 10-15 лет.


Хорошая модель должна помещаться в голове

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


и нарушается инкапсуляция

Каким образом она нарушается? Что до синтетических абстракций — pure fabrication то есть, вы же их вводите для того что бы было удобнее, а не просто так.


p.s. я уважаю мнение Дэна, но как по мне конкретно этот наброс так себе. В набросе про dependencies as a code smell намного больше пользы.

но это еще более субъективно.

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

Это то что я вижу регулярно. В идеале, человек имеет в голове стройную модель, то что в XP называют «метафора» и все синтетические абстракции в рамках этой метафоры уже не совсем синтетические. А на практике просто набор сервисов, провайдеров и контроллеров вокруг структур данных с нарушением этой самой инкапсуляции в полный рост, потому что SRP.
Если мотивирован, то ожидайте существенного снижения его производительности.

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


Словом, маленькие дела и все такое. Тут приучили VO использовать, там, агрегаты попробовали вместе построить, и вот уже через какое-то время все уже не так плохо. Ну а то что модель у нас не очень чистая в процессе — она никогда очень чистой не будет, нам не это нужно.


потому что SRP.

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

Уровень этих концепций несопоставим.

Bounded Context как по мне про SRP (просто на более высоком уровне модулей). Вы же надеюсь понимаете что эти принципы работают не только для методов классов.


То есть SOLID в среднем проще DDD.

это то что нарушает SRP

SRP про кохижен. OCP про каплинг и инкапсуляцию. Вы должны учитывать оба принципа.

кохижен и каплинг это метрики кода. инкапсуляция это принцип проектирования. srp это тоже метрика, но ни как не принцип.
srp это тоже метрика, но ни как не принцип.

Определение с вами не согласно.

Определение писал дядя Боб, я считаю его популистом. Поэтому мне не столь важно что он написал в определении, как то что он написал в обоснование своей теории

Вот я и говорю: вас не устраивает ваше же понимание SRP, о котором вы выносите какие-то суждения. Ну да, сколько угодно, но к SRP, который входит в SOLID, и который люди применяют на практике, это никакого отношения не имеет.

важный момент, который хотел подчеркнуть Боб, это то, что вам важно анализировать поток изменений требований, разбираться "почему вдруг понадобилось это менять". Кому и т.д. И учитывать эту информацию что бы делать "гибкой" ту часть системы которая более подвержена изменениям.


Иначе, в отрыве от этого, мы будем вынуждены добавить те самые лишние слои абстракции, что далеко не всегда является благом.

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

кохижен и каплинг это метрики кода

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


Ну и опять же, есть скажем принцип "у модуля кохижен должен быть высоким а каплинг низким". Можно учитывать только этот принцип, а можно применять SOLID. Результат будет приблизительно таким же.

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

Да? Покажете, пожалуйста, алгоритм расчета cohesion на уровне класса.

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

lac of cohesion все же немного "не то".

LCOM это уж слишком примитивно. Я бы рассматривал взвешенные связи, например распределяя вес по всему AST по типу как гугл распределял PR. Ну и по остальным моментам тоже самое. Но «очень легко» тут не будет, а потянет на солидную работу. Которая на выходе все равно сделает нейронку генерящую порно.
Именно что легко, количество непересекающихся в обработке, наборов данных класса.
количество непересекающихся в обработке, наборов данных класса.

Что такое "непересекающиеся в обработке наборы данных"?

Это когда часть методов оперируют на данных A и B, а другая часть на данных C и D.

Каков cohesion у статических методов класса string? А у Enumerable.*?

Понятия не имею, мне достаточно того что в методе стринг достаточная инкапсуляция и как абстракция он тоже оправдан. Enumerable вообще про другое.
Понятия не имею

Ну то есть на двух тривиальных примерах ваш алгоритм расчета метрики дает результат "понятия не имею".

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

Ну то есть ровно то же самое, в чем вы критикуете SRP.

Верно. Некоторое преимущество только в том, что она чуть более формальная и никто не пытается с ее помощью проектировать ООП софт.
никто не пытается с ее помощью проектировать ООП софт

Если вы не пытаетесь, то это еще не значит, что никто не пытается.

то возникает вопрос в какой из них должна располагаться логика взаимодействия.

Да, и на этот вопрос нужно дать ответ. Для того что бы примерно прикинуть правильно ли вы дали ответ и придумали целую кучу принципов (SRP, high cohesion, low coupling, information hiding, protected variations)


Каким образом, по вашему мнению, SRP говорит что не надо разбивать класс string на сервисы?

у этого класса одна единственная причина для изменений. Так же мы можем сказать что этот класс обладает такой характеристикой как communicational cohesion, что в целом вполне удовлетворительно.

Да, и на этот вопрос нужно дать ответ.

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

Это какая?
но SRP дает неправильный ответ.

SRP дает вопрос, а не ответ. А уже ответ на него вы предоставляете. Так что не стоит обвинять в ваших решениях принцип, который предлагает вам лишь способ как проверить правильность вашего ответа.


Это какая?

Работа со строками, разумеется. Вполне себе единственная область ответственности.


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


SRP намного более сложный принцип нежели "у всех классов должен быть один метод".

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

возьмем простое определение "SRP это когда у сущностей одна причина для изменения". Все довольно просто, проблема только со словами "причина" и "изменения". Причина обычно роль, а значит мы должны уметь ее определять. Изменения — это только то изменение в поведении которое эта роль может захотеть. И должна быть только одна роль, которая хочет что-то менять в сущности.


То что принцип "сложный" не значит что с ним что-то не так. Скажем LSP по каким-то причинам считают сложным принципом, хотя из всех что есть в SOLID это пожалуй самый простой.


Преобразование строки с учетом культуры это тоже работа со строками?

нет, поскольку тут важным становится знание о культуре. То есть мы должны делигировать эту ответственность кому-то кто знает о культурных отличиях. Если при работе со сроками единственная роль, которая бы хотела что-то менять это разработчик, во втором варианте у нас уже осуществляется эффект на конечного потребителя, и появляется новая роль которая может захотеть поменять формат под себя. Причем для каждой культуры будет своя роль, а значит правила мы так же должны сгруппировать каждый в своем маленьком модуле. Эти модули затем мы можем сгруппировать в один (i18n какой), так как вся совокупность ролей, меняющих содержимое модуля делает это по одной причине.

Да что вы докопались до этих строк?
Это библиотека. Библиотека для использования широким кругом лиц. Библиотека большая, разноплановая.
Здесь явно напрашивается отделение внутренней архитектуры от внешнего интерфейса.
Использовать строки удобнее когда все методы в одном классе. А вот писать и поддерживать — когда оно разделено на слабосвязанные сущности.
Соответственно пишем код «как должно быть», а потом закрываем это тонким фасадом где каждый метод буквально состоит из одного вызова реального класса.
Ну а внутренняя архитектура?
В первой итерации я бы набросал пачку интерфейсов. Интерфейс сравнений, интерфейс поиска, интерфейс манипуляций, интерфейс расчлененки и т.п.
Сделал бы несколько базовых классов строк (явно напрашиваются раздельно односимвольные строки и многосимвольные, плюс в зависимости от общей архитектуры возможно еще два разделения — с длиной в начале или с нулевым байтом в конце), расширил бы их врапером который реализует все эти интерфейсы.
Ну и отдельно уже реализацию под все это.
Архитектуру реализации уже строим под реализацию. В первом приближении бьем по классическому вопросу «что будет меняться вместе?».
Потом набутстрапить соответствующие классы и частично их внутренние методы, когда структура уже «на бумаге», то отрефакторить первоначальное разбиение, за ним отрефакторить изначальную структуру интерфейсов. На этом этапе мы уже довольно четко видим возможные различия в функционале разных реализаций, так что можем выстроить структуру интерфейсов которые будут использоваться потом в клиентском коде. (Именно на этом этапе, поскольку напрашивается какой-то условный IString, но в процессе может оказаться что мы решим что-то делать только для однобайтовых строк, или к примеру мы окажемся настолько ленивы что для строк с нулем на конце мы не будем делать методы связанные с длиной, так что лучше такие вещи чуть отложить до тех пор пока не будет скелет готов).
Дальше по мере развития кода мы правим только соответствующие классы. Разбиваем их на слои, или просто разбиваем на более мелкие сущности, объединяем, переписываем, покрываем тестами — все как у людей.
При этом интерфейсы и фасад меняются редко, так что вполне можно сказать что они у нас являются отдельной сущностью, так что и по архитектуре тоже верно что мы его отделили от всего остального.

В общем что сказать то хотел. Фасад отдельно, мухи отдельно.
в реальной жизни получается, что никто в проекте не может запомнить где находится та или иная функциональность.

Вот как раз чаще это получается при нарушении SRP, когда у одного класса много ответственностей и совсем не очевидно где какую искать.

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

нет.

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

Как раз я его трактую буквально.

Что значит "буквально"? Как вы определяете нарушает ли что-то этот принцип? как вы выделяете зоны ответственности?


Скажем если мы обратимся к первоисточнику (книга Agile Software Development: Principles, Patterns, and Practices дяди Боба), там есть следующий абзац:


If, on the other hand, the application is not changing in ways that cause the two responsibilities to
change at different times, there is no need to separate them. Indeed, separating them would smell of
needless complexity.

То есть, для определения зоны ответственности нам уже нужно смотреть на несколько вещей:


  • источник изменений. Для чего они вносятся? С какой целью и кем?
  • Затрагивают ли изменения весь модуль или лишь его части?
  • Как изменения модуля затрагивают клиентский код? (тут еще про ISP нужно помнить)

То есть все уже не так тривиально.

SRP как раз провоцирует на уменьшение количества сущностей, которые нужно держать в голове одновременно. Вот если взять ActiveRecord, который по определению имеет две отвественности — при работе с сущностью (в широком смысле слова) нам всегда нужно помнить, какой метод сохраняет состояние, какой его изменяет, а какой делает и то, и другое. В случае его "конкурента" DataMapper мы не лезем в сущность, когда нас интересует её персистентность, и не лезем в ORM, когда нас интересует бизнес-логика.

вы неправильно считаете сущности, до тех пор как мне не понадобится изучить детали реализации, сущность у меня будет ровно одна, в случае же с маппером появляется дополнительная сущность — маппер.
Если сохранение будет добавляться в разные методы работы с сущностями то тут никакой ОРМ не поможет.

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


Так смысл ORM по паттерну ActiveRecord именно в смешивании бизнес-логики и логики хранения. Он применяется для того, чтобы иметь возможность писать методы типа


class Order {
  public void cancel() {
    this.status = 'canceled';
    this.save();
  }
}
Он применяется для того, чтобы иметь возможность писать методы типа

Ну я бы так не сказал, это совсем необязательно. Создали снаружи и сохраняем снаружи. Можно еще unit of work сделать и сохранять их в одной транзакции.

Необязательно, но чаще применяется именно так. Если использование методов типа save() или isDirty() запрещено внутри бизнес-методов, то это лишь говорит о том, что SRP переносится с уровня объекта на уровень метода. Ну и в целом не очень тогда понятна цель использования именно ActiveRecord, если не рассматривать случай "#жричтодали". Единственное объяснение, приходящее в голову, чисто техническое — методы логики хранения имеют прямой доступ к защищенному состоянию бизнес-объекта, что упрощает код хранения, без вываливания наружу всех кишок. Но опять же на практике с ActiveRecord защищенное состояние редко встречается, максимум приватные свойства сущности оборачиваются в геттеры/сеттеры на уровне базового класса, а то и просто публичные свойства.

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

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

Наличие метода save() говорит только о способности сущности себя сохранять, даже без всякой экзотики популярный MySQL на MyISAM таблицах не поддерживает транзакционность.


В целом же, да, можно придумать ситуацию, где он будет оправдан и бизнес-логика с логикой хранения смешиваться не будет. Вот только на практике сплошь и рядом за вызовом order.cancel() следует вызов order.save(), а потом кто-то это замечает и переносит save в cancel, даже не создав новый метод cancelAndSave.


Антипаттерны не потому плохи обычно, что делают что-то плохо, а потому что легко их использовать неправильно.

order.save() это нормально, ненормально делать это сайдэффектом. Транзакционность можно делать на уровне приложения собрав все сущности с флагом isDirty. Важно именно то что вызов save() осуществляется явно и на уровне приложения.
ActiveRecord удобен тем, что отдельная работа с хранилищем убирается снаружи и делается более явной внутри. Это хорошо заметно со связями. Можно отдельно обратиться к свойству user_id, к объекту User, к запросу на получение User, загрузить его вместе с родительской сущностью по требованию (решение проблемы N+1). Я с Доктриной мало работал, но кажется там для этого используются какие-то магические прокси, которые подменяют что-то на лету.

Прокси там для опциональной ленивой загрузки.

Совершенно верно. Главный императив разработки ПО — управление сложностью. А кто не согласен, тот льстит себе, и думает что у него неограниченный размер черепа (если немного перефразировать известное выражение Дейкстры). Вот главный фундамент качественной архитектуры: число семь.

Комментирование к оригинальной статье закрыто, но это не означает что перед распространением этой статьи не нужно было ознакомиться с комментариями к ней. В этих комментариях ясно и лаконично определено какие именно ошибки в проектировании (да, именно ошибки), привели автора к этой идее. Автор оригинальной статьи не способен смоделировать предметную область и ошибки в этом моделировании валит на RDM. Автор оригинальной статьи явно не знаком с трудами Роберта Мартина в полной мере. Сам Дядя Боб неоднократно высказывался против применения Simple Data Structures как основного элемента программ. Его примеры и обьяснения принципов изобилируют применением инстансов, которые несут операции, а не данные. Да, он сам говорил что для внутренней реализации могут быть использованны данные, но контракт юнита не должен давать к ним прямого доступа.
Далее: автор оригинальной статьи использует ActiveRecord, наследует сущьность и говорит что это из-за RDM он не может легко заменить хранилище на то, которое не поддерживается его базовым классом. Тем самым нарушая рекомендации (см. пункт Persistence) Uncle Bob'а и снова сваливая всю ответственность на RDM.
Тестируемость: Uncle Bob, как большой поклонник и пропагондист TDD, разумеется посвятил этой теме не мало материала, с короым автор оригинальной статьи так же не ознакомился перед её написанием. Тестироваться, прежде всего, должен контракт, а не внутренняя структура (детали реализации) т.к. детали реализации могут меняться, но контракт должен выполняться, о чём как Роберт Мартин так и Мартин Фаулер и Стив Макконелл неоднократно писали. Автор же, нарушая essence тестирования и покрывая функционал тестом "лишь бы тесты были" принципиально не правильно применяет тестирование получая сложность test maintain и, как и везде, обвиняя в этом RDM, а не себя.
Предложеный в статье рефакторинг, который бы привёл к состоянию близкому к ADM так же не является оптимальным. К таким последствиям может привести использование в приложении структуры данных идентичной к той в которой данные хранятся в постоянном хранилище, что не является обязательным. Структура, в которой данные хранятся (напр. таблица и row в ней), не должна влиять на дизайн самого приложения.

Как по мне, то в коментариях к оригинальной статье верно подметили:

1. В примере БМПО связан уровень доступа к данным и уровень бизнес-логики
2. БМПО спроектировано плохо:

«Когда клиент отправляется в магазин, он держит товар/товары и отправляет их в кассу, чтобы купить их.Это не ответственность клиентов, чтобы сказать, может ли он купить свои товары, это ответственность сопутствующего лица в кассе.»

Оригинал:

blog.inf.ed.ac.uk/sapm/2014/02/04/the-anaemic-domain-model-is-no-anti-pattern-its-a-solid-design/#comment-205
Простите, но это полная ерунда. Анимичная модель, это прежде всего модель, которая не управляет парадам-приложением. При анимичных моделях вся логика, как правило аккумулируется в контроллерах,
что приводит к размазыванию, что в свою очередь мешает переносимости-переиспользованию.

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

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

Так же, говоря о моделях, очень важно обозначать архитектуру, так как анимичная модель не может существовать в mvc или mvvm, а другие в mvp. Другими словами, модель обуславливает саму архитектуру и смешивать все в одну кучу не верно.
Логика в контроллерах? Оооок
Молодой человек, конечно, далековат от истины. Но Вы, Naglec, схватили его суждения не за тот хвост). Потому что именно здесь он имеет кое-какие шансы так говорить.

Вообще говоря, существует логика приложения, и бизнес-логика, которая, в свою очередь, разделяется на application-specific(dependent) бизнес-логику и, собственно, предметную бизнес-логику (извиняюсь за тавтологию).

Предметная бизнес-логика — это логика уровня предметных (Domain) моделей. В данной статье предлагается не разделять подвиды бизнес-логики, и все засовывать в сервисы.

А в таком случае, срабатывает принцип, изложенный Мартином Фаулером:

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

The easier question to answer is probably when not to use it. You probably don’t need a Service Layer if your application’s business logic will only have one kind of client say, a user interface and its use case responses don’t involve multiple transactional resources. In this case your Page Controllers can manually control transactions and coordinate whatever response is required, perhaps delegating directly to the Data Source layer. But as soon as you envision a second kind of client, or a second transactional resource in use case responses, it pays to design in a Service Layer from the beginning. («Patterns of Enterprise Application Architecture» Martin Fowler)


Тут, правда, есть нюансы, но я не хочу перегружать этот комментарий, если интересно, то после данной цитаты я их приводил в этой статье по проектированию Сервисного Слоя.
UFO just landed and posted this here
> не нужно искать хэлпер для форматирования какого-то поля объекта

Форматтеры в модели, это как раз пример плохой богатой модели. Из-за таких моделей появляются подобные статьи. Модель богатая, потому что в ней описана вся ее бизнес логика, а не потому что в ней описано вообще все-все, что к ней относится. Форматирование, этой слой UI.

Это зависит, мне кажется. Например, если обращение к пользователю по имени различается в зависимости от каких-то факторов (скажем, до 18 лет — ФИ, 18+ — ФИО) — это может и с натяжкой, но бизнес-логика.


И мне лично в таких случаях было бы комфортнее писать person.Name.OurSpeciallyFormattedName, а не new PersonNameFormatter().FormatOurSpeciallyFormattedName(person).
Сущность Person это не особо забивает, потому что из нее торчит не простыня форматов имени на все случаи жизни, а только что-то вроде public PersonName Name => new PersonName(this), и именно этот PersonName инкапсулирует всю логику, связанную с разными форматами имени. Вроде как такой торчащий из сущности PersonName — это пример Value Object, хотя тут я могу заблуждаться.


На истину не претендую; если такой подход чем-то плох, рад был бы услышать критику.

UFO just landed and posted this here

Закон Деметры. Статья 1, п. 1.

С большой натяжкой. И, главное, это провоцирует добавление этой самой простыни для всех случаев жизни. Вы сами может и не добавите, а кто-то другой, увидев, что вы добавили OurSpeciallyFormattedName, добавит для другого случая, например для отчётов в налоговую FiscalFormattedName, где ФИО надо выводить полностью. А потом пенсионный попросит не ФИО, а ФИ.О… И это в рамках одной юрисдикции, при неизменяющихся требованиях собственно бизнеса.


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

Для описанных случаев я бы ни VO ни хелпер не использховал.

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

Давайте разделим простые примеры и более сложные.

За простой пример давайте возьмем ситуацию, когда Person у нас не интернализируется, в отчетах не учавствует и собственоо правила форматирования имени у него всегда одни. Для такого случая, я думаю применение обычного свойства было бы уже достаточно:
public class Person
{
    private string firstName;
    private string lastName;
   
    public Person(string first, string last)
    {
        firstName = first;
        lastName = last;
    }

    public string Name => $"{firstName} {lastName}";   
}

Другое дело, если, к примеру, нам нужно будет показывать имя Person на сайте и в отчете, применяя разные методы форматирования.
Тогда можно обратиться к адаптерам/моделям отображения или т.п., но никак не к VO и не добавлять логику для отображения и форматирования для каждого места использования в сущность. Получится что-то наподобие:
public class Person
{
    public string FirstName 
    { get; set; }
    public string LastName 
    { get; set; }
   
    public Person(string first, string last)
    {
        FirstName = first;
        LastName = last;
    }
}

public class WebPerson
{
    private Person person;
   
    public Person(Person personEntity)
    {
        person = personEntity;
    }

    public string Name => $"{personEntity.FirstName} {personEntity.LastName}";   
}

public class ReportPerson
{
    private Person person;
   
    public Person(Person personEntity)
    {
        person = personEntity;
    }

    public string Name => $"First Name: {personEntity.FirstName} | Last Name: {personEntity.LastName}";     
}


А если добавится интернализация, что-то наподобие:

public class InternationalPerson
{
    private Person person;
   
    public Person(Person personEntity)
    {
        person = personEntity;
    }

    public string Name => PersonNameFormatter.format(person);     
}


Не подумайте, что вышеприведенные примеры являются однозначно правильным путем для реализации. Это просто псевдо-С# код который показывает общую идею.

Я хочу сказать, что операции над данными сущности не обязательно должны быть в самом классе сущности но это и не значит, что их все надо выносить в сервисные классы.
Как уже сказал VolCh, сущности не должны отвечать за свое отображение.
В сущности должен находиться код, который работает с данными сущности в рамках бизнес логики.
К примеру, подобные методы вполне могут быть в сущности или VO сущности:
person.isActive();
person.Status.IsActive();
person.Activate();
person.IsAdult()


Надеюсь мой посыл понятен. Если нет, я приведу примеры получше.

Выделить Name в value object свойство объекта Person с одним стандартным преобразование в строку и, возможно, методом типа format(string format) в целом хорошая идея, по-моему, как по смыслу предметной области "физическое лицо", так и по частым юзкейсам в различных системах. Случаев когда нам нужно только имя или только фамилия, не для целей презентации или поиска значительно меньше, чем случаев, когда мы оперируем им как одним целым и сравниваем разные имена по полному совпадению имени. Банальные даты и то чаще нужно по частям анализировать, чем имена, а представление их как единого целого (VO с возможностью получить день, месяц или год или просто строка единого формата) можно сказать стандарт де-факто.

согласен.

Все действительно упирается в варианты использования и то, что хорошо в одном случае, может быть совершенно не применимо в другом.

Насчет Name как value object с одним стандартным преобразование в строку, да это хороший вариант, для некоторых случаев избыточен но если говорить обобщенно, то я был бы рад иметь Name, Date, Status и т.п. как VO по умолчанию.

Огорчает только то, что далеко не все фраемворки (в частности ORM) дают возможности для удобной работы с VO и далеко не во всех ОО языках VO распространены и нормально поддерживатся.
Либо я невнимательно читал, либо автор подобрал неудачные примеры. В первый раз вижу, чтобы в классах-сущностях находилась какая-то бизнес-логика.
Классы сущностей — одно из двух основных мест для размещения бизнес-логики. Второе — сервисы (доменные). Грубое правило: то, что изменяет одну сущность, в неё и помещается, что несколько — в сервис. Больше особо и помещать некуда, остальное, по идее, или инфраструктура, или логика приложения.
Если часть логики поместить в сущности, а часть в сервисы, можно запутаться. Удобнее когда вся логика одного уровня находится в одном месте — в сервисах. В сущностях видел только что-то очень простое, типа валидации. Сам пишу на java, но сомневаюсь, что в других ОО языках это будет сильно отличаться.
Похоже, вы тоже используете подход с анемичной моделью, особо не задумываясь об этом. Удивлены, что это, оказывается, «антипаттерн»?
Удивлён. А ещё удивлён, что здесь вообще используются понятия «паттерн» и «антипаттерн». В моём понимании паттерн — готовое решение для какой-то проблемы, которое можно вписать в любую архитектуру, решение более высокого уровня. Так что впору вводить понятие «антиархитектура»)

Паттерн — хорошее готовое решение, антипаттерн — плохое :)

Такого не бывает. GOTO и глобальные переменные это чаще всего плохо, но изредка это хорошо. Всё это хорошо/плохо — исключительно вопрос зрелости.


Незрелый разработчик причинит меньший ущерб, если не воспользуется GOTO и глобальными переменными там, где их применение было бы вполне уместно, просто потому, что не использует их нигде — патамучта "антипаттерн" или какое-то другое страшное слово.


Но это не делает их действительно антипаттерном или плохим решением в принципе, в отрыве от конкретной ситуации.

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

Если часть логики поместить в сущности, а часть в сервисы, можно запутаться

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


Вся проблема с тем, насколько плохо люди анализируют два параметра модулей при проектировании: cohesion и coupling. Для анемичной модели, сервисов менеджеров и т.д. есть свое определение силы cohesion и влияние на coupling. И размещение логики в сущностях/объектах значениях как раз таки один из способов снизить связанность и повысить кохижен модуля.

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

Наружу нужно выносить логику взаимодействия объектов, не укладывающихся в композицию, в has-a. За своё собственное состояние и состояние своих "детей" объект должен отвечать сам. Сложные взаимосвязи изменений можно реализовать разными способами, простыми сценарными скриптами, например, или доменными событиями, обработчики которых преобразуют события в последовательность вызовов разных методов разных объектов модели.

Если граф обьекта сложный, то понятие «детей» усложняется. Доведу пример с детьми до крайности. Если ребёнок вдруг закашлял, что с этим делать решат мама, папа, бабушка, дедушка, воспитательница детсада — в зависимости в какой иерархии (контексте) он находится. Принятие решения также зависит от времени дня, окружения (один или вокруг другие дети) и т.д. Поэтому в реальных системах подобные решения выводятся в сторонние сервисы. А им для принятия решения нужны только данные.
В первом приближении моё правило звучит так: чем сложнее иерархия или структура обьекта, тем меньше логики на его нижних уровнях.

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

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

Речь не о ребёнке. Если надо имплементировать что-то подобное. Где должен находиться метод, выдающий решение, что с ним (ребёнком) в данной ситуации делать?
Догматически — в классах мамы, папы, соседа, прохожего или в их общем super (в терминологии Java).
Прагматически — во внешнем сервисе.
Но это моё личное мнение. Я Вам его не навязываю.

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

Хочется верить, что решения мамы и папы будут одинаковы или не очень зависить от их собственного состояния.
Ну а если перестать терзать ребёнка с этой аналогией и несколько абстрагироваться. Что мы имеем:
1. Имеется сложный граф обьектов, узлы которого как-то можно разбить на уровни.
2. В этом графе один узел должен изменить состояние другого узла с помошью его публичных методоа (в терминах Java)
3. Для проведения изменения узлу требуется информация об изменяемом обьекте, собственном состоянии и состоянии некоторых других узлов.
Вопрос: должна ли вся логика определения, как изменить узел (в нашем примере — ребёнка) быть сосредоточена в изменяющем узле.
Ваша позиция — должна всегда.
Моя позиция — не всегда. Чем больше информации от других узлов требуется, тем полезнее выносить её за пределы изменяющего узла.
Я правильно сформулировал суть различия наших мнений?

Спорная позиция про одинаковость решений мамы и папы с точки зрения подготовки человека к самостоятельной жизни, но давайте о программировании :)


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


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


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

Мое ИМХО основанное на опыте. Анимичная модель отлично справляется там, где архитектура подвержена изменениям. Можно иметь несколько вариантов сервисов, причем переключаться между ними несколько раз на дню. Если же приложение — монолит, где происходит в основном фикс багов, то да удобнее держать логику и данные в одном месте.
там, где архитектура подвержена изменениям

Замечательное замечание! Именно поэтому я и говорю, что за анемичной моделью будущее — по крайней мере, в web-приложениях, где я обитаю. Там изменения всё время как из рога изобилия.

Как по мне, то анемичная модель или богатая — это часть архитектуры, причём важная часть. А если говорить о бизнес-логике, то как бы и не самая важная.
Соглашусь. Я бы еще дополнил таким соображением: если «приложение» — не самодостаточная система, а некая библиотека, решающая прикладную задачу с более менее четко очерченными границами (например, работа с XLS-файлами), то для нее использовать богатую модель с продуманной разветвленной иерархией классов является вполне оправданным. Благодаря тому, что границы намечены заранее, вполне реально запроектировать систему достаточно гибкой, чтобы адаптироваться под новые требования, при этом возникновение таких требований, которые поломают всю структуру, довольно маловероятно.
На противоположном полюсе я бы поставил энтерпрайз системы, особенно «полукоробочного» плана, когда компании продается готовый продукт, подвергаемый серьезной модификации под нужды конкретного заказчика. Заранее предусмотреть все требования даже одного заказчика — архисложная задача. А уж сделать так, чтобы будущие требования будущих клиентов без проблем укладывались в существующий продукт (который бы при этом не был «голым» фреймворком) — еще сложнее. Рано или поздно обязательно окажется, что допущения, справедливые для 10 клиентов, не соответствуют бизнес-модели 11-го. Для таких продуктов, как мне кажется, анемичная модель подходит весьма хорошо. В коробочном продукте реализуется базовая логика, отвечающая потребностям большинства заказчиков, а там, где логику надо переопределить, сделать это легко — достаточно подменить соответствующую службу, никак не нарушая работы остального кода.

Для других приложений может быть найдено место на этой оси где-то между двумя полюсами. Они оба имеют право на существование. Но когда один называют «паттерном» (_«Так делать правильно!»_), а второй — «анти-паттерном» (_«Дети, не делайте так!»_), то, ИМХО, второй подход незаслуженно маргинализируется.

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

Мне вот каждый раз любопытно: а чем же, концептуально, подход "анемичная модель + сервисы" отличается от, собственно, функционального программирования (ну, за исключением мутабельности)?

Концептуально — наличием переменных, операции присваивания, императивностью. Короче, всем. А еще в функциональных языках есть другие характерные плюшки: абстрактные типы данных, выведение типов компилятором, паттерн матчинг и пр. Не говоря уж о стремлении в них к четкому разделению на чистые и нечистые функции (и минимизации числа/размера последних).


(Не вам) Так что не надо анемичные модели оправдывать крутостью ФП. Анемичные модели — это неправильное использование ООП, и не более того.

А у вас в веб-приложения прям таки «бохатая модель» везде? Я хотел бы на это посмотреть.
У меня лично (может я и горе-проектировщик, конечно) в контексте больших постоянно развивающихся ASP.NET MVC приложений нормально уживаются именно сервисы с логикой + анемичная модель.
В небольших приложениях (особенно НЕ веб) с ограниченной, заранее ясной бизнес-логикой хорошо взлетает «бохатая модель»/DDD/SOLID/ООП и прочие радости.

Научите меня как строить крупные ынтырпрайз веб приложения с постоянно меняющейся бизнес-логикой на основе Rich Data (Domain?) Model. Книжки, статьи, примеры. Спасибо.

Честно говоря, не очень понял запрос. Что такое веб- и не-веб приложения? Система управления предприятием с веб-интерфейсом это веб-приложение? А простой (или сложный) интернет-магазин? Поэтому далее будет моя собственная классификация приложений и их особенностей.


Для простого бложика сойдет любая парадигма, потому что мало кода можно распределить как угодно. Active Record, только end-to-end тесты.


Для небольшой системы с бизнес-логикой DDD уже сработает лучше. Репозитории, объекты, хранящие внутри свое состояние, не раскрывающие его наружу и зависящие только от поведения других объектов — наше все. А так же сервисы уровня домена, если нужно работать с несколькими сущностями, и уровня приложения, когда нужно задействовать инфраструктуру.


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


Короче, выделяйте то, что меняется, и держите вместе то, что меняется вместе.


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


И научитесь как бы находиться в двух режимах — программирование на уровне объектов и программирование самих объектов.

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

Т.е. вот у нас есть Order. Бизнес начинает придумывать разные хитрые правила изменения состояния заказа в зависимости от погоды, дня недели и цвета чулок Марьиванны. Куда мы пихнем эти правила? Куда-то наверх. При этом модель будет анемичной, IOrderProcessingService будет являться фасадом, а внутрях у ней неонка с фабриками, стратегиями, репозиториями марьиванн, хитрыми пайплайнами и прочими классными шаблонами.

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

Правила (условия) изменения состояния сущностей можно хранить в спецификациях, проверять их в сервисах, а собственно изменения производить в сущностях, вызывая их методы (не тупо сеттеры, а с семантикой бизнес-процессов, не order.setStatus(STATUS_CANCEL), а order.cancel()) из этих сервисов, при условии соответствия сущности спецификации.


В веб-приложения событийная модель для предметной области вполне встраивается, даже в таких пайплайновых серверных языках как PHP, работающих в режиме CGI, не говоря о возможностях пробрасывать события на клиента через WebSockets.

Правила (условия) изменения состояния сущностей можно хранить в спецификациях, проверять их в сервисах, а собственно изменения производить в сущностях, вызывая их методы (не тупо сеттеры, а с семантикой бизнес-процессов, не order.setStatus(STATUS_CANCEL), а order.cancel()) из этих сервисов, при условии соответствия сущности спецификации.


С этим подходом согласен
Концептуально — наличием переменных, операции присваивания, императивностью.

Это как раз не "концептуально", потому что на уровне "объекты и сервисы" всего этого нет.

Как это нет? А с чем тогда работают сервисы, как не с переменными?


Анемичная модель — это процедуры и переменные-структуры. А вот в ФП вообще нет доступного программисту напрямую состояния.

Я не очень согласен в вашим определением «сервисов + анемичной модели».
Мне это все видится скорее как:
Сервисы (фасады) -> некоторая объектная модель с логикой ->
анемичная модель (сущности) как способ хранить состояние. Возможно, с примитивной логикой в рамках одной сущности

Это то, как лично у меня получается жить в современном ынтырпрайзе с ORM/DDD и прочими штуками. Да, это не кошерное ООП, но оно работает в вебе. There is no silver bullet, все дела.
Я не очень согласен в вашим определением

Это не мое определение. Принято считать, что анемичная модель — это объекты без или почти без логики, а вся логика в процедурных сервисах.

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

С пришедшими данными.


Анемичная модель — это процедуры и переменные-структуры.

Совершенно не обязательно. Анемичная модель — это модель, лишенная логики. Ничто не мешает ей быть immutable.

Я имею ввиду, что анемичные модели — это переменные. Сервисы — процедуры, которые работают с этими переменными. Это ни разу не ФП.

Я имею ввиду, что анемичные модели — это переменные.

Модель — не переменная. Модель — структура данных.

Как сервисы напишите, так они работать и будут. Могут принимать иммутабельные структуры в качестве параметров и возвращать другие иммутабельные структуры в качестве результатов.

Мои Best Practicles

1) Объекты только в виде POJO, никакой логики внутри. Ибо если внутрь класса класть бизнес-логику, он быстро превращается в god object, внутри появляется очень много зависимостей и зависимостей от вызовов методов. Плюс нарушается принцип инкапсуляции — зона ответственности весь класс и все потомки, нет четких границ. В случае же процедурного-подобного подхода зона ответственности четко ограничена, и четко видны вход и выход.
Плюс объекты с бизнес-логикой внутри сложно тестировать, ибо оно зависит от внутренних состояний

2) Бизнес-логика в виде процедурно-подобных методов в классах-сервисах. Легко тестировать, легко расширять, легко видна зона ответственности, легко делится на слои, легко разделяется на разные классы

3) Наследование только в виде расширения POJO объектов, в соответствии с первым пунктом
4) Делим приложение на слои, минимальное количество: слой записи, слой чистой бизнес-логики и слой взаимодействия с внешним миром

5) Юнит-тестирование только в случае непонятного или страшного кода, в котором я не уверен. Гнаться за 100% покрытием — вредно, ибо увеличивает кодовую базу и затрудняет рефакторинг и правки.
Вместо юнит-тестов по максимуму интеграционные тесты, прогоняющие реальные сценарии работы с начала и до конца
С пунктами 1-4 почти полностью согласен, а вот с пунктом 5 нет. По следующим причинам:
1. В прогонных тестах трудно создать условия, которые тестируют важные аспекты поведения. Например, что система правильно себя поведёт в определённой ситуации.
2. Если прогонно-интеграционный тест перестает работать, бывает трудно понять почему именно. Ошибка может показать себя позже появления или вообще в конце.
3. Когда система только создаётся, интеграционные тесты трудно писать. И встаёт дилема: либо пишем костыли и потом их меняем на приличный тестовый код, либо тестируем всё уже в самом конце,
Ибо если внутрь класса класть бизнес-логику, он быстро превращается в god object

это если мы не контролируем процесс и не прибегаем к тем же SOLID и рефакторингу.


Плюс нарушается принцип инкапсуляции — зона ответственности весь класс и все потомки, нет четких границ.

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


Как по мне основная проблема в том, что часто при проектировании сущности люди руководствуются структурами данных а уже потом поведением. И вдруг оказывается что каждый метод объекта использует лишь часть полей и половина из них вообще никак не пересекаются. А еще чаще мы вообще имеем дело со случайным формированием модулей.


Плюс объекты с бизнес-логикой внутри сложно тестировать, ибо оно зависит от внутренних состояний

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


Легко тестировать, легко расширять, легко видна зона ответственности

легко приводит к высокой связанности и действию на расстоянии. Ну и по поводу "легко тестировать" стоит уточнить что мы закончим с большим набором интеграционных тестов. То есть тестировать такую логику в изоляции весьма проблематично. Да и как правило такой подход приводит к большему количеству зависимостей и меньшей выразительности логики.


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


Наследование только в виде расширения POJO объектов

В моем варианте это "наследование как костыль" (я про extends классов). Стараюсь обходиться интерфейсами. Шарить часть данных в наследниках как-то не часто доводится, protected практически не нужны (только как костыли потому что лень думать как разделить лучше).


Делим приложение на слои

Рекомендую к просмотру: Microservices and Rules Engines – a blast from the past — Udi Dahan. Добавил метку на момент про слои. Мне кажется Уди лучше объяснит что не так со слоями.


Гнаться за 100% покрытием — вредно, ибо увеличивает кодовую базу и затрудняет рефакторинг и правки.

тут два момента:


  1. 100% покрытие кода тестами не нужно, потому что оно не говорит нам о качестве тестов (мутационные тесты могут показать реально покрытие логики, но их удобнее использовать для поиска бесполезных юнит тестов, что часто случается у новичков) и насколько хорошо эти тесты все покрывают. Это инструмент который позволяет быстро найти участки кода, которые вообще не тестированы, но тот факт что они не протестированы не означает что они должны.


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

Вместо юнит-тестов по максимуму интеграционные тесты, прогоняющие реальные сценарии работы с начала и до конца

J.B. Rainsberger — Integrated Tests Are A Scam

У вас аргументы в стиле «надо делать хорошо, и тогда проблем не будет». Глупость.
Методы класса зависят от состояния класса — факт. Повреждение состояний вызовет повреждение метода.
И уж тем более завязываться на внутреннее состояние. Давайте уж сразу глобальными переменными хуячить.
Микросервисы — зло.

Тесты это в первую очередь клиентский код

Нет. Юнит-тесты это закрепление текущей реализации кода. Белый ящик, дублирование логики.
Интеграционные тесты — действия пользователей, внешнее воздействие
Методы класса зависят от состояния класса — факт.

факт если они зависят от состояния класса. Я дал вам ссылку с разбором всех 4-х вариантов и о нюансах и о том какую сложность несут те или иные варианты.


И уж тем более завязываться на внутреннее состояние. Давайте уж сразу глобальными переменными хуячить.

Простите, вы точно понимаете что говорите? если ваша логика засивит от состояния вы так и так от нее зависите. В контексте интеграционных тестов и black-box тестирования у вас все еще есть фикстуры, то есть какой-то стэйт системы. И вы от него зависите.


Микросервисы — зло.

Инженер не должен мыслить категориями зло или добро, плохо или хорошо. Да и потом, я дал вам ссылку на фрагмент который про связанность системы в контексте слоев и о том, как именно слои влияют на связанность.


Нет. Юнит-тесты это закрепление текущей реализации кода. Белый ящик, дублирование логики.

Это вы сейчас может свои тесты описали, но вообще это немного не так. Опять же ссылку я дал.

Интеграционные тесты — действия пользователей, внешнее воздействие

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

Методы класса зависят от состояния класса — факт. Повреждение состояний вызовет повреждение метода.

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


И уж тем более завязываться на внутреннее состояние. Давайте уж сразу глобальными переменными хуячить.

Внутренне состояние объекта, защищенное от изменения снаружи (неважно модификаторами доступа, замыканиями, документацией или соглашениями) как раз способ максимально удалиться от глобальных переменных, не уходя в ФП, где переменных либо нет как сущности, либо они инкапсулированы на уровне локальных переменных функции. Когда ни один клиент объекта даже не знает о существовании внутреннего состояния, он и изменить его не сможет.

Как-то странно передавать Item в Customer, чтобы создать Order, не находите? Если нам нужен заказ, то его и надо создавать:


class Order extends Entity {

    cost : number

    constructor(
        protected customer : ICustomer ,
        protected positions : { product : IProduct , count : number }[] ,
        protected shipper : IShipper ,
    ) {

        this.cost = this.positions.reduce(

            ( cost , pos )=> price + pos.product.price * pos.count ,

            // кидает исключение, если не может доставить
            this.shipper.cost( this.positions , this.customer.region ) ,
        )

    }

    purchase() {

        for( let pos of this.positions ) {
            // кидает исключение, если не может зарезервировать
            pos.product.reserve( pos.count , this.customer )
        }

        // кидает исключение, если не может снять денег
        this.customer.charge( this.cost )
    }

}
1) Берем плохую практику. Выбираем синтетический пример в котором она не слишком воняет
2) Берем хорошую практику. Пишем для нее пример который ужасен и ей не соответствует, при этом явно хуже чем пример из п1)
3) Win!

Принцип единственной ответственности не позволяет так отжигать.
У нас есть сущности пользователя, товара, счета покупателя, службы доставки, корзины и допустим сделки.
У клиента есть его география и ссылка на его счет.
При этом у него может быть очень много ЕГО логики.
Регистрация, повышение статуса, добавление товара на продажу (методы товара лежат у товара), валидация полей, поздравление пользователя с ДР… все что угодно.
Но это или чисто его методы или тонкий сахар для доступа к логике связанных данных (например доступ к списку его товаров будет тонким вызовом логики товаров).
У товара есть куча свойств, включая продавца, цвет коробочки, цена (цены, в зависимости от оптового статуса покупателя и объема конкретной покупки) и конечно список доступных методов доставки. Ну и методы работы со всем этим. Например АПИ вычисления того какая же все-таки тут нужна цена для конкретной сделки.
Корзина просто содержит товары и их количество. Хотя тут тоже может быть специфическая логика например дергающая резервирование товара на складе и релиз оного резерва при разрушении корзины по таймауту.
Сделка инкапсулирует в себе данные из корзины (с помощью которой она узнает цены у товаров), доставку (и ее возможность, ведь у нас уже есть и клиент с его адресом, и товар с его адресом и служба доставки с возможными у нее маршрутами) и валидацию доступного баланса у клиента (возможно с резервированием суммы если процесс оформления сделки длительный), методы добавления дополнительной информации и изменения статуса сделки и т.п.
При этом разумеется каждая ответственность у каждой сущности в отдельном классе расширяющем основной (ну или трейты если есть).
В таком виде можно и без сервисов довольно приличную логику пилить. При этом все модели толстые, но вся логика легко отделяется друг от друга, юнит-тесты просты и понятны, допиливать и документировать не сложно.
Необходимо добавить новое поле (или изменить тип данных существующего).
В конструктор класса Order необходимо передать дополнительный параметр.
Довольно необычное обоснование что класс перегружен ответственностью…

SRP ни в коем случае не призывает делать в одном классе ровно один метод.

Если бы базовые библиотеки языка C# были написаны в том духе, в котором предлагает автор, например вместо
                var a = DateTime.Now;
                var b = a.AddDays(1);
                b = a.AddMinutes(1);

нужно было бы писать
                var a = new NowService.GetNowTime();
                var b = new AddDaysService().Calculate(a, 1);
                b = new AddMinutesService().Calculate(a, 1);

то представьте сколько было бы сервисов, и как сложно было бы пользоваться языком. В продакшн проекте методов как правило тоже не мало, поэтому если будет 100-500 сервисов, то кодом тоже будет сложно пользоваться.
Другое дело, что при использовании богатой доменной модели тоже нужно с умом распределять методы по классам, а не как попало. Например, кастомер может по-разному валидироваться в разных ситуациях, а добавление в кастомера методов ValidateForA(), ValidateForB(),… ValidateForF() быстро перегрузит кастомера ответственностью; валидность — это не характеристика кастомера, а то, как к нему относится кто-то вовне.
Ну и не стоит забывать, что даже в классической богатой доменной модели есть место для сервисов (судя по словам Эрика Эванса).

С автором статьи не согласен. За перевод спасибо.

Первый способ подходит для относительно простых случаев. Второй способ более гибок за счет разделения ответственности. И применительно к Entity, он лучше подходит. И вызов методов в первом случае, как раз наоборот, может быть длиннее и сложнее за счет передачи зависимостей через параметры. А в сервис зависимости передаются через DI-контейнер. И проблема с "нарушением" инкапсуляции, которую часто упоминают, очень надумана. Разве это сложно инкапсулировать логику в каком-то одном модуле или классе?!

нужно было бы писать

var a = new NowService.GetNowTime();
var b = new AddDaysService().Calculate(a, 1);
b = new AddMinutesService().Calculate(a, 1);

Не-не-не! Никаких new NowService! Только DI, только хардкор!


public interface IJunkService
{
    DateTime DoJunkAction();
}

public class JunkService : IJunkService
{
    private readonly ITimeProviderService _timeProviderService;
    private readonly ITimeManipulationService _timeManipulationService;

    public JunkService(ITimeProviderService timeProviderService,
                       ITimeManipulationService timeManipulationService)
    {
      _timeProviderService = timeProviderService;
      _timeManipulationService = timeManipulationService;
    }

    public DateTime DoJunkAction()
    {
      var now = _timeProviderService.GetNowTime();
      var b = _timeManipulationService.AddDays(now, b);
      b = _timeManipulationService.AddMinutes(now, b);
      return b;
    }
}

Поначалу, возможно, выглядит абсурдно. Но, если подумать, так можно а) безболезненно обойти проблемы с рассинхронизацией времени на разных серверах / в разных часовых поясах (когда ХЗ что на самом деле вернет DateTime.Now) и б) легко и непринужденно протестировать логику, завязанную на события в будущем или прошлом. ITimeManipulationService, скорее всего, перебор, но если речь будет идти не про время, а про какие-то сущности, которые по-разному могут обрабатываться в разных странах/регионах/компаниях — такой подход оказывается довольно удобным (хотя, конечно, со своими издержками).

UFO just landed and posted this here

Спасибо, и правда интересная статья. Про абстракцию "бесконечный итератор timestamp-ов" я лично никогда не задумывался.

ООП полностью соответствующий SOLID плавно превращается в ФП. так что полное следование этим принципам — тоже не очень, кмк.
Да не превращается он в ФП. На мутабельные состояния объектов в SOLID запретов нет, а значит об основных принципах ФП речи нет.

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


То есть в определённых границах, насыщенная модель сложна, но и в ней есть место декомпозиции. За этими границами это такой же простой объект, как DbContext или Mapper, или что угодно ещё.


Зато, я верю что работу по Анемичным моделям легче спроектировать и делегировать. Моя работа делать ПО в срок с установленным качеством. Если ваша не продавать софт лично, я не понимаю зачем вам это.


Отдельный разговор, то что большинство примеров АМ не показывают соблюдения инвариантов.

Зато, я верю что работу по Анемичным моделям легче спроектировать и делегировать.

Это если работаешь по водопаду или типа того, спроектировали, сделали и забыли, максимум багфиксинг на поддержке.

Я на то же самое намекаю)

Sign up to leave a comment.

Articles