Pull to refresh

Comments 20

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

Единственное, с чем Я бы не согласился, это с тем, что среда определяет архитектуру приложения. Хотя размышляя сейчас, Я прихожу к выводу, что и так сказать можно, это в любом случае строительные блоки архитектуры.
Так же и "архитектурные паттерны" ООП действительно являются архитектурными, просто это определенный разрез архитектуры.

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

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

Архитектура проекта - это высокоуровневая организация, идущая прежде всего от доменной области.
Скажем больница - это палаты под Х пациентов, Y операционных, рентгенологическое отделение, административные помещения, и приемное отделение с Z кабинетами врачей. Обязательно наличие грузового лифта.

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

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

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

UFO landed and left these words here

Неплохой пример.

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

И разница в функциях и требованиях делает их очень разными

Так ООП никогда и не определял архитектуру.

ООП всегда был парадигмой программирования, а не архитектурой.
То есть вещи которые можно сравнивать с ООП это модульное программирование, или банальный спагетти код, где все вообще одним большим куском.
Даже функциональное программирование я бы ставил немного отдельно, а не в один список.

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

Хм, и всё же я, например, склонен считать ООП про макроархитектуру, где в тоже время ФП и процедурщина - микро.

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

Т.е. если с этой стороны рассматривать, то ООП про архитектуру как раз, это супер-надстройка над ФП/ПП, а ФП и ПП уже про частную реализацию, про внутрянку методов и организацию (реализацию) алгоритмов внутри них. И даже если составлять алгоритм из композиции объектов (типа new Where(new Eq('some', 23), new Gte('any', 42))), то это больше про ФП, хотя и используются объекты.

P.S. Очевидно, что всякие АОП рассматривать не имеет смысла, т.к. это дополнение к существующим прадигмам, а не самостоятельная.

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

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

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

А вот изоляция в ООП и упрощение разделения кода на объекты конечно выше, но все-таки это больше относится к тому, как это будет написано, а не как это будет взаимодействовать.

Вот если посмотреть с точки зрения, чем ООП отличается от процедурного программирования в рамах архитектуры? Да почти ничем.

В рамках архитектуры - ничем. Но ООП только про архитектуру и взаимодействие. А процедурщина и про архитектуру и про реализацию.

Захотелось прокомментировать)

Структурирование кода:

В какой то старой книжке было деление программы на архитектурные и конструктивные элементы. Архитектурные обладают независимостью от платформы и долговременной стабильностью, конструктивные платформоориентированы и могут образовывать стеки ввода вывода и тепе. К примеру использование com и lpt в рамках одного канала обмена - использование разных стеков конструкционного по.

Не делайте ничего лишнего

Звучит это вроде и правильно, но в жизни ни разу не было так, чтобы это срабатывало. В одном из первых проектов заказчик и руководитель убеждали быстро сделать что то вроде демо - была некая среда разработки и библиотека С с printf на хост. Мы убедили начальство, что сначала надо разработать коммуникационное ядро для сети процессоров, а алгоритмы подгружать в соответствие с конфигурацией многопроцессорной системы. Это заняло месяца 3, ещё пара ушла на отладку алгоритмов. Но вот следующий проект на этой же сети был выполнен за месяц. При упрощённом подходе он бы занял столько же, как и первый, если бы вообще заработал из за отсутствия средств отладки, нагруженности и математики. В других проектах возможность "переписать потом", как правило, отсутствовала не смотря на то, что руководство очень увлекалось всякими аджайлами. Некоторые писали почти демо, которое потом долго и мучительно допиливали тратя ещё столько же времени. В общем излишняя начальная простота может существенно затруднять не только развитие продукта, но и обычную работу.

Слои и взаимодействия

Уже давно интересно, почему на Хабре часто ссылаются на Чистую архитектуру, в курсе существования OSI RM, но вот OSE RM как то не модно. Может потому, что разрабатывают программы, а не системы?)

Хорошая статья, спасибо автору!

Единственное что я бы добавил - для кого именно написана эта статья? мне показалось что это написано для новичков и в таком разрезе все это полезно - новичку с первых строчек кода нового проекта очень сложно построить грамотные и компактные методы-классы

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

Мне кажется ООП уже несколько лет как в опале и даже в исконно-оопешых языках уже все пишут в "функциональном" (по факту, в процедурном) стиле. Когда есть объекты для хранения данных (структуры) и есть объекты-сервисы, содержащие функции для обработки переданных данных.
Но как по мне, так ООП вполне хорошо подходит для написания бизнес логики (ядра системы). Так называемая, rich model, когда объект позволяет собрать в одной программной сущности данные и логику их обработки. Очень удобно, когда у тебя вместо модуля из набора структур и функций есть экземпляр объекта 'Курс', например, и любой редактор выдаст тебе список доступных действий с ним (его публичные методы).
Правда давно уже не видел, чтоб кто-то такой подход практиковал. Во многом из-за опасений, что мутабельный стейт создаст проблем при многопоточной обработке. Но как часто в реальности есть потребность менять один экземпляр предметной области в ядре системы несколькими параллельными потоками?

Дело обычно не в стейте, а в том, что тогда класс "Курс" будет занимать несколько тысяч строк, и в том, что для действий с курсом обычно требуются внешние зависимости. Вы будете email о покупке курса отправлять из класса сущности? Как вы будете пробрасывать компонент, который это делает, через параметры метода? И отправлять email до коммита транзакции? Или курс сам себя будет коммитить?

Сохранение в базу, отправка email - это вещи которые не связаны с доменным ядром системы. Это все уже делается на других слоях (о чем, собственно, статья и говорит).
В ядре системы должны быть лишь операции, меняющие саму бизнес-сущность.
И да, для каких-то сложных сущностей может получится достаточно внушительный по размеру класс. Но как по мне, гораздо безопаснее держать в одном месте все методы (функции), которые могут менять бизнес-сущность и легко получать их список, чем иметь простую структуру данных и менять ее поля в разных функциях, которые по всему проекту раскиданы. Гораздо удобнее, когда у каждой бизнес-сущности есть "владелец", который единственный, кто может ее менять и предоставлять другим частям системы ограниченный API для взаимодействия. Так почему же не объединить это в одном куске кода (классе) - в этом и была главная задумка классов и ООП.
Если на примере веб приложения говорить, то мне больше нравится, когда контроллер получает бизнес сущность из слоя работы с базой, вызывает у сущности метод с нужной бизнес логикой для ее изменения, передает измененную сущность для сохранения. А вот уже на слое сохранения данных в базу как раз и формируются все нужные ивенты, на которые потом триггерится отправка email, например. Но гораздо более популярен подход, когда контроллер дергает некую функцию, в которой по факту мешанина из бизнес логики и работы с внешними системами (база, очереди, другие сервисы). В результате, отправка email до комита транзакции выглядит вполне себе даже хорошим решением (на самом деле - не делайте так)

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

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

Но как по мне, гораздо безопаснее держать в одном месте все методы
когда у каждой бизнес-сущности есть "владелец", который может предоставлять другим частям системы ограниченный API для взаимодействия
Так почему же не объединить это в одном куске кода (классе) - в этом и была главная задумка классов и ООП

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

формируются все нужные ивенты, на которые потом триггерится отправка email

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

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

Ну то есть у вас толстые контроллеры, которые управляют сохранением сущностей.
Что если надо сделать то же действие в консольной команде, будете копипастить код контроллера?
Что если надо для загрузки изображения к товару сначала сохранить сущность File в базу, загрузить ее в файловое хранилище, и только после успешной загрузки сохранять ProductImage с id файла? Это все будет в контроллере?

У меня есть статья на эту тему с примером бизнес-требований, попробуйте написать хотя бы метод sendForReview в вашем стиле (там 40 строк логики) и сравните код.

Бизнес‑требования есть и к операциям, которые не меняют бизнес‑сущность

...

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

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

Так с логикой в сервисах все то же самое - все методы действий с сущностью можно поместить в один класс сервис

Да, но нет. Анемичная модель никак не может запретить менять состояние сущности из любого места в обход API, предоставленного сервисом. Если с кодом работает больше одного человека, то рано или поздно это произойдет. Плюс в сервисах возникает большой соблазн добавлять зависимости на другие сервисы и ответственность этого сервиса начинает сильно разрастаться (со всеми последствиями нарушения single responsibility).

Ну то есть у вас толстые контроллеры, которые управляют сохранением сущностей.Что если надо сделать то же действие в консольной команде, будете копипастить код контроллера?

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

У меня есть статья на эту тему с примером бизнес-требований

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

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

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

Анемичная модель никак не может запретить менять состояние сущности из любого места в обход API

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

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

Плюс в сервисах возникает большой соблазн добавлять зависимости на другие сервисы и ответственность этого сервиса начинает сильно разрастаться (со всеми последствиями нарушения single responsibility)

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

В этой статье есть примеры кода. Там для соответствия SRP код выносится в отдельный класс, который пробрасывается в исходный.

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

А если у вас в контроллере происходит маппинг сущности, которую сервис вернул в DTO ответа - это толстый контроллер, или еще нет?

А при чем тут DTO ответа, если я сказал про сохранение?
Возврат ответа это ответственность контроллера, он для этого и нужен.

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

Вы говорите про DDD, но сами почему-то ему не следуете. В DDD эти вещи делаются не в контроллере, а в Application Service. Это и есть "новый уровень абстракции", и он делается всегда.

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

Его легко писать, а также легко поддерживать и развивать.

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

Много умных слов, а кода нету. Как обычно и бывает в таких случаях.
Если упрощает, то покажите. У меня 30 строк валидации, если можно упростить, значит будет еще меньше. Но вы почему-то предпочитаете писать много текста.

И это снимает проблему блокировок

Вы похоже не поняли, зачем нужны блокировки. Никакая стейт-машина не запретит пользователю отправить 10000 товаров на ревью в одном процессе и загрузить файл с новыми данными на 8000 товаров в другом процессе, где список товаров пересекается на 90%. Один и тот же товар может обрабатываться в обоих процессах буквально в один и тот же момент времени. А если товар на ревью, то новые данные загружать нельзя.

Мьютекс это стандартный примитив синхронизации параллельных процессов, его нельзя заменить стейт-машиной.

Вы говорите про DDD, но сами почему-то ему не следуете. В DDD эти вещи делаются не в контроллере, а в Application Service.

Не стоит, к слову, путать DDD и гексагоналку/слоистую архитектуру. В DDD нет ничего ни про какие слои так-то ;)

вы опять дизайн системы путаете с имплементацией дизайна )

мой изначальный посыл как раз и был, что рич модель в ООП как раз и заставляет сначала подумать о дизайне. Ведь очевидно, когда в бизнес сущность Курс тащишь сервис для отправки email - это дикость. А вот в анемичной модели в CourseService затащить EmailService - вполне нормальное решение. Хотя по сути, это одно и тоже - нарушение границ контекста и смешивание уровней абстракции

Хотя по сути, это одно и тоже - нарушение границ контекста и смешивание уровней абстракции

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

Ведь очевидно, когда в бизнес сущность Курс тащишь сервис для отправки email - это дикость.

Конечно, потому что EmailService должен быть в вызывающем коде, который работает с этой сущностью.

Sign up to leave a comment.

Articles