Comments 198
Более того, мне даже кажется, что за анемичной моделью будущее. Все приложения, по большому счету, создаются для обработки данных. И представление разработчика о предметной области (модель) — это всего лишь одно из множества возможных представлений (моделей). Эту мысль замечательно со всех сторон показывает коллега maxstroy в цикле своих статей.
Мы никоим образом не сможем избежать хранения данных, но мы можем избежать связывания данных с правилами их обработки. Ничего не имею против ООП, но я также ничего не имею и против ФП. Просто, чем сложнее проектируемая предметная область, тем выше шанс, что придется один и тот же набор данных рассматривать с точки зрения разных моделей. И чем раньше отделить данные (то, что хранится в базе, грубо говоря) от обработчиков (то, что существует в программном коде), там меньше вероятность применить имеющуюся модель (объект = данные + поведение) в непредназначенных для этой модели (поведения) условиях.
Абсолютно соглашусь, что паттерн и антипаттерн стали обычными разговорными словами у разработчиков но, на сколько мне известно, во многих вузах преподаватели говорят «шаблон», а не «паттерн».
По крайней мере когда я учился то преподаватели еще импользовали слово «шаблон».
Собственно начинающие разработчики скорее всего будут знать «шаблоны», а не «паттерны».
Да и людям, которые слабо знакомы с английским и только начинают познавать азы проектирования, слово «шаблон» будет более знакомо нежели «паттерн»(по крайней мере так было у многих людей с которыми мне доводилось общаться)
P.S. не подумайте что я с Вами спорю — просто хотелось изложить свою точку зрения и наблюдения.
Но во когда смешивают «паттерны» и «шаблоны» в одном контексте — вот это я считаю плохим тоном.
Я предпочитаю использовать или английский термин или перевод, но не все вместе — считаю что от этого получается какая-то каша.
В целом, анемичные классы данных имеют право на жизнь, но, по-моему, по умолчанию надо стараться совмещать данные и логику в одном классе.
/me не понял, почему нельзя классы, обеспечивающие инварианты, сделать как обёртку к "анемичным"? Вроде и волки сыты, и овцы целы, и пастуху вечная память — модель, "богатая" снаружи и "анемичная" внутри.
мне (и не только) кажется тут более будет понятен в контексте ООП такой принцип — чем меньше у метода параметров/зависимостей (в том числе неявных в виде полей и методов классов) — тем лучше
плюс в идеале у метода не должно быть ненужных параметров
пример БМПО в таком случае очевидно хуже так как каждый метод принимающий Item неявно зависит от логики purchase, а в примере АМПО такая зависимость будет явно указана через PurchaseService
Автор статьи забыл сказать с т.з. чего нечто является антипаттерном.
С т.з. ООП — это явный антипаттерн, т.к. ООП означает поведение, когда объекты обмениваются сообщениями, вызывая методы и во главу угла ставится поведение, а данные прячутся.
Анемичность же — явный признак не ООП, а его "соседа" ADT. Читаем для примера http://www.cs.utexas.edu/~wcook/papers/OOPvsADT/CookOOPvsADT90.pdf
Когда данные живут отдельно от поведения — это уже признак разорванности и не-целостности концепций. Т.е. банально "вы не умеете их готовить". Представьте организм, в котором бы кости и мышцы были отдельно, кожа сбоку, а кровеносная система вообще своя у всех. Вот это пример анемичности.
Любая красивая, гармоничная и качественно спроектированная система не даётся с ходу. Это либо опыт проектировщика, либо банально эволюция.
Классическое ООП = «Rich Data Model» = антипаттерн.
Это заявление выглядит как типичное «ваше ООП отстой, а вот ФП — это круто».
Говнокодище пишут и на ООП языках и на ФП — проблема, в основном, в людях, а не в ООП или ФП.
К примеру, люди берут типичный MVC framework для Enterprise системы, засовывают всю бизнес логику в AcctiveRecord, а потом мы видим тысячи гневных отзывов о AcctiveRecord.
Но в то же время тысячи людей используют типичный MVC framework с AcctiveRecord для небольших систем и вполне себе счастливы, а заказчик получает новый фичи по рассписанию.
Так вот, не в AcctiveRecord проблема у первых, а в них самих. Каждый шаблон, методология и язык применимы для решения определенного ряда задач и если попытаться выйти за рамки применения то сразу начнутся проблемы.
Просто с засовыванием бизнес-логики в ActiveRecord ещё можно мириться в достаточно больших приложениях, он для того и создан. Но вот когда в одном методе переплетается и бизнес-логика, и логика хранения (как в примере в статье), вот тут уже тушите свет…
Я имел ввиду, что зачастую ругаются на паттерны, фраемфорки и языки из-за того, что используют их не по назначению или неправильно.
Для примера и привел ActiveRecord т.к. с ним очень часто допускают ошибки и, как Вы и сказали, начинают делать лапшу из бизнес и логики хранения. В итоге получаются классы на 1500 — 4000 тысячи строк где код на все случаи жизни с переплетением бизнес правил и логики ОРМ.
Не приниать на код-ревью классы где в методах больше 20 строк а классы больше 200 строк. Ну не строго конечно, но превышения должны быть единичны и иметь вескую причину.
Когда они начнут их дробить на слои создавая из лапши лазанью наследования, у них хоть чуток начнут шестеренки крутиться на тему структуры. Но даже если лазанья останется, а не появится желание разделять сущности, то все равно с таким макаронным монстром жить будет проще. Разумеется если соблюдены хоть какие-то нормы именования и компановка хоть какую-то логику имеет.
Обычно, увы, ругаются не те, кто их применил, а те, кому это применение досталось в наследство. Непосредственно применяющие обычно просто не замечают проблем или, вполне сознавая их, относят их к техническому долгу. Ведь по сути ActiveRecord считается антипаттерном не потому, что совмещает две отвественности в одном классе, а потому что очень часто эти ответсвенности применяющие паттерн пихают уже в один метод, например, добавляя в мутирующий состояние метод после собственно мутации ещё и вызов save(), чтобы не вызывать его явно каждый раз. И вот, без глубокого вникания в код ты уже и не знаешь, произойдёт ли сохранение после какого-то вызова или нет, где-то происходит, а где-то нет. И это в случае "плоского" объекта уже плохо, а уж если там по цепочке объекты мутируют друг друга...
Действителльно припекает зачастую тем, кто получает в подарок ведро лапши.
Но добавлю, что порой даже те, кто начал варить лапшу, понимают что они наделали, но винят в этом ActiveRecord, framework и т.п. (сам грешен — так делал в прошлом)
А насчет Вашего описания проблемы с использованием ActiveRecord — я об этом и думал, но не расписал более детально как Вы.
Не понимаю, как можно винить паттерн или фреймворк, если ты сам решил его применять, особенно понимая, что применяешь ты его неправильно :)
1. Решение могло быть принято давно и не тобой, а ты теперь разгребаешь последствия нескольких лет его правильного и неправильного применения. При этом вторые обращают на себя куда больше внимания по понятным причинам, из-за чего может казаться, что они составляют 90% общего объема.
2. Решение могло быть принято тобой, но неверно понято рядом последователей. И поскольку некоторые подходы позволяют отстрелить себе ноги легче, чем другие, — есть основания «винить» (ну, точнее, признавать недостатки) фреймворки, которые в большей мере допускают неверное их использование.
представим что есть Junior или Middle разработчик. Ему дают задачу и он ее рализует основываясь на подходах принятых в текущем framework'е. Потом идет следующая задача, за ней следующая и в какой-то момент приложение разрастается, а подходы продолжают применяться как для более простого варианта и со временем все скатывается в помойку.
Разработчик делает вывод, что виноват framework.
В том, что его не предупредили вовремя, что пора менять фреймворк или, хотя бы, подходы?
В идеальном мире мы с легкостью меняем фреймворк, преписываем весь говнокод во что-то прекрасное и каждый разработчик рождается специалистом.
Но реалии таковы, что во многих фирмах фреймворк выбран за разработчиков и хочешь не хочешь, а берешь то, что решили за тебя.
То же и с подходами.
К примеру, фирма конвеером клепает простенькие заказы на Yii2 и вдруг заходит что-то серьезное. Скорее всего начнут делать как для конвеера и наберут кучу проблем т.к. надо было отказаться сразу от Yii2, а скорее всего и от PHP. (это так, пример из пальца чтобы передать суть)
Все это происходит уже лет 20, и никаких видимых результатов не приносит. Пора уже по-тихоньку заканчивать.
Начать заканчивать можно с того, что прекратить эти километровые споры про «Anemic vs. Rich Data Model»
А учат их ООП-шники во главе с Фаулерами и ДядьБобами. Сугубо гуманитарными методами, в стиле «белочка и зайка есть животные по признаку наличия четырёх лапок».
Я вот каждый раз, когда это слышу, спрашиваю (и ни разу не получил еще ответа): а что же гуманитарного в том, как учат "ООП-шники" вообще и Фаулер и Мартин в частности?
Оу, научный метод. А расскажите мне, пожалуйста, про научный метод (прямо вот начиная с критериев научности) применительно к разработке ПО?
Это ну примерно как быть Стивеном Хокингом, который придумал некую теорию, которую надо проверить на каком-нибудь ускорителе, и быть оператором этого ускорителя. Где-то посередине — те, кто построил ускоритель (создали вашу ОС, средства разработки в ней, тп).
Использование научных достижений и их, скажем так, созидание — суть разные вещи. Вы пользуетесь булевой логикой, что есть результат науки, но вы не создаете эту булеву логику с нуля.
Казалось бы, прекрасный пример того, что в разработке ПО научный метод совершенно не обязателен. Мне-то интересны, наоборот, примеры использования, причем хоть сколько-нибудь мейнстримного, "научного метода" в разработке.
Как минимум, прикладное и системное программирование — не научная деятельность, а инженерная. Как и любая (хотя может и не любая) другая инженерная деятельность она базируется на достижениях науки, но напрямую научный метод не использует, принимает господствующие теории и гипотезы на веру и использует их предсказательную силу.
архитектура по принципам 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 сущностей» (второе, на мой взгляд, имеет куда большее значение в практической деятельности).
В случае большого количества мелких сущностей помнить и понимать их устройство, как ни удивильно, проще, т.к. они естественным образом группируются по подобию. Например, «это валидаторы, по одному на каждый тип сущности, они делают то-то и то-то», «а это репозитории, они устроены примерно так-то и так-то, вызываются обычно из таких-то мест», «вот эта пачка классов соответствует различным стратегиям, используются там-то, выбор стратегии зависит от того-то» и т.п. В случае больших классов с многочисленными методами и внутренними взаимосвязями когнитивная нагрузка выше, как результат, помнить и понимать их устройство — сложнее.
Это вопрос декомпозиции/модульности системы, как организовать поведение так, что бы с этим легко было работать. Что бы модули были логически целостной единицей (SRP тот же как раз про это), и что бы связи между отдельными модулями были минимальны (это по сути то, о чем говорит нам и information hiding и подобные концепции).
Относительно God Object — очень просто разбить модули на маленькие, гораздо сложнее разбить их так, что бы количество связей между ними было минимальным. Но анемичные модели (предмет дискуссии статьи) это как раз таки приводит к большей связанности между модулями и понижает кохижен модулей. Что в свою очередь приводит к большой когнетивной нагрузке.
«а это репозитории, они устроены примерно так-то и так-то, вызываются обычно из таких-то мест»
а это logical cohesion и он является плохой практикой. Модули группируются по функционалу а не по подобию. В случае репозитория у вас в модулях представляющих разные элементы предметной области будет у каждого свой интерфейс репозитория. Реализации оных уже можно сгруппировать в один модуль.
Однако даже этот вариант лучше жирных классов, поскольку последние как правило больше про случайную прочность модуля (то есть все элементы взяли и распихали почти рандомно).
Практика показывает что люди очень плохо понимают куда ложить ту или иную програмную сущность.
а это logical cohesion и он является плохой практикой. Модули группируются по функционалу а не по подобию.
С этим согласен, но я имел в виду не столько группировку в одном модуле, сколько выстраивание картины мира в голове разработчика. Если он знает, как в общем устроены сущности определенной категории в его проекте (те же репозитории, к примеру), то ему не нужно помнить каждую из них в лицо, чтобы представлять, для чего, что и как она делает. С «толстыми» сущностями такая унификация труднодостижима, у каждой своя индивидуальная специфика.
Если поведение из одного класса перенести в новый класс, то классов становится больше.
А если оставить в том же, то в одном классе будет больше методов. И проблема количества сущностей никуда не денется. Но разные классы позволяют, по крайней мере, выстраивать иерархию, уменьшая количество объектов, которые надо держать в голове одновременно.
В общем случае функциональность сущностью не является.
Сущностью в понимании 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, но почему это должно меня волновать?
Изменение алгоритма является основанием для изменения класса?
Изменение алгоритма является основанием для изменения класса?
Того, в котором реализован этот алгоритм — да.
Не обязательно. Смотрите первоисточник, вам его уже даже процитировали.
нет, все еще одна — вам нужно как-то по другому со строками работать.
То есть условная роль, которая требует изменений, все еще одна.
p.s. если для вас сложны роли и зоны ответственности и все это такой ужас вызывает — не смотрите в сторону DDD.
Вот тут нагуглил еще одного недовольного, причем не только 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 посмотреть, они чуть менее расплывчато сформулированы.
что это неудобно, значит надо будет разнести.
но это еще более субъективно. Я знаю разработчиков которым удобно глобальный стэйт и процедуры. Просто потому что они так привыкли за 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, который входит в SOLID, и который люди применяют на практике, это никакого отношения не имеет.
важный момент, который хотел подчеркнуть Боб, это то, что вам важно анализировать поток изменений требований, разбираться "почему вдруг понадобилось это менять". Кому и т.д. И учитывать эту информацию что бы делать "гибкой" ту часть системы которая более подвержена изменениям.
Иначе, в отрыве от этого, мы будем вынуждены добавить те самые лишние слои абстракции, что далеко не всегда является благом.
Есть метрика "количество ответственностей у программной сущности", есть принцип единственной ответственности "значение этой метрики должно быть равно единице".
кохижен и каплинг это метрики кода
замечу что это не совсем классические метрики, они очень субъективны, они проявляются по разному и вы не можете "посчитать" их в численном представлении. Можно лишь сказать "низкая", "допустимая" и "высокая".
Ну и опять же, есть скажем принцип "у модуля кохижен должен быть высоким а каплинг низким". Можно учитывать только этот принцип, а можно применять SOLID. Результат будет приблизительно таким же.
почситать из легко на уровне класса
Да? Покажете, пожалуйста, алгоритм расчета cohesion на уровне класса.
lac of cohesion все же немного "не то".
количество непересекающихся в обработке, наборов данных класса.
Что такое "непересекающиеся в обработке наборы данных"?
Каков cohesion у статических методов класса string
? А у Enumerable.*
?
Понятия не имею
Ну то есть на двух тривиальных примерах ваш алгоритм расчета метрики дает результат "понятия не имею".
то возникает вопрос в какой из них должна располагаться логика взаимодействия.
Да, и на этот вопрос нужно дать ответ. Для того что бы примерно прикинуть правильно ли вы дали ответ и придумали целую кучу принципов (SRP, high cohesion, low coupling, information hiding, protected variations)
Каким образом, по вашему мнению, SRP говорит что не надо разбивать класс string на сервисы?
у этого класса одна единственная причина для изменений. Так же мы можем сказать что этот класс обладает такой характеристикой как communicational cohesion, что в целом вполне удовлетворительно.
Да, и на этот вопрос нужно дать ответ.
верно, но SRP дает неправильный ответ.
у этого класса одна единственная причина для изменений.
Это какая?
но SRP дает неправильный ответ.
SRP дает вопрос, а не ответ. А уже ответ на него вы предоставляете. Так что не стоит обвинять в ваших решениях принцип, который предлагает вам лишь способ как проверить правильность вашего ответа.
Это какая?
Работа со строками, разумеется. Вполне себе единственная область ответственности.
Что бы вообще проверить, нарушается ли SRP, нам нужно проанализировать изменения требований к этому модулю, желательно побольше изменений и попытайтесь проанализировать являются ли они общими (например, все они связаны с одной и той же структурой данных — массивом символов — лежащих в основе. Или еще чего такого.
SRP намного более сложный принцип нежели "у всех классов должен быть один метод".
Вот вы говорите работа со строками. А какая именно работа? Преобразование строки с учетом культуры это тоже работа со строками?
Только одно это уже говорит что с ним что то не так.
возьмем простое определение "SRP это когда у сущностей одна причина для изменения". Все довольно просто, проблема только со словами "причина" и "изменения". Причина обычно роль, а значит мы должны уметь ее определять. Изменения — это только то изменение в поведении которое эта роль может захотеть. И должна быть только одна роль, которая хочет что-то менять в сущности.
То что принцип "сложный" не значит что с ним что-то не так. Скажем LSP по каким-то причинам считают сложным принципом, хотя из всех что есть в SOLID это пожалуй самый простой.
Преобразование строки с учетом культуры это тоже работа со строками?
нет, поскольку тут важным становится знание о культуре. То есть мы должны делигировать эту ответственность кому-то кто знает о культурных отличиях. Если при работе со сроками единственная роль, которая бы хотела что-то менять это разработчик, во втором варианте у нас уже осуществляется эффект на конечного потребителя, и появляется новая роль которая может захотеть поменять формат под себя. Причем для каждой культуры будет своя роль, а значит правила мы так же должны сгруппировать каждый в своем маленьком модуле. Эти модули затем мы можем сгруппировать в один (i18n какой), так как вся совокупность ролей, меняющих содержимое модуля делает это по одной причине.
Это библиотека. Библиотека для использования широким кругом лиц. Библиотека большая, разноплановая.
Здесь явно напрашивается отделение внутренней архитектуры от внешнего интерфейса.
Использовать строки удобнее когда все методы в одном классе. А вот писать и поддерживать — когда оно разделено на слабосвязанные сущности.
Соответственно пишем код «как должно быть», а потом закрываем это тонким фасадом где каждый метод буквально состоит из одного вызова реального класса.
Ну а внутренняя архитектура?
В первой итерации я бы набросал пачку интерфейсов. Интерфейс сравнений, интерфейс поиска, интерфейс манипуляций, интерфейс расчлененки и т.п.
Сделал бы несколько базовых классов строк (явно напрашиваются раздельно односимвольные строки и многосимвольные, плюс в зависимости от общей архитектуры возможно еще два разделения — с длиной в начале или с нулевым байтом в конце), расширил бы их врапером который реализует все эти интерфейсы.
Ну и отдельно уже реализацию под все это.
Архитектуру реализации уже строим под реализацию. В первом приближении бьем по классическому вопросу «что будет меняться вместе?».
Потом набутстрапить соответствующие классы и частично их внутренние методы, когда структура уже «на бумаге», то отрефакторить первоначальное разбиение, за ним отрефакторить изначальную структуру интерфейсов. На этом этапе мы уже довольно четко видим возможные различия в функционале разных реализаций, так что можем выстроить структуру интерфейсов которые будут использоваться потом в клиентском коде. (Именно на этом этапе, поскольку напрашивается какой-то условный IString, но в процессе может оказаться что мы решим что-то делать только для однобайтовых строк, или к примеру мы окажемся настолько ленивы что для строк с нулем на конце мы не будем делать методы связанные с длиной, так что лучше такие вещи чуть отложить до тех пор пока не будет скелет готов).
Дальше по мере развития кода мы правим только соответствующие классы. Разбиваем их на слои, или просто разбиваем на более мелкие сущности, объединяем, переписываем, покрываем тестами — все как у людей.
При этом интерфейсы и фасад меняются редко, так что вполне можно сказать что они у нас являются отдельной сущностью, так что и по архитектуре тоже верно что мы его отделили от всего остального.
В общем что сказать то хотел. Фасад отдельно, мухи отдельно.
в реальной жизни получается, что никто в проекте не может запомнить где находится та или иная функциональность.
Вот как раз чаще это получается при нарушении 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.
Антипаттерны не потому плохи обычно, что делают что-то плохо, а потому что легко их использовать неправильно.
Комментирование к оригинальной статье закрыто, но это не означает что перед распространением этой статьи не нужно было ознакомиться с комментариями к ней. В этих комментариях ясно и лаконично определено какие именно ошибки в проектировании (да, именно ошибки), привели автора к этой идее. Автор оригинальной статьи не способен смоделировать предметную область и ошибки в этом моделировании валит на 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. Другими словами, модель обуславливает саму архитектуру и смешивать все в одну кучу не верно.
Вообще говоря, существует логика приложения, и бизнес-логика, которая, в свою очередь, разделяется на 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)
Тут, правда, есть нюансы, но я не хочу перегружать этот комментарий, если интересно, то после данной цитаты я их приводил в этой статье по проектированию Сервисного Слоя.
Форматтеры в модели, это как раз пример плохой богатой модели. Из-за таких моделей появляются подобные статьи. Модель богатая, потому что в ней описана вся ее бизнес логика, а не потому что в ней описано вообще все-все, что к ней относится. Форматирование, этой слой UI.
Это зависит, мне кажется. Например, если обращение к пользователю по имени различается в зависимости от каких-то факторов (скажем, до 18 лет — ФИ, 18+ — ФИО) — это может и с натяжкой, но бизнес-логика.
И мне лично в таких случаях было бы комфортнее писать person.Name.OurSpeciallyFormattedName
, а не new PersonNameFormatter().FormatOurSpeciallyFormattedName(person)
.
Сущность Person это не особо забивает, потому что из нее торчит не простыня форматов имени на все случаи жизни, а только что-то вроде public PersonName Name => new PersonName(this)
, и именно этот PersonName инкапсулирует всю логику, связанную с разными форматами имени. Вроде как такой торчащий из сущности PersonName — это пример Value Object, хотя тут я могу заблуждаться.
На истину не претендую; если такой подход чем-то плох, рад был бы услышать критику.
Закон Деметры. Статья 1, п. 1.
С большой натяжкой. И, главное, это провоцирует добавление этой самой простыни для всех случаев жизни. Вы сами может и не добавите, а кто-то другой, увидев, что вы добавили OurSpeciallyFormattedName, добавит для другого случая, например для отчётов в налоговую FiscalFormattedName, где ФИО надо выводить полностью. А потом пенсионный попросит не ФИО, а ФИ.О… И это в рамках одной юрисдикции, при неизменяющихся требованиях собственно бизнеса.
Да, в целом, это хороший подход вещи типа имени выделять модели в отдельный 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 распространены и нормально поддерживатся.
Паттерн — хорошее готовое решение, антипаттерн — плохое :)
Такого не бывает. GOTO и глобальные переменные это чаще всего плохо, но изредка это хорошо. Всё это хорошо/плохо — исключительно вопрос зрелости.
Незрелый разработчик причинит меньший ущерб, если не воспользуется GOTO и глобальными переменными там, где их применение было бы вполне уместно, просто потому, что не использует их нигде — патамучта "антипаттерн" или какое-то другое страшное слово.
Но это не делает их действительно антипаттерном или плохим решением в принципе, в отрыве от конкретной ситуации.
Если часть логики поместить в сущности, а часть в сервисы, можно запутаться
Можно конечно, особенно если проект сложный. Тогда для "запутаться" в целом ничего особо не нужно дополнительно.
Вся проблема с тем, насколько плохо люди анализируют два параметра модулей при проектировании: cohesion и coupling. Для анемичной модели, сервисов менеджеров и т.д. есть свое определение силы cohesion и влияние на coupling. И размещение логики в сущностях/объектах значениях как раз таки один из способов снизить связанность и повысить кохижен модуля.
Наружу нужно выносить логику взаимодействия объектов, не укладывающихся в композицию, в has-a. За своё собственное состояние и состояние своих "детей" объект должен отвечать сам. Сложные взаимосвязи изменений можно реализовать разными способами, простыми сценарными скриптами, например, или доменными событиями, обработчики которых преобразуют события в последовательность вызовов разных методов разных объектов модели.
В первом приближении моё правило звучит так: чем сложнее иерархия или структура обьекта, тем меньше логики на его нижних уровнях.
мама, папа, бабушка, дедушка и воспитательница детсада взаимодействуют с ребёнком исключительно через публичные методы и не лезут в его внутреннее состояние своими грязными руками.
Хороший пример :) Единственное, ещё они взаимодействуют с ним путём реакции на его публичные события, как бы подписываясь на них, если ребёнок рядом с ними. А так же, когда ребёнок дергает их публичные методы.
Догматически — в классах мамы, папы, соседа, прохожего или в их общем super (в терминологии Java).
Прагматически — во внешнем сервисе.
Но это моё личное мнение. Я Вам его не навязываю.
Если решения сильно зависят от того, кто их принимает, то прагматически точно лучше в классах мамы, папы и т. д., чем устраивать простыню проверок на тип субъекта и лезть в его внутренности для анализа его внутреннего состояния типа раздражена ли сейчас мама и не получил ли папа отрицательной стимуляции на работе.
Ну а если перестать терзать ребёнка с этой аналогией и несколько абстрагироваться. Что мы имеем:
1. Имеется сложный граф обьектов, узлы которого как-то можно разбить на уровни.
2. В этом графе один узел должен изменить состояние другого узла с помошью его публичных методоа (в терминах Java)
3. Для проведения изменения узлу требуется информация об изменяемом обьекте, собственном состоянии и состоянии некоторых других узлов.
Вопрос: должна ли вся логика определения, как изменить узел (в нашем примере — ребёнка) быть сосредоточена в изменяющем узле.
Ваша позиция — должна всегда.
Моя позиция — не всегда. Чем больше информации от других узлов требуется, тем полезнее выносить её за пределы изменяющего узла.
Я правильно сформулировал суть различия наших мнений?
Спорная позиция про одинаковость решений мамы и папы с точки зрения подготовки человека к самостоятельной жизни, но давайте о программировании :)
Моя позиция скорее такая: логика определения, как изменить узел (в технических терминах, какие свойства и на что изменить, что делегировать и т. п. ) должна быть сосредоточена публичном методе изменяемого объекта, выражающем цель изменения через термин предметной области. Информация от других узлов или ссылки на эти узлы передаётся параметром метода, иногда конструктора, иногда изменяемый узел создаёт и контролирует другие узлы сам, в том числе полностью пряча их от внешнего мира, скрывая часть общего графа за своим контрактом.
Изменяющий узел принимает решение о том, когда ему нужно изменить состояние изменяемого узла и сообщает ему о желательных изменениях в терминах предметной области. Изменяемый узел сам решает как его менять, ну или решает не менять, так же решая сообщать об этом изменяющему или просто молча проигнорировать.
Не всегда это возможно или практически разумно, но к этому нужно стремиться, по крайней мере, если следуем ООП как методологии разработки, а не просто "пишем на * с классами" в процедурном или функциональном стиле.
там, где архитектура подвержена изменениям
Замечательное замечание! Именно поэтому я и говорю, что за анемичной моделью будущее — по крайней мере, в web-приложениях, где я обитаю. Там изменения всё время как из рога изобилия.
На противоположном полюсе я бы поставил энтерпрайз системы, особенно «полукоробочного» плана, когда компании продается готовый продукт, подвергаемый серьезной модификации под нужды конкретного заказчика. Заранее предусмотреть все требования даже одного заказчика — архисложная задача. А уж сделать так, чтобы будущие требования будущих клиентов без проблем укладывались в существующий продукт (который бы при этом не был «голым» фреймворком) — еще сложнее. Рано или поздно обязательно окажется, что допущения, справедливые для 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.
1) Объекты только в виде POJO, никакой логики внутри. Ибо если внутрь класса класть бизнес-логику, он быстро превращается в god object, внутри появляется очень много зависимостей и зависимостей от вызовов методов. Плюс нарушается принцип инкапсуляции — зона ответственности весь класс и все потомки, нет четких границ. В случае же процедурного-подобного подхода зона ответственности четко ограничена, и четко видны вход и выход.
Плюс объекты с бизнес-логикой внутри сложно тестировать, ибо оно зависит от внутренних состояний
2) Бизнес-логика в виде процедурно-подобных методов в классах-сервисах. Легко тестировать, легко расширять, легко видна зона ответственности, легко делится на слои, легко разделяется на разные классы
3) Наследование только в виде расширения POJO объектов, в соответствии с первым пунктом
4) Делим приложение на слои, минимальное количество: слой записи, слой чистой бизнес-логики и слой взаимодействия с внешним миром
5) Юнит-тестирование только в случае непонятного или страшного кода, в котором я не уверен. Гнаться за 100% покрытием — вредно, ибо увеличивает кодовую базу и затрудняет рефакторинг и правки.
Вместо юнит-тестов по максимуму интеграционные тесты, прогоняющие реальные сценарии работы с начала и до конца
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% покрытием — вредно, ибо увеличивает кодовую базу и затрудняет рефакторинг и правки.
тут два момента:
100% покрытие кода тестами не нужно, потому что оно не говорит нам о качестве тестов (мутационные тесты могут показать реально покрытие логики, но их удобнее использовать для поиска бесполезных юнит тестов, что часто случается у новичков) и насколько хорошо эти тесты все покрывают. Это инструмент который позволяет быстро найти участки кода, которые вообще не тестированы, но тот факт что они не протестированы не означает что они должны.
- если тесты мешают вам рефакторить, это может быть симптомом того, что ваш тестируемый код обладает очень высокой связанностью (что не удивительно, учитывая предыдущие пункты). Тесты это в первую очередь клиентский код. То есть он испытывает все проблемы проектирования контрактов, которые они тестируют. Можно грамотно использовать этот инструмент для того что бы делать выводы почему связанность так высока и как это можно изменить.
Вместо юнит-тестов по максимуму интеграционные тесты, прогоняющие реальные сценарии работы с начала и до конца
Методы класса зависят от состояния класса — факт. Повреждение состояний вызовет повреждение метода.
И уж тем более завязываться на внутреннее состояние. Давайте уж сразу глобальными переменными хуячить.
Микросервисы — зло.
Тесты это в первую очередь клиентский код
Нет. Юнит-тесты это закрепление текущей реализации кода. Белый ящик, дублирование логики.
Интеграционные тесты — действия пользователей, внешнее воздействие
Методы класса зависят от состояния класса — факт.
факт если они зависят от состояния класса. Я дал вам ссылку с разбором всех 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 )
}
}
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, скорее всего, перебор, но если речь будет идти не про время, а про какие-то сущности, которые по-разному могут обрабатываться в разных странах/регионах/компаниях — такой подход оказывается довольно удобным (хотя, конечно, со своими издержками).
Пример с датой и DI описан в статье Beyond Mock Objects. Может будет любопытно.
Когда разработчики хотят использовать Анемичную модель, они делают несколько хуже себе.
Да, агрегат (и я имею в виду насыщенную модель) становится сложным, у него много обязанностей. Но это в одном слое/уровне или за некоторым фасадом. Агрегат при этом требует соблюдения простых правил, и ответственен только сам за себя.
То есть в определённых границах, насыщенная модель сложна, но и в ней есть место декомпозиции. За этими границами это такой же простой объект, как DbContext или Mapper, или что угодно ещё.
Зато, я верю что работу по Анемичным моделям легче спроектировать и делегировать. Моя работа делать ПО в срок с установленным качеством. Если ваша не продавать софт лично, я не понимаю зачем вам это.
Отдельный разговор, то что большинство примеров АМ не показывают соблюдения инвариантов.
[Перевод] Анемичная модель предметной области — не анти-шаблон, а архитектура по принципам SOLID