Про применение термина Low Coupling
С переводом терминов Low Coupling и High Cohession есть путаница. Чтобы её избежать - я буду применять эти термины без перевода.
Кратко
SOLID улучшает coupling, но ухудшает читабельность
Для большинства проектов читабельность важнее связности
В начале разработки проекта делайте упор на читабельность и ясность
Как выглядит SOLID система
Давайте воссоздадим небольшой кусочек программы, который будет максимально приближен к SOLID. Этот пример я взял из курса Роберта Мартина по архитектуре (программиста, который и собрал вместе принципы SOLID).
Вот с такого минимального набора классов мы начинаем. Контроллер работает с http. Он достаёт данные из запроса, создаёт типизированную DTO и передаёт её в UseCase. Последний является точкой входа в ту часть приложения, которая не знает про http.
Давайте применим к этой структуре все принципы SOLID. На выходе мы получим вот это:
Что здесь происходит? Рассказываю. Controller
использует UseCaseFactory
, чтобы создать UseCaseActivator
. Потом он использует RequestBuilder
, чтобы заполнить RequestDTO
. Так как DTO находится на стороне UseCase'а - UseCaseActivator
принимает не её, а вырожденный интерфейс Request
(интерфейс маркер). Который попадая в UseCase
downcast'ится к RequestDTO. И вот UseCase
начинает свою работу.
Я не буду разбирать, почему структура именно такая, и как она соотносится с принципами SOLID. Если хотите, то посмотрите это в оригинальном курсе (Clean Coders: Clean Code - эпизоды 7, 14 и 18. Есть на торрентах).
Будьте уверены, что увидев эту структуру у себя на проекте, вы подумаете, что это шутка по типу FizzBuzzEnterprise. Но нет. Это 100% SOLID структура от человека, который собирал и представил миру SOLID принципы.
Здесь я хочу донести одну мысль. Система, полностью построенная по SOLID, обладает отвратительной читабельностью. Это не значит, что принципы SOLID не работают. Если вы в 20 человек пишете desktop приложение на Java, которое можно расширять с помощью jar файлов - без SOLID можете даже не начинать. У меня есть опыт работы с командой, которая решила положиться на структуру фреймворка, а не на SOLID, когда писала свой монолит. Каждые несколько недель мы посылаем им bug report'ы, потому что на их стороне сломалось что-то, что ранее работало. Моя мысль лишь в том, что SOLID ухудшает читабельность. И про это написана вся эта статья.
Какую цель преследует SOLID
Роберт Мартин считает, что главным свойством архитектуры является "возможность откладывать принятие решений как можно дольше". В пример приводится его проект (Fitnesse), в котором они откладывали реализацию реляционной базы данных. А к концу разработки поняли, что лучшим вариантом будет хранение данных в обычной файловой системе. Откладывание решения о внедрении реляционной СУБД позволило им найти более производительный вариант хранения без потери в функциональности. Принципы SOLID работают в том же направлении. Они позволяют откладывать решения, а так же создавать структуру, в которой старые решения не будут изменяться под влиянием новых.
Так же одним из частых обоснований при объяснении принципов SOLID является возможность не перекомпилировать части приложения. Например при изменении компонента, связанного с базой данных, вам не нужно перекомпилировать компоненты предметной области, так как зависимости между ними инвертированы (БД зависит от domain, а не наоборот).
А теперь представьте, то вы разрабатываете web-приложение. В качестве языка вы выбрали PHP. В качестве framework'а - Laravel. И у вас точно будет MySQL или Postgress.
Ваш язык - интерпретируемый. Значит проблемы перекомпиляции у вас нет. SOLID и Clean Architecture говорят вам, что вы должны абстрагироваться от framework'а и СУБД. А значит вы не сможете использовать возможности фреймворка из классов бизнес-логики. А так же не сможете делать SQL запросы из UseCase'ов. Для всего этого вам понадобятся абстракции и инверсия зависимостей.
Вы защитили себя от изменений СУБД, framework'а и перекомпиляции. Но ни первое ни второе скорее всего никогда не изменится. А интерпретируемый язык решает все проблемы, связанные с перекомпиляцией. Вы получили в 3 раза больше файлов. При этом решив несуществующие на вашем проекте проблемы.
Так получилось, потому что SOLID нацелен на low coupling. Проблемы, которые он решает, появляются на больших и сложных проектах (>100'000 строк кода, а бизнес логика имеет много инвариантов). И в них он безусловно полезен. Но если ваш проект проще и меньше, есть много других, более полезных принципов архитектуры.
Свойства программной системы
Я предлагаю отслеживать у программ 3 показателя:
Low Coupling - как разные компоненты системы влияют друг на друга. Если при изменении в одном модуле вам приходится так же вносить изменения в большое кол-во других модулей, то у вашей системы high coupling. Каждое изменение требует больше времени, и может сломать те места, которые концептуально не связаны с изменённым кодом;
Читабельность - насколько легко программисту понять, что делает программа. Хорошие абстракции, унификация терминов и простой поток передачи управления делают программу простой для понимания. Программисту не приходится прыгать из файла в файл. Он точно знает, в какой файл пойти, чтобы исправить какое-то поведение. А читая код строка за строкой он будет понимать, в каком состоянии сейчас находится система, и как та или иная команда это состояние меняет;
Производительность - насколько быстро ваша программа работает и как мало памяти она использует. Чем быстрее работает приложение и чем меньше памяти она использует - тем лучше.
Улучшая один показатель мы непременно ухудшим 2 других. Это можно отобразить на следующей схеме:
Ваша программа будет точкой на этой схеме. Она может находиться в углу с производительностью, если вы делаете высоконагруженное приложение. Может находится справа, между читабельностью и low coupling, если вы делаете обычный CRUD. Может быть строго вверху в low coupling, если это большой модульный монолит.
На такой схеме легко понять, что делают с программой принципы SOLID. Они утягивают её от производительности и читабельности в сторону low coupling. Про это был большой спор между Робертом Мартином и Кейси Муратори (да, они спорили в markdown файлах на github, что вы им сделаете).
Я считаю важным запомнить эту закономерность. SOLID улучшает low coupling, но ухудшает читабельность и производительность. Не во всех приложениях нужен low coupling. А значит при их разработке не имеет смысла ухудшать остальные два показателя и сильно заострять своё внимание на следованию принципам SOLID.
С чего начинается читабельность
Предположим вы проанализировали своё приложение, и поняли, что читабельность для него важнее, чем low coupling. На каких принципах вам основывать свою архитектуру? С чего начать?
Готовые абстракции. В вашей сфере уже есть готовые паттерны для решения задач. Начните с их использования. Возьмите те, что предлагает ваш фреймворк. Вставляйте их названия в имена файлов. Например в web-разработке устоявшимися абстракциями будут: страницы, контроллеры, репозитории, запросы, ответы и т.п.
Словарь терминов. У бизнеса, для которого вы делаете программу, уже есть устоявшийся набор терминов. Например, для авиакомпании это будут рейсы, пассажиры, билеты и т.п. Составьте из них словарь и обновляйте его по мере работы. Используйте эти термины в именах файлов и классов. Избегайте программистских терминов в именах классов, отвечающих за бизнес-операции.
Транзакционный сценарий. Организуйте свою программу на основе транзакций. Это функции, которые либо выполняются полностью и изменяют состояние системы, либо не завершаются и, как следствие, не должны изменять состояние. Такая функциональность есть в реляционных СУБД. Перенесите эту идею в свой код.
Подробнее про читабельность и организацию кода вокруг неё вы можете узнать из книг:
"Изучаем DDD - предметно ориентированное проектирование" - Влад Хоносов
"Код, который умещается в голове" - Марк Симан
Заключение
Я могу назвать себя адептом чистого кода, чистой архитектуры, SOLID и всего, что с этим связано. И когда я начал работать - я приносил эти знания в команды, с которыми работал. Я всегда отвечал за архитектуру. И наши приложения были модульными, SOLID'ными; мы смогли избежать многих проблем, которые затронули наших коллег, не использовавших эти принципы.
Но чем дольше я работал над этими приложениями, тем чаще я стал замечать, что их low coupling начинает нас путать. У нас была большая фора по low coupling, которая заставляла нас прыгать от файла к файлу больше, чем было нужно. И тогда я пришёл к осознанию того, о чем написал в этой статье. Я перешёл от верхней точки треугольника "low coupling" немного ближе к читабельности. И очень доволен своим выбором.
Надеюсь я смог помочь и вам. Напишите в комментариях, что вы думаете про SOLID и про предмет обсуждения. Это моя первая статья на Habr. Я написал её за одну ночь, и решил стразу выложить, чтобы она не пылилась на моём диске, как много других недописанных мною статей. Если статья хорошо зайдет - буду писать еще. У меня много идей, которыми я хочу поделиться.
Комментарии (28)
lrmpsm53
03.01.2025 12:53Ваши примеры пытаются походить на solid. Но вы создаёте кучу бессмысленных абстракций
Абстракции создаются на уровне доменов (ddd) и уровне взаимодействия с внешним миром (базы данных, межпроцессное взаимодействие, http контроллеры и прочее)
Я бы советовал дополнительно изучить значение каждой из букв. Они хорошо описано в книге Мартина Чистый код
MrShnaider Автор
03.01.2025 12:53Если под "бессмысленными абстракциями" вы подразумеваете структуру кода "после применения SOLID", то эти абстракции не мои. Я взял их из курса Роберта Мартина по SOLID. Вот кадр из урока (кусочек из статьи слева-сверху):
Чтобы и контроллеры и UseCase'ы соответствовали принципам OCP и DIP действительно нужно столько дополнительных классов. Можно заменить
Factories
для интерактора используя инъекцию зависимостей и передаватьUseCaseActivator
как параметр вController
. Но сильно нашу схему это не упростит, и мой посыл остаётся актуален: "SOLID улучшает coupling, но ухудшает читабельность"
SadOcean
03.01.2025 12:53Мне кажется вы во многом правы, и это связано с более глубоким вопросом - как и зачем декомпозировать, что такое модуль и в каких целях он используется, а так же зачем нам, собственно SOLID и правила, какие у них границы применимости.
Иногда ответы очевидны, к примеру когда ты пишешь маленькую библиотеку функций.
Но зачастую нет - программы сложны, не всегда очевидно, где заканчивается одна часть и начинается другая, где должен быть расположен тот или иной код. Я очень часто встречал примеры чрезмерной декомпозиции, интерфейсы между родственными классами, сложная структура связанности без явной на то причины, шаблонные классы на разных уровнях, дублирующие друг друга, когда один является просто адаптером другого на следующий уровень.Вторая важная деталь - за архитектуру нужно платить. Создавая архитектурный шов "про запас", например с расчетом иметь возможность сменить реализацию источника данных позднее, мы должны заплатить за это - дополнительными интерфейсами и слоями, более сложными объектами.
Цена может быть невелика, особенно по сравнению с болью решаемых проблем, но ее нужно заплатить все равно.Как итог, помимо хороших рекомендаций, как делать что-то, зачастую нужны не менее хорошие рекомендации, как чего-то не делать, или в каком месте первые рекомендации не применимы.
Tishka17
03.01.2025 12:53Правильные абстракции призваны как раз улучшить читаемость. Они помогают понять как работает программа без изучения абсолютно всего кода. Отсутствие абстракций часто приводит к необходимости зазубрить весь код чтобы понять как работает небольшая его часть. Плохие абстракции делают и это недостаточным.
При этом я соглашусь, что добавление абстракций или дробление кода может ухудшить "отлаживатьмость" то есть поиск мест где были совершены ошибки, в частности если эти самые абстракции протекли.
MrShnaider Автор
03.01.2025 12:53Вы абсолютно правы. Как говорил Эдсгера Дейкстра: "Цель абстракции состоит не в том, чтобы добавить неопределенности, а в том, чтобы создать новый семантический уровень, используя который мы можем быть абсолютно точны".
SOLID не работает с абстракциями, и не добавляет "новый семантический уровень". Как описывает их Роберт Мартин: "SOLID - это принципы менеджмента зависимостей".
Посыл статьи как раз в этом. В большинстве проектов хорошие абстракции, имена и транзакционная целостность (а все вместе - читабельность) важнее, чем менеджмент зависимостей.
nv13
03.01.2025 12:53Нельзя сложные вещи сделать просто, а простые сложно делать незачем. У сложной низкая связанность будет улучшать читабельность, поскольку логика будет оперировать меньшим количеством сущностей, чем есть их имплементаций. Соответственно, если какая то сущность имеет и будет иметь лишь одну имплементацию, то зачем козе баян)
sshikov
03.01.2025 12:53Давайте применим к этой структуре все принципы SOLID.
Вот после этого места уже можно не читать. Давайте применим... зачем? Вы даже не соизволили сформулировать цель применения.
SOLID не самоцель, это инструмент. Инструмент для улучшения некоторых показателей кода. У вас показатели были плохие? Какие именно?
А если вы не знаете, чего хотели - вот ровно это непонятно что вы и получили в итоге.
Я исхожу из того, что вы применяли принципы просто "чтоб было". С поправкой на то, что это учебная статья, вы разумеется можете показать игрушечный код, но "давайте применим все принципы" - это все равно за пределами разумного. И кстати, достижение low coupling тоже не самоцель, связность кода - это просто показатель, который полезно измерять, и который в текущем (исходном состоянии) вас вполне может устраивать.
Иными словами - если код уже достаточно хорош, сопровождается и развивается сравнительно легко, понимается (текущей командой проекта) без проблем - нахрена его ломать и пытаться "улучшить" непонятно что?
mvv-rus
03.01.2025 12:53Да. Но есть нюанс. Кроме знаний немалую роль в поведении людей играет вера.
Принципы SOLID, как и прочие результаты теоретизирования, вполне могут быть учением, предметом веры. И адепты у этой веры находятся: либо их личный склад ума, либо их личный опыт не позволяют смотреть на эти принципы критически. И адепты несут эту веру в массы, не допуская сомнений.
И если такой адепт начнет влиять на руководство (или, хуже, сам станет руководителем), то эти принципы, да ещё и в максимально примитивном варианте (ибо чем примитивнее, тем проще верить), могут внезапно стать ключевыми показателями, по которым будет определяться кому сколько денег платить, кого повышать, кого вообще брать на работу.
piton_nsk
03.01.2025 12:53Я не буду разбирать, почему структура именно такая, и как она соотносится с принципами SOLID.
А было бы интересно, может я чего не понимаю, но выглядит как усложнение на пустом месте. Например, надо обязательно делать именно фабрики, хотя черт знает сколько времени есть DI из коробки? Вот если написано примерно вот так
Services.AddTransient<UseCase>(); public class Controller { private readonly IUseCase _useCase; public Repository(IUseCase useCase) { _useCase = useCase; } }
то это уже плохо, это не solid?
mvv-rus
03.01.2025 12:53хотя черт знает сколько времени есть DI из коробки
DI именно "из коробки", т.е., в самом языке и его стандартной библиотеке, обычно нет. В C#, в частности - нет. DI, если говорить конкретно, например, про ASP.NET Core, есть только в некоторых фреймворках, причем - в специально выденных местах. И во всех таких местах тамошний DI - это Service Locator в каком-нибудь другом месте внутри фрейймворка. То есть - та самая фабрика, просто ее
замели под коверубрали под капот, чтобы не отсвечивала и джунов не смущала.piton_nsk
03.01.2025 12:53DI именно "из коробки", т.е., в самом языке и его стандартной библиотеке, обычно нет.
Наверное про "из коробки" я преувеличил (в моем любимом ASP.NET Core есть), но в любом случае DI контейнеров готовых уйма.
То есть - та самая фабрика, просто ее
замели под коверубрали под капотЯ к тому и веду, что если у нас есть DI контейнер, который делает за нас кучу работы, зачем делать свои фабрики? Это не будет соответствовать принципам солида?
Я вот смотрю на "кадр из урока", это же какая-то жесть.
mvv-rus
03.01.2025 12:53моем любимом ASP.NET Core есть
Он там где есть, а где нет.
В шаблонах Web Host и Generic Host (базовых до версии ASP.NET 6) он при конфигурировании конвейера обработчиков (там это делалось в методе Configure Startup-класса) DI был, а в WebApplication он больше не с нами (хотя WebApplication - это тот же Generic Host, только хорошо запрятанный).
В методах конфигурирования конвейра обработчиков для базовых методах расширения IApplicationBuilder - Use, Run и Map - его нет ни на этапе конфигурирования, ни на этапе вызова обработчика, а в UseMiddlware, например, и на этапе конфигурирвания, и на этапе вызова обработчика он есть. В подсистеме маршрутизации (которая конфигурируется через IEndpointRouteBuilder) при вызове обработчика точки назначения маршрута в базовом варианте (где обработчик обязательно имеет тип RequestDelegate) DI, очевидно, не используется (ибо список параметров делегированного метода фиксированный), а в более позднем расширенном варианте Minimal API, где обработчиком может быть любой делегат, использование DI возможно, и так далее.Я к тому и веду, что если у нас есть DI контейнер, который делает за нас кучу работы, зачем делать свои фабрики?
Иногда приходится. Например, потому что время существования объекта, получаемого через DI, не годится для выполнения задачи. Например - в EF. Объект DbContext в контейнере там обычно конфигурируется с временем жизни запроса (т.е., он - один тот же самый на всё время обработки запрос), а параллельное обращение к себе из нескольких задач этот объект не допускает. Поэтому при попытке распараллелить обработку с использованием EF приходится получать через DI объект фабрики, получать экземпляры DbContext от фабри ки и самостоятельно следить за тем, какой использовать и за их временем жизни. Впрочем, MS там о нас позаботилась, и такую фабрику написала.
А очевидная альтернатива фабрике для такого случая - создавать DbContext напрямую - она явно не SOLID.Я вот смотрю на "кадр из урока", это же какая-то жесть.
Мартин, к сожалению, не познал вовремя мудрость, содержащуюся в ASP.NET Core ;-) Но у него есть уважительная причина: AFAIK ASP.NET Core тогда просто ещё не было.
Tishka17
03.01.2025 12:53SOLID вообще не требует DI, btw
mvv-rus
03.01.2025 12:53"Спасибо, Кэп"(с). Но предыдущий комментатор завел речь именно о DI как средстве реализации принципа инверсии зависимости, и я отвечал именно ему.
Farongy
03.01.2025 12:53Dependency Injection это внедрение зависимостей. Никакого отношения к Dependency Inversion не имеет. Используя Injection, ничего не мешает реализовать прямой порядок зависимостей. И Inversion можно реализовывать без Injection.
Dependency Injection это скорее способ IoC.
mvv-rus
03.01.2025 12:53Заниматься схоластикой и спорить за абстрактные принципы я даже не буду. Хочу лишь отметить, что автор исходного комментраия упоминал DI именно в контексте принципов SOLID.
MrShnaider Автор
03.01.2025 12:53Да, можно использовать DI. Мой ответ из другого комментария:
Чтобы и контроллеры и UseCase'ы соответствовали принципам OCP и DIP действительно нужно столько дополнительных классов. Можно заменить
Factories
для интерактора используя инъекцию зависимостей и передаватьUseCaseActivator
как параметр вController
. Но сильно нашу схему это не упростит, и мой посыл остаётся актуален: "SOLID улучшает coupling, но ухудшает читабельность"В данном примере предполагается, что
Factory
будет отдавать нам не толькоUseCaseActivator
, но иRequestBuilder
, так как их реализации используются одновременно. На схеме у автора это не показано (и у меня как следствие), что косяк. Но в видео-уроке это проговаривается в контексте.Так что
Factory
можно заменить на инъекциюUseCaseActivator
иRequestBuilder
. И все ключевые зависимости останутся без изменений
mvv-rus
03.01.2025 12:53Читабельность - насколько легко программисту понять, что делает программа.
Я всегда считал и продолжаю считать, что нет такой объективного показателя для текста программы, как читабельность. Читабельность есть вещь субъективная. Она, как и красота, в глазах смотрящего, то есть - в голове читающего код. И сильно зависит от того, кто этот код читает, от его знаний и опыта.
К примеру, увидев такой кусок кода -
int i,j; //... assign smth. to i and to j i^=j; j^=i; i^=j;
- человек, не знающий эту идиому, остановится и задумается - "что это было?", а знающий - отметит на автомате, что здесь были поменяны значения i и j, и продолжит читать дальше.
Так что оценка читабельности - она, в лучшем случае, качественная. Например, один коллега меня тут недавно уверял, что для него код, разбитый на множество мелких функций, раскиданных по всему тексту программы, более читабелен, чем код из одного большого метода, состоящего из последовательности логически слабо связанных блоков, а для меня - строго наоборот (если чо, я много читал исходники того же ASP.NET Core и намаялся раскапывать каждый раз, что там на самом деле делается). Думаю, для натренированного таким образом человека код в стиле SOLID - он, наоборот, оказывается более читабельным.
sshikov
03.01.2025 12:53Я всегда считал и продолжаю считать, что нет такой объективного показателя для текста программы, как читабельность.
Ну так я чуть выше про это же и писал. Если приложение пишется двумя людьми, и они оба высокой квалификации, то что понятно им и легко - вполне может стать сложно, если нанять еще человека с квалификацией пониже, и тем более - еще десять человек стажеров. Поэтому в том числе, применение принципов чтобы "улучшить" что-либо, сначала требует продемонстрировать, что сейчас что-то в коде плохо. Причем не абстрактно плохо, а именно для текущего проекта и команды.
Farongy
03.01.2025 12:53В вашем примере, если это вынести в функцию и обозвать её swap, то понятно будет и знающему и не знающему.
mvv-rus
03.01.2025 12:53Вопрос как лучше меня не интересовал. Обмен содержимого через три XOR был приведен как пример идиомы, которую знают не все, и которая, тем самым, делает читаемость субъективной.
PS А по жизни я этот прием первый раз видел в ассемблерной программе (исходный код IBM VM/SP), делать там обмен через вызов процедуры было бы слишком громоздко.
Dhwtj
03.01.2025 12:53Вторую картинку я бы реализовал на чистом ФП
// Тип для DTO (Объект для передачи данных) type RequestDTO = { payload: string }; // Создание DTO const createRequestDTO = (payload: string): RequestDTO => ({ payload }); // Тип для UseCase type UseCase = (requestDTO: RequestDTO) => Promise<string>; // Конкретная реализация UseCase const concreteUseCase: UseCase = async (requestDTO) => { const { payload } = requestDTO; // Симуляция какой-либо асинхронной операции await new Promise((resolve) => setTimeout(() => resolve(), 1000)); return `Обработана полезная нагрузка: ${payload}`; }; // Фабрика для создания UseCase (если это может понадобиться) const createUseCase = (): UseCase => concreteUseCase; // Чистая функция для обработки запроса const handleRequest = async (payload: string, useCase: UseCase = createUseCase()): Promise<string> => { const requestDTO = createRequestDTO(payload); return useCase(requestDTO); }; // Пример использования handleRequest('пример полезной нагрузки').then((result) => { console.log(result); // Вывод результата: "Обработана полезная нагрузка: пример полезной нагрузки" через секунду });
Возможность добавления нового обработчика сохранена. Абстракции не текут. Площадь соприкосновения частей кода минимальна.
ООП не умеет делать маленькие стабильные абстракции, он слишком конкретен и постоянно требует абстрактных прокладок между классами.
MrShnaider Автор
03.01.2025 12:53Обратите внимание, что в вашем примере функция
handleRequest
ссылается наcreateRequestDTO
, а та в свою очередь ссылается наRequestDTO
. НоRequestDTO
используется функциейUseCase
. А значит единственный способ упаковки, который не вызовет цикличные зависимости будет такой, как на этой схеме:В таком случае любое изменение внутренней структуры
RequestDTO
потребует изменение как минимумcreateRequestDTO
, что означает перекомпиляцию и перевыпуск компонента контроллеров (того, что слева от двойных линий). А именно этого мы пытались избежать, добавляя абстракции, связанные с интерфейсом-маркером (красный блок на второй картинке в статье).Поэтому если вас не волнуют вопросы перекомпиляции и перевыпуска компонентов - лучше вообще не плодить абстракции и остановиться на первой схеме из статьи. И не важно, в какой парадигме вы пишете. Если же вам важно разделить перекомпиляцию и перевыпуск компонентов контроллера и useCase'а, то вариант с абстракциями, как на второй схеме - это единственный способ.
А на счет ООП и ФП. Если в проекте есть полиморфизм, то для меня это уже ООП. Я не считаю, что классы == ООП. В конце-концов функция с замыканием нескольких переменных - это уже объект с одним методом. Если код организован на основе отправки сообщений, где отправитель не знает о внутреннем устройстве получателя - это ООП. Если код организован как конвейер по преобразованию данных - это ФП. Я выбираю парадигму исходя из задачи. Поэтому не могу поддержать вас в недолюбливании ООП, хотя и согласен, что с некоторыми задачами эта парадигма не справляется (как и шуруповерт не справляется с гвоздями)
av_in
03.01.2025 12:53Вы сейчас развенчали фанатиков Дяди Боба. Я и сам наблюдал обертки над готовыми абстракциями: efcore, di (mediador) и даже ILogger в Asp.NET. Но красный кружочек по центру зелёного треугольника на вашей схеме как раз опять отрицает все компромиссы. Нет, тем и сложна наша работа, что нужно искать идеальный баланс. И большой сложный монолит должен болтаться где-то посередине сомнительной области.
amazingname
03.01.2025 12:53Это очень интересная тема, и по моему мнению автор этой статьи скорее не прав, в том что SOLID ухудшает читаемость или вообще что-либо.
Первое - не стоит пытаться понять примеры авторов книг про SOLID буквально. В книгах всегда есть две беды: невозможность привести настоящий пример ввиду ограниченности размеров книги и терпения читателя и вместо этого всегда демонстрация концепций на примерах, где они сплошной овер-кил; и вторая беда - чтобы концепция прогремела где-то кроме академических кругов, концепцию в книгах по IT всегда доводят до абсурда с элементами вбрасывания сами знаете чего на вентилятор.
Двигаемся дальше. А что вообще такое SOILD? Отбросим лишнее: "L" - это про правильное наследование, которое всегда лучше неправильного, там не с чем спорить. I - про адекватные задаче интерфейсы, опять таки при разумном использовании там не с чем особенно спорить.
Остается S-O-D, которые в принципе все об одном и том же: программный код должен быть декопозирован таким образом, чтобы он мог развиваться в будущем с наименьшими затратами. И для этого есть по сути только одно средство: сделать код чуть более абстрактным и общим, чем это нужно в текущий момент. Эти 3 буквы поясняют с разных сторон этот принцип избыточной абстрактности:
O - центральное пожелание, чтобы классы были спроектирована так, чтобы их не требовалось переделывать, а только заменять или расширять (как этого достичь здесь не говорится, но в разных источниках используется два подхода к формулированию принципа);
S - одно из средств достижения O: если у нас есть две возможные причины для переделки кода в будущем, то не не должно быть класса, который нужно переделывать и по первой причине и по второй одновременно; если для класса есть такая необходимость - это признак того, что он возможно недостаточно декомпозирован;
D - второе средство достижения O: зависимость на абстракции подразумевает, что границы между классами/модулями должны быть достаточно абстрактными и ясными, что упростит переиспользование классов, замену классов как бонус - тестирование.
И подходим к главному, как это использовать.
Использовать SOLID нужно исключительно по назначению - как средство борьбы со сложностью кода при дальнейшем развитии продукта. А бороться со сложностью нужно конечно же там, где она есть.
Если вам нужно написать сайт с почти однослойной логикой (типа извлекли информацию, показали, изменили, сохранили) в котором есть контроллеры+сервисы+хелперы, то самый лучший способ это реализовать - действовать в рамках традиций и существующих фреймворков. Вся сложность здесь будет на стороне многочисленности требований к продукту а не в сложной архитектуре кода. Высасывать из пальца SOLID в "билдерах DTO" это пустая трата времени.
Но в какой-то момент задача может стать сложнее и тогда SOLID просто неизбежен и в этом случае он никогда не ухудшит читаемость кода или его производительность.
Попробую привести настоящий а не условный пример. Допустим, у нас есть некое хитрое хранилище данных организованное в SQL-ной базе, схему которого пользователь определяет динамически в UI с возможностью добавления хитрых связей между объектами, вычисляемых полей и так далее. Для начала мы пишем API, которое достает эту информацию и отдает в UI. На втором этапе оказалось, что нам нужно паблик REST API для этих же данных, а на третьем - что нужно еще и GraphQL API, а на четвертом мы уходим с SQL-ной базы на NoSQL.
Понятно, что можно написать последовательно три абсолютно разных макаронины кода для каждой из этих целей, а потом менять их все три на NoSLQ и понятно что это будет плохой код.
Выход здесь - уже на этапе когда мы еще не знаем что будет нужен REST API, GraphQL и NoSQL писать переиспользуемый код. Для этого код должен быть попросту несколько более абстрактным чем это нужно на первом этапе и принципы SOLID дают неплохую поддержку в решении этой задачи.
Т.е. нам нужно вычислять вычисляемые поля и для этого мы пишем классы, которые решают исключительно эту задачу. Так же оставляем возможность добавлять произвольный набор таких классов ничего не ломая в другом коде.
Нам нужно читать данные из хранилища, значит нужны классы, которые делают исключительно это.
Принцип хранения может поменяться (например, можно по разному хранить наследуемые сущности), тогда нам придется заменить один или несколько классов.
Может поменяться SQL-ная база - опять нам нужно будет заменить один класс.
Требования API могут поменяться (специализированное API для UI -> REST AP -> GraphQL), но у нас уже есть достаточно абстрактные классы, которые умеют фильтровать данные и извлекать связанные объекты. Значит в будущем мы просто расширяем этот набор классов, и так далее.
Все перечисленные особенности архитектуры не сделают код менее читаемым. Скорее наоборот, он станет проще для понимания, потому что в нем будут отражены более базовые и более очевидные концепции работы с данными.
Farongy
Думаю, что всё хорошо в меру. Любая идея, доведённая до абсурда, перестаёт быть хорошей и правильной. По типу - болезнь легче предотвратить, чем лечить и всякие ранние диагностики, чекапы, etc. это хорошо. Но ходить проверяться каждый день, кажется, что перебор.
MrShnaider Автор
Дополню ваш комментарий цитатой Роберта Мартина, на счет того, как не довести своё желание следовать принципам "до абсурда":
По моему мнению, лучшие способ не доводить идею до абсурда, это понимать что лежит в основе этой идеи, и какую проблему она решает. И оставаться приземленным, используя идею как полезный инструмент, а не как истину в последней инстанции. В принципе, об этом "приземлении" отчасти и была эта статья.
sshikov
Именно это вы и не продемонстрировали. Вы пытались применить все принципы к коду, который весьма вероятно не требовал применения ни одного. А если и требовал - то вы этого не показали, а сразу начали "применять". Если вы именно это хотели показать - у вас получилось так себе.
Вот именно. Я допускаю, что я невнимательно что-то читал, если так - ткните пальцем, где вы описали, какие проблемы были в вашем примере кода до применения принципов?