Pull to refresh

Comments 91

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

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

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

Само-собой, лучший вариант — адаптация всех подходов под ваши задачи.

Как правило, статьи про "чистую архитектуру" заканчиваются списком лучших практик без определения этой самой "чистоты" и методов ее измерения. Оценки "легко" и "понятно" такие же субъективные. Есть научные статьи, которые предлагают разные формулы измерения трудности восприятия кода, но мне известна лишь одна метрика - cognitive complexity от Campbell. Есть ли ещё?

Поддерживаемость и простота — объективные метрики качества.

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

Я для себя отказался от YAGNI, KISS и SOLID, это всё уже стало частью поп-культуры программирования и мало что значит. Вбрасывать это в разговор - к срачу.

Кстати, и overengineering и oversimplification можно использовать для успокоения нервов особо бесноватых индивидуумов. Иногда выходить за рамки, мне кажется, можно и нужно.

Как-то это странно звучит. По началу мне нравился КиШ, а потом его стали крутить везде, опопсела группа, а я хочу андеграуд, попса это зло :) Хотя по факту ничего не поменялось.

Что значит "отказался" от YAGNI, KISS, SOLID? Делаем наперекор? Если не упарываться, то эти принципы выведены из практики, а не практика построена на этих принципах. Другое дело, когда их выпячивают и ожидают знания академических определений назубок зачем-то. Но это совсем другая проблема, из области дай дураку...

Что значит "отказался" от YAGNI, KISS, SOLID? Делаем наперекор? Если не упарываться, то эти принципы выведены из практики, а не практика построена на этих принципах. 

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

Понял. У меня другое мнение на этот счёт. Многие люди признают, что с опытом заявленные принципы становятся очевидными и применяются даже без явного знания или умышленного следования им. Эти принципы нужны для согласования работы, новичкам. Я понимаю, какому-нибудь сеньору всё итак понятно, и ему все эти лекала не нужны, он чувствует как надо, но зачем это экстраполировать на всех? Да, может они устарели местами, завирусились, это не делает консолидированный опыт плохим. Как минимум, это может быть полезно на code review, естественно без фанатизма. Отрицание это напоминает юношеский максимализм. Чего эти бородатые светила с олимпа нам тут напридумывали? Нам и без этого хорошо. Но это ИМХО. Тренд отрицания всего и вся тоже можно назвать завирусившимся.

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

Понимаете, вопрос ни в том какой у кого мнение. Можно было бы сказать что Роберт Мартин и ко, никакие не светилы с олимпа создавшие грандиозные проекты а скорей успешные писатели. Что если посмотреть его проекты то он давно не следует своим же принципам. Что в своем блоге он писал что когда придумывал SOLID не очень понимал принцип подстановки Лисков. Что принципы формировались так чтобы получилось красивое слово а не по соображениям важности. Но я не будет приводить это как аргументы так как это в некоторой мере субъективно. Я приведу объективный аргумент.

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

Как минимум, это может быть полезно на code review, естественно без фанатизма.

Выкатил Петя МР, а ему Дима говорит: ты тут нарушаешь SRP, после чего начинается срачь, т.к у каждого свое видение. Потому как SRP не описываем хоть как то детерминированный механизм реализации собственного постулата а получается тотальная субъективщина. Толку от такого принципа, как от совета прохожего: "живите правильно и все у вас будет". Тоже самое с ISP и т.д. Принцип LSP наиболее адекватный и детерминированный но самый редко применимый, да и к примеру, если посмотреть на стандартную библиотеку Java, то там раз через раз он нарушается (видимо разработчики языка программирования забыли сходить к Мартину).

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

Тренд отрицания всего и вся тоже можно назвать завирусившимся.

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

Постараюсь кратко ответить на это по существу:

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

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

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

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

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

Бездумно чему-то следовать -- очень плохо. Надо применять с умом. Как и бездумно отрицать и нивелировать накопленный опыт.

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

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

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

То что он разжёваны и то что их вообще нужно толковать - это и есть проблема, у нас вроде бы не теология а программирование. Если у принципа нет однозначной интерпретации то пойдут толкования, различные взгляды и направления. Кто будет определять кто из толкователей самый правильный и какая трактовка самая верная? И мы приходим к тому с чего начали: на МР два разработчика будут спорить каждый со своей колокольни, потому что правильного ответа нет как и методики. Если же ссылается к толкованиям от авторитетов то мы превратиться в подобие религии а должны хоть как то походить на науку.

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

Когда строят здание то руководствуются не абстрактными общими принципами а конкретными обоснованными с точки зрения науки и практики нормами.

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

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

Т.е. суть в том, что они действуют не только между участниками одной команды, а между участниками всех команд в мире.

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

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

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

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

Извините, но я не понял откуда взялось "толкования". Их не надо толковать, они правда хорошо описаны.

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

Различные взгляды произрастают только из-за лени и недостатка хоть какого-то опыта. Вот будет какой-нибудь новоиспечённый джун убеждать вас в том, что 2+2 это 22. Вы правда после этого начнёте утверждать, что алгебра плоха, потому что кто-то неправильно её толкует? Ну да, будут такие люди, это неизбежно.

Когда строят здание то руководствуются не абстрактными общими принципами а конкретными обоснованными с точки зрения науки и практики нормами.

Принципы достаточно хорошо согласуются с практикой. Их ноги растут из практики.

В общем, я не очень понимаю, какую проблему вы нашли в SOLID-е. И чем предлагаете заменить. Да, я пока слышу вас так: всё это древнее и плохое, их кто-то не так воспринимает, надо выкинуть.

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

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

Я сути проблемы не увидел. Потому что в моей практике, принципы работают хорошо, они действительно помогают писать хороший код и не тратить время на объяснение вещей, которые уже хорошо объяснили и описали. И люди уже приходят с этими знаниями, которые стыкуются со знаниями людей в командах. Это плохо? Ну видимо по-вашему да, надо каждый раз изобретать свой уникальный язык и систему правил в каждой команде. Так ведь интересней :)

Ну вообще OL и немного D не самые очевидные штуки. И таки ниспосланы.

Отказался использовать в аргументации: звучит фальшиво и лениво. Стоит использовать только аргументы которые можно опровергнуть, иначе создаётся ощущение что тебя послали на 4..5 букв. YAGNI, KISS, SOLID и прочие имеют значение на момент прочтения - суть уловили, впитали и забыли.

Например:

  • я положил весь код в контроллер - не потому что KISS, а потому что это CRUD без логики; появится логика - усложним _немного_

  • я не разделил логику и доступ к данным - не потому что я не люблю Single Responsibility, а потому что меня задолбало смотреть в два монитора сразу наш микросервис уже достаточно responsible

  • я обработал все ошибки в лог - не потому что YAGNI, а потому что я не знаю, что здесь вообще будет происходить и, прежде чем писать обработку, хочу на это сначала посмотреть глазами (добавлю алерт в kibana на этот лог)

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

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

В ряде случаев это становится проблемой. И да, иногда нерешаемая. Но об этом в статье не написано ни слова.

Могу ли я написать простой юнит-тест на эту логику, не поднимая половину приложения? Если нет, то код - ГО*НО!

Aбстракция, которая не используется, — это не актив, это долг.

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

  1. Выносишь сложную логику в функциональное ядро (чистые функции на статике).

  2. Остальное остаётся без покрытия или покрывается сразу интеграционными тестами.

  3. PROFIT - "легкотестируемый кодом без неиспользуемых абстракций". И без возни с моками.

Иронично, но вот об этом я и писал - люди не понимают концепции и не хотят вникать.

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

Понадобится второй из другого места — вот тогда и выделите интерфейс.

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

Попробуйте в каком-нибудь проекте, ради эксперимента:

  • писать тесты только на чистые функции (очень много кода пишется только по идею "тестируемости")

  • инжектить классы, а не абстракции (пока не будет двух реализаций)

Я где-то 8 лет назад поймал себя на мысли что у меня тесты стали походить на произведения Лавкафта: элдрич юнит тесты с циклопиан моками тестируют проблемы из параллельного измерения. Найти баланс я не смог, поэтому решил пойти с нуля: не писать моковые тесты вообще, в надежде что где-то что-то начнёт ломаться и я пойму где же эти тесты необходимы. Вот только ничего не сломалось; я если и ошибаюсь где-то, то это либо находят интеграционные тесты, либо я сам при смоук тестировании, либо кто-нибудь случайно через год.

Вы не туда воюете :)

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

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

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

Затем, опять же, в меру своей упоротости, скажет что единственную реализацию надо пометить final.

"Мокнуть final class не получится!" возразим ему мы. "Используй реальную реализацию, а не мок!", ответит нам он.

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

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

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

ПРОСТО ПОЙДИ ИНИЦИАЛИЗИРУЙ И ПРОКИНЬ ЗАВИСИМОСТЬ В КОНСТРУКТОР КЛАССА ВО ВСЕХ 1488 ТЕСТАХ, НО НИ В КОЕМ СЛУЧАЕ НЕ ДОБАВЛЯЙ ИНТЕРФЕЙС, ЭТО НЕНУЖНАЯ АБСТРАКЦИЯ, ВОТ ВЕЧНО ТЫ МУДРИШЬ.

Затем эта зависимость для инициализации требует еще зависимость, и она тоже единственная (по требованию бизнеса) и финальная (по требованию тимлида), и цикл повторяется.

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

Понимаете?

Я, например, не понимаю.

У меня тесты написаны на чистые функции (хоть и C#) и у меня нет боли от того, что изменился конструктор и вот теперь мне приходится...

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

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

Если бы мои тестируемые методы работали не с конкретными параметрами, а с моками, то мне всё равно пришлось бы эти моки менять.

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

ну а у вас оказали

если не пытаться "тестировать всё", а покрывать тестами только необходимое и сложное, то как-то сама собой вырабатывается устойчивость тестов к рефакторингу

ну а у вас ее, устойчивости, не получилось. почему так, если вы сделали всё правильно?

"главное в поисках виноватого (не) выйти на себя" (с) :)

я еду без моков и прекрасно себя чувствую.

"я просто пошел и переписал все тесты когда у одной из зависимостей SUT появилась новая зависимость, и это прекрасно, ведь я точно не усложняю себе жизнь лишними ненужными абстракциями" :)

>ну а у вас оказали
Нет, "изменения конкретных классов внутри тестируемых методов" влияния не оказали.

>ну а у вас ее, устойчивости, не получилось. почему так, если вы сделали всё правильно?

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

>я просто пошел и переписал все тесты когда у одной из зависимостей SUT появилась новая зависимость

Просто сам SUT изменился по причинам, которые я объяснил выше. И переписать тесты, написанные в функциональном стиле, было конечно же проще, чем переписать те же тесты, написанные на моках. А переписывать тесты всё равно пришлось бы.

Надеюсь, я смог рассеять возникшие у вас недоразумения.

построение алгоритмов и моделей приходится вести итерационно, игнорируя некоторые аспекты

судя по анамнезу - забили на SRP, и отсюда все проблемы (классы делают слишком много, тащат с собой много нерелевантного)

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

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

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

>судя по анамнезу - забили на SRP, и отсюда все проблемы

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

>вступая в полное противоречие с кейсом что вы описываете (пришлось менять все тесты из-за новой зависимости)

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

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

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

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

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

ну не надо так не надо :) вы потратили наше время зря.

пусть у вас всё получится, хорошего вам дня

Мне кажется, я привёл довольно конструктивные аргументы в своём первом сообщении, на что в ответ получил телепатическое код-ревью в стиле "код-говно". Неудивительно, что беседа после этого продолжилась так, как она продолжилась.

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

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

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

В действительности, архитектура - это то, как мы видим наш код. Это не сам вгляд на код, это способ его понимания и описания.

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

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

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

Вот сейчас как раз развязываю кусок говноархитектуры, куда 5 лет назад была положена форк-бомба с запуском O(n^3) параллельных процессов, где n - количество сообщений одновременно обрабатываемых консьюмерами. Бонусом эти "архитекторы" болт положили на принцип high-cohesion/low-coupling.

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

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

Зато, те люди, стоящие у истоков mvp, наверно, бонусы получили за херак-херак-и-в-продакшен.

Если кто-то мне скажет: "не надо стремиться проектировать архитектуру с самого начала" или что-то подобное, то я отправлю разгребать его остатки того болота, там еще много дурнопахнущих закоулков.

И что, граждане, которые придумали плодить процессы в количестве O(n^3), способны были бы родить разумную архитектуру, если бы их заставили её с самого начала полностью расписать?

Что-то слабо верится.

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

Зато, те люди, стоящие у истоков mvp, наверно, бонусы получили за херак-херак-и-в-продакшен

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

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

Бизнесу всегда нужно ещё вчера и всё сразу, поэтому этим можно оправдать всё, вообще всё...

Напоминаю

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

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

  • Соблюдай правило: "если работает — не трогай", даже если не понимаешь как.

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

  • Не используй системы контроля версий и не делай бэкапов — пусть ощущение риска закаляет.

  • Ставь хаотичность и "быстроту" выше порядка и архитектурного планирования.

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

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

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

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

YAGNI упомянут совершенно не к месту. Это не «принцип, который почему-то большинство игнорирует», а скорее философская установка, которая формально «за всё хорошее и против всего плохого», но при этом не даёт никаких конкретных границ или ограничений, в отличии от реальных принципов

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

YAGNI упомянут совершенно не к месту. Это не «принцип, который почему-то большинство игнорирует», а скорее философская установка, которая формально «за всё хорошее и против всего плохого», но при этом не даёт никаких конкретных границ или ограничений, в отличии от реальных принципов

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

Между SOLID и YAGNI столько же общего, сколько между SOLID и ОМОН — их объединяет только то, что это аббревиатуры. Подскажите, вы действительно считаете, что фраза "вам это не понадобится" и вполне конкретный принцип инверсии зависимостей — это ровно одно и то же?

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

Например, очень часто можно встретить мнение, что принцип подстановки Барбары Лисков (LSP) не применим к Go из-за отсутствия наследования, но это не так.

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

Философия Go как раз и направлена на то, чтобы этих шагов было меньше

Чистая архитектура прекрасно сочетается с языком Go — вы не найдёте принципов, которые бы противоречили style guide языка или его философии.

Проблема, прежде всего, в самих инженерах. Автор и множество комментаторов, судя по их высказываниям, вовсе не понимают, что такое чистая архитектура. Для них DRY, KISS и SOLID — это одно и то же. Со стороны выглядит как: "ничего не понял, но на всякий случай осуждаю".

Эххх....

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

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

ЗЫ - Моя основная специализация это "восхваления бога оптимизатора" в низкоуровневых языках, и ни одна новомодная конструкция чистого кода или какого-то паттерна не приводит к увеличению эффективности. А код которые генерят эти паттерны просто сумашедше тратит такты, но с другой стороны в нём легко разобраться, он логичен и самодокументируем, что тоже важно в разработке.

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

Я тут относительно недавно общался в коментах по поводу DDD. Как пробрасывать 100500 полей в сущность при создании - через конструктор или еще как. Раньше я использовал анемичную модель.

В общем запилил я тренировочный микросервис, который условно резервирует товар на складе. По DDD, логика в сущностях, ORM, и все такое. Ну а так как резерв товара - это блокировки в любом случае в том или ином виде, подготовил пару вариантов - на оптимистичной и пессимистичной блокировке (SELECT ... FOR UPDATE).

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

Короче этот DDD выдал 20 rps для варианта с пессимистичной блокировкой.

Плюнул, перенес логику в хранимки, чтобы не тащить все это по сети из БД на сервер - стало 420 rps. Хотя пожалуй код из хранимки можно поместить как есть в репозиторий (там три запроса - update, insert и select). Но это все равно не станет "логикой внутри сущности".

Так что теперь мой главный вывод про DDD - это 100 раз подумать, прежде чем использовать DDD.

В DDD самая главная неправда - это постулирование искусственного отрыва кода от данных. Хотя весь Enterprise - это про работу с данными и персистентностью. Т.е., да, на верхнем уровне надо обсуждать потребности бизнеса и строить модели по возможности абстрагируясь от способа хранения. Но когда мы сажаем эти модели "на землю", то всегда получается, что персистентность имеет значение.

Моё мнение, DDD это не про сущность в программном коде в виде какого-нибудь класса со 100500 полями. Это детали реализации. Но почему-то упорно бытует мнение, что DDD = рич объект. Вовсе нет.

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

Вы программист или аналитик? если я верно понял из книг, то DDD это про бизнес дизайн и архитектуру всего приложения. А кодом ты реализуешь это по своему усмотрению...
Помню отличную фразу в одном фильме: "Может быть потому что ты му*ак?" - "Хорошая мысль, многое объясняет"...

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

Анализировай!©

DDD это про бизнес дизайн и архитектуру всего приложения

Да. Обследование ещё. И переход к проектированию. А проектировать уже как знаешь

Я программист. Еще я решил лишний раз проверить и скачал книгу Эрика Эванса, который считается основателем DDD и поискал там слова model, behavior и the code. В тех или иных местах встречаются фразы, которые говорят о том, что доменная модель в итоге преобразуется в программный код и там внутри должно быть поведение.

Knowledge rich model. The objects had behavior and enforced rules. The model wasn’t just a data schema

and the code is refactored to reflect the deeper model

И если диаграммы - это все же ближе к проектированию, а не коду, то вот скрин с примером кода rich объекта с поведением внутри.

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

И то, как я закодил свой тренировочный микросервис - это все же в какой-то мере DDD, только оно ужасно тормозит.

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

Начало транзакции
  Сетевой вызов №1
Дергаем БД и получаем строку с блокировкой
  Сетевой вызов №2
ORM маппит строку в объект
Бизнес-логика, валидация, обновление состояния
  Кидаем exception, если все плохо
ORM мапит объект в запрос на обновление
Дергаем БД и делаем update строки
  Сетевой вызов №3
Дергаем БД и делаем insert строки
  Сетевой вызов №4
Завершаем транзакцию и отпускаем блокировку
  Сетевой вызов №5

А вот как это выглядит, если отказаться от DDD (от программной части) и перенести в хранимку

Вызов хранимой процедуры с параметрами
  Сетевой вызов №1
    Начало транзакции
      Делаем update строки с блокировкой
      Делаем insert строки
      Делаем select (аки бизнес-правило / валидация)
        Кидаем exception, если все плохо
    Завершаем транзакцию

Что принципиально изменится, если в первом случае вы будете формировать весь SQL сценарий, по типу хранимки, на стороне приложения и отправлять одним запросом в базу? А если напишете то же самое в виде экстеншена для БД?

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

Смотря где это разместить.

Если вне сущности - например в репозитории (метод типа ReserveInventory) - то это не DDD по канонам Эванса. Бизнес логика вытекает на уровень инфраструктуры. Сущность похудеет и станет анемичной.

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

Погодите, управление транзакциями это уровнь application, а не domain. От репозитория в домене лежит только его интерфейс, а конкретная реализация живет в infrastructure. Модели не используют репозитории - это привилегия use case'ов.

Rich модели в домене это не про то, чтобы вообще всю бизнес логику в них притащить. Логика, связанная с бизнес сценариями, живёт в юз кейсах - т.е на уровне application (собственно ради чего мы его и пишем). Domain (уровень данных если угодно) отвечает за обеспечение базовых гарантий, ограничений, инвариантов - на уровне этих самых данных. Объекты домена могут быть связаны только с другими объектами домена и ни чем другим больше. Поэтому в них нет и не может быть ни какого IO.

Реализовывать логику средствами базы или приложения это вопрос не к DDD. Здесь выбор между 2-tier и 3-tier архитектурами, у которых есть отдельные причины и аргументы.

У меня в голове отложилось, что логика сущности должна быть внутри сущности, а application слой - это когда нам надо координировать работу двух сущностей, ну например

var бабло = счет1.Списать();
счет2.Пополнить(бабло);

Возможно я прав или не совсем прав.

А когда речь идет про одну сущность, все должно быть внутри нее и application слой получается тонким. Собственно у меня так и получилось. Точнее один агрегат и две сущности в нем - InventoryLevel и InventoryOperation (отражает историю списания или пополнения). У InventoryLevel есть метод Reserve, который делает все что нужно.

В смысле это "все что нужно" делается в памяти сервера. А потом, когда выходит из application слоя - оно же там обернуто в unit of work и транзакцию - вот там все и сохранятся в БД.

В противном случае, если я в конкретно своем примере вытяну бизнес логику из сущности в application слой - внезапно сущность станет анемичной. А это опять не DDD.

А ну и собственно я не могу разместить нативный SQL в application слое. Это специфика хранилища, а абстракция над хранилищем - это репозиторий.

Вот у меня было так

APPLICATION слой (AllocateInventoryLevelUseCase)

public async Task ExecuteAsync(AllocateInventoryLevelRequest request, CancellationToken cancellationToken)
{
    if (request is null)
        throw new ArgumentNullException(nameof(request));

    request.Validate(new AllocateInventoryLevelRequestValidator());

    var inventoryLevelIds = request.Items
        .Select(e => e.InventoryLevelId)
        .Distinct()
        .ToArray();

    var inventoryLevels = await _inventoryLevelRepository.GetByIdsAsync(
        inventoryLevelIds, cancellationToken);

    foreach (var item in request.Items)
    {
        var inventoryLevel = inventoryLevels.FirstOrDefault(e => e.Id == item.InventoryLevelId);
        if (inventoryLevel is null)
        {
            throw new InvalidOperationException($"Inventory not found with Id: {item.InventoryLevelId}");
        }

        inventoryLevel.Allocate(
            item.Quantity,
            item.ShipmentId,
            item.ShipmentLineId,
            request.IdempotencyKey,
            request.Reason);
    }

    await _unitOfWork.SaveChangesAsync(cancellationToken);
}

DOMAIN слой (InventoryLevel)

public void Allocate(int quantity, long shipmentId, long shipmentLineId, string baseIdempotencyKey, string? reason = null)
{
    var operation = new InventoryLevelOperation(
        Id, baseIdempotencyKey, OperationType.Allocate,
        quantity, shipmentId: shipmentId, shipmentLineId: shipmentLineId,
        reason: reason);

    CheckIdempotencyKey(operation.IdempotencyKey);

    if (quantity <= 0)
        throw new ArgumentException("Quantity must be positive.");

    if (Available < quantity)
        throw new InvalidOperationException("Not enough available inventory.");

    Allocated += quantity;
    Available -= quantity;

    ValidateNonNegative();
    AddOperation(operation);
}

А стало так

APPLICATION слой (AllocateInventoryLevelUseCase)

public Task ExecuteAsync(AllocateInventoryLevelRequest request, CancellationToken cancellationToken)
{
    if (request is null)
        throw new ArgumentNullException(nameof(request));

    request.Validate(new AllocateInventoryLevelRequestValidator());

    return _unitOfWork.DoRetryAsync(
        ct => _inventoryLevelRepository.AllocateAsync(
            request.IdempotencyKey, request.Reason, 
            request.Items.Serialize(), ct),
        ct => Task.FromResult(true),
        IsolationLevel.RepeatableRead, cancellationToken: cancellationToken);
}

Слой INFRASTRUCTURE (InventoryLevelRepository)

public Task<int> AllocateAsync(string idempotencyKey,
    string? reason, string itemsJson,
    CancellationToken cancellationToken)
{
    return _dbContext.Database.ExecuteSqlRawAsync(@"
        CALL AllocateInventoryLevels(
            {0}, -- pIdempotencyKey
            {3}, -- pReason
            {4}  -- pItems (JSON)
        );",
        [
            idempotencyKey,
            reason ?? (object)DBNull.Value,
            itemsJson
        ],
        cancellationToken);
}

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

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

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

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

когда заходит речь об оптимизациях

А зачем вам такие оптимизации? Даже крупные интернет-магазины нормально работают с логикой в приложении. 420 заказов в секунду это довольно много, не в каждом магазине столько есть. В большинстве случаев важнее поддерживаемость кода.

Вообще у вас какая-то слишком большая разница получилась, в 20 раз. Мне кажется, так не должно быть. Можете выложить на GitHub код микросервиса и код процедуры? Интересно глянуть.

У нас SaaS, у клиентов по 200-300-500 тыс заказов в день, и SLA на обработку в течение 2-3 часов. 20 rps это прям на грани, нужен запас, 50-100 rps хотя бы. И вообще конкретно эти нагрузочные тесты - слишком изолированы. Думаю в реальности там не будет 420, потому что сервер БД будет занят еще много чем другим.

Вот тут https://habr.com/ru/articles/955714/comments/#comment_28961570 я писал детали. Там были еще другие оптимизации и с тех пор я еще кое-что подкрутил. Стало стабильно 400-420 даже с проверкой остатка в конце

Кидайте логин github в личку, дам доступ.

И собственно выше в своих коментах я опубликовал часть кода. Как было (20rps) и как стало (400 rps)

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

А другой мастер, Влашин, приведёт примеры кода на ФП. Ну и что?

Так изначальное утверждение было - что DDD это вообще не про код, а чисто про проектирование. Я решил перепроверить по книге. Какие примеры там были таки и привел.

Уже при сравнительно небольших объемах кода чистая архитектура становится неповоротливой, в неё сложно вносить серьезные изменения и какие-то неучтенные проблемы на первых этапах или новые вводные просто начинают рано или поздно ломать или дико усложнять проект. Код динамичен, а порой, очень динамичен и, имхо, архитектура кодовой базы должна быть более восприимчива к этому. С этой точки зрения, vertical slices проще в управлении. Слои внутри слайса можно настраивать исходя из потребностей и менять их в последующем гораздо проще.

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

Мб какие-то аргументы подъедут?

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

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

Кстати, анемичными и рич бывают не только энтити, но и тимлиды. И чем анемичнее тимлид, тем больше приколов в коде.

Звучит как что-то интересное! Можно подробнее?

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

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

Когда инструмент становится самоцелью, это превращается в проблему.

Не соглашусь с автором с формулировке причины.

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

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

аргументы: это правильно с точки зрения ООП, это предотвращает создание невалидных состояний бла, бла, бла и + еще 100500 аргументов. Сами придумаете, суть и так понятна

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

Проблема в том, что никто ни о чем не может, договориться.

Глупый вывод.

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

Если у вас требуется чтобы объект всегда защищал свою валидность, то богатая модель. Если инварианты плохо мапятся на вашу модель, то инварианты/ правила должен обеспечить кто-то ещё, например сервис. Если нет постоянного правила "всегда", а правило зависит от use case, то правило только в сервисе и выгоды богатой модели нет.

Можно посмотреть на это с другой стороны и применить всеми любимую функциональщину. Что-то типа цепочки:
контроллер -> OrderRequest -> бэкенд (ValidOrder | InvalidOrder) -> (SavedOrder | OrderSaveError | SomeOtherError) -> опять контроллер -> (паттерн матчинг по результату)

Инварианты здесь находятся в сигнатурах методов, участвующих в цепочке процессинга. Можно ещё модный railway oriented programming имаджинировать)

В функциональном подходе:

Тип гарантирует инвариант (например, ValidOrder может существовать только если прошёл валидацию)

Smart constructor проверяет правила при создании: createOrder: OrderRequest -> Result<ValidOrder, ValidationError>

Функции перехода сохраняют инварианты: processPayment: ValidOrder -> Result<PaidOrder, PaymentError>

Ответственность распределена:

  • Компилятор следит чтобы нельзя создать невалидный тип в обход конструктора

  • Конструкторы/функции валидации проверяют бизнес-правила

  • Railway-цепочка не даёт пропустить обработку ошибок

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

В ООП с богатой моделью та же логика была бы в конструкторе/методах класса Order

Если у вас требуется чтобы объект всегда защищал свою валидность

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

Ты еще не начал писать свой код, а он го*но

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

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

Во во, вам в секцию "свидетелей rich моделей" но заходите только предварительно надев каску, ребята там нервные :)

Шоколад может есть себя, чужим ртом, это нормально, если это не разрешить, то увеличивается риск создания God object :) В итоге, на вопрос, что делает этот класс, тебе отвечают - забей, это просто класс, смотри методы :)

В ООП сложное/искусственное понимание что объект за что-то "отвечает", что приводит к странностям. Классика споров о богатых моделях.

Проблема ООП тут в смешении:

  • Данных (у кота есть желудок)

  • Поведения (кот ест)

  • Взаимодействия (кто кого кормит)

В итоге получаем философские вопросы: cat.eat(food) или food.beEatenBy(cat) или owner.feed(cat, food)?

В функциональном подходе проще:

type Cat = { stomach: Stomach }

type Food = Chocolate | Fish

let feed (food: Food) (cat: Cat) : Result<Cat, FeedingError> = ...

Никаких вопросов "кто за что отвечает". Есть данные и функции их трансформации.

Поэтому Влашин и показывает — можно делать DDD без этих мук выбора "в какой класс положить метод". Домен выражен типами и функциями, а не распределением ответственности между объектами

Мне больше нравится ООП'шный кот: cat.eat(food), - поскольку и до и после это все тот же самый кот.

food.beEatenBy(cat) выглядит отвратительно - не хочу смотреть на еду после того как она была проглочена котом.

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

Смешно, ага.

Ничего, работать с суперпозицией вполне удобно. Легко пробрасывать ошибки наверх. А обрабатывать ошибки наверху, в контроллере / апи

Мне нравится подход в ECS, когда у нас есть сущность - это буквально что угодно с набором компонентов, пускай у нас будет "кот" у него есть мурчало - это будет Purr, еще будет система, которая будет обрабатывать всех мурчал PurrSystem.

И теперь у нас все сущности с компонентом Purr будут обрабатываться в системе PurrSystem. Всё по канону - Entity только набор компонентов, Component только набор данных без логики, System только набор логики без данных.

Жалко, что к энтерпрайзу это сложно применить и отлаживать сложно.

public struct Purr : IComponent 
{
    public uint Tone;
    public uint Volume;
}
public class PurrSystem : QuerySystem<Purr> 
{
    private readonly ISoundService _soundService;

    // ...

    public void OnUpdate() 
    {
        Query.ForEach(MakeSound)
    }

    private void MakeSound(ref Purr purr, Entity entity) 
    {
        if (entity.IsNull || entity.HasTag<Mute>)
            return;

        _soundService.Sound(..., purr.Volume, pure.Tone);
    }
}

а еще и потому что у меня с этого конкретно горит.

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

Поэтому внести свою лепту я просто обязан

Круговорот срачей в интернете.

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

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

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

Сколько написано интернет магазинов, но есть ли явный и общепринятый шаблон написания магазина?

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

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

С автомобилями - да. 4 колеса диаметрами от 12 до 20, двс, дисковые тормоза через 2х контурную систему через гур на педали, руль это колесо, а не штурвал, управление педалями и ручкой КПП, 2-3 зеркала, ну вы поняли...

Конструктор может менять и добавлять многое, но чаще ему не нужно и он может опереться на общую стандартную практику. (Добавить АБС, но система будет такая же)

У нас же вечно трициклы с тврд и управлением рычагами или корабли-поезда на бурлаках, да архитектурно продуманные (ну хотелось бы), сколько мы тратим на них времени и сил?

. 4 колеса диаметрами от 12 до 20, двс, дисковые тормоза через 2х контурную систему через гур на педали, руль это колесо, а не штурвал, управление педалями и ручкой КПП, 2-3 зеркала, ну вы поняли...

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

На этом уровне для интернет-магазина сейчас даже фреймворк не нужен - любая LLM напишет код для абстрактной витрины с корзиной минут за 15. Но вопросы безопасности, надёжности, бухгалтерского и складского учета, обработки персональных данных и т.п., что требуется в реальной жизни, оставляем за скобками.

И что за фреймворки для интернет магазинов?

Shopify, BigCommerce, WooCommerce и т.п.

Интересная тема!


Ищу ответ на один простой вопрос - а где границы? До какого момент "нечто" это доменный объект, с какого момента он сервис и когда становится доменом?

Доменный объект очень удобная штука, не надо думать над композицией, можно свалить всю логику в доменный объект и назвать это rich model. Но во время длинного пути превращения доменного объекта в целый домен, все мелкие доменные объекты, волшебным образом превращаются в DTO без поведения. Поведение само собой "уезжает" в доменные сервисы. Имхо SRP на "длинных дистанциях" само принуждает себя соблюдать...

На практике действительно часто так: начинаем с Order.calculateTotal(), а через год там уже 20 стратегий расчёта, и метод переезжает в PricingService.

Но граница довольно чёткая:

  • Доменный объект — инкапсулирует инварианты своего состояния (Order не может быть с отрицательной суммой)

  • Доменный сервис — когда нужна координация нескольких агрегатов или внешние правила (расчёт скидки по истории клиента)

Проблема rich model не в том, что логика в объекте, а в попытке впихнуть туда ВСЮ логику. Order.ship() выглядит нормально, пока не понимаешь что для доставки нужны склады, курьеры, маршруты, то есть это не его логика...

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

Нужно искать баланс:

  • Проектирование: намечаем основные bounded contexts, не переусложняя, оставляя возможность для эволюции

  • Рефакторинг: когда код "подсказывает" — вот тут граница просится

Rich domain model популяризовал Мартин Фаулер в "Patterns of Enterprise Application Architecture" (2002), как противопоставление Anemic Domain Model.

Почему именно rich:

- Исторический контекст — в 90-х/00-х после волны процедурного кода с отдельными структурами данных, ООП обещало "объединить данные и поведение"

- Transaction Script был нормой — процедуры работающие с тупыми DTO

- Rich model казалась элегантным решением — инкапсуляция, сокрытие, всё в одном месте

Фаулер продвигал это как "правильное ООП", где объекты "умные" и сами себя защищают. Эванс подхватил идею в DDD.

Но время показало что это работает только для простых случаев. Как только появляется сложная координация — rich model начинает трещать по швам.

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

"создайте один документ" - не работает. Вы создаете этот документ,
потом в него предлагают правку, а пол проекта уже написано на его основе,
и создаются задачи на переписывание старого кода, они не успевают реализоваться и на 10%,
как вносятся новые правки, и потом мы видим похожую ситуацию как описал автор,
только разбитую не разработчиков, а на эпохи (с такого-то года эпоха active record, а с такого-то эпоха DDD и т.д.)

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

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

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

"Это то, что Мартин Фаулер называет «преждевременной оптимизацией» архитектуры." - ссылка на статью описывающую оптимизацию как производительность, вообще не о том.

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

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

"Сможет ли новый джун разобраться в этом коде без посторонней помощи" - на этот вопрос нет ответа. Понятность кода субъективная.

Sign up to leave a comment.

Articles