Обновить

Комментарии 21

Не очень понятно, что вы предлагаете. В принципе все делят на слои, а как - это уже "все мужчины имеют свой способ избавления от последней капли".
Понятно выделение слоя данных, а вот с бизнес-логикой не все понятно - где границы связности сущностей?! База в том, что сущности без связности вообще не работают. А если связывать в логике, то голанг видит зацикленность импортов. Кто-то разбивает через интерфейсы, это весьма тяжело поддерживать, когда много сущностей связано с многими сущностями.
При этом не вижу ценности делать "тупые контроллеры" - я обнаружил, что этот слой очень ценен, так как обладает особенной логикой (https://habr.com/ru/articles/901938/). Например: http-обработчик проверяет доступ через jwt-токен, и выдает http-ошибку, а обработчик из очереди не будет проверять доступ перед исполнением, и выдает ошибку только в логгер.
И такой подход юзаю уже год - код легко поддерживать, изменения вносить очень удобно. Пока кризиса не предвижу.

"где границы связности сущностей?!" - не понял ваш вопрос. Если вы о моделях (DTO), то общие живут в отдельной папке internal/models не общие - соответственно в <layer>/models. Что вы подразумеваете под "связывать в логике"?
Моя идея (не моя изначально конечно) - вся бизнес-логика - только в слое service. Проверка доступов очевидно не относится к бизнес-логике. Бизнес-логику (именно "бизнес") лучше не размазывать по слоям а сосредоточить в слое service. Тогда и понимать код и сопровождать будет проще. Ну это вроде как очевидный тезис...
Например: репозиторий выполняет только атомарные функции с одной таблицей - читать/писать/изменять/удалять. Если нужно создать запись в таблице "заказы" и при этом изменить запись в таблице "оплаты" - делаем один метод "СоздатьЗаказ" в слое service, который в транзакции делает два обращения к репозиторию. Но именно в слое service - я об этом.
Если же у вас возникает зацикленный импорт - верный признак ошибочной архитектуры.

Собственно если в двух словах о том, что я предлагаю: Объекты из верхнего слоя могут обращаться к объектам нижнего слоя, но не наоборот. Нижележайщий слой относится к тому что выше - как сервер к клиенту. Клиент может вызывать методы сервера, но сервер не может вызывать методы клиента. Таким образом, вызовы распространяются всегда в одну сторону "сверху вниз", что исключает зацикленные ссылки между слоями. Порядок слоев сверху вниз: "api" -> "service" -> "controller". Всё. В этом суть моего предложения.

Модели DTO весьма странные штуки, которые, имхо, появились в go из другого языка. В go стоит передавать данные как аргументы. Но когда полей много, и их порядок сложен, то возникает соблазн объединить в один объект. Норм если точечно, но если юзать везде, то вдруг оказывается, что их также надо поддерживать. И это горькая пилюля.
Пример зацикленности - организация и рабочая группа как часть. Организация знает и спрашивает вложенные рабгруппы. Но в рабгруппе хочется узнать про организацию, и кто ее директор. И "в лоб" уже цикл. Разруливается, но если таких много - персона и участник организации и туда-обратно. Вот про такие границы между сущностями я думал.
Логика сверху-вниз - это обычное явление. Есть варианты с тем что верх и что вниз - у кого-то это снизу-вверх, но тождественно. И тут есть споры - можно ли сверху-вниз стучаться, минуя слои - из верхней логики сразу в слой данных, например. Если упираться и последовательно проксироваться через слои, то при росте количества функций выглядит весьма уродски.
Транзакции тоже создают вопросы. ИМХО, должны быть на верхнем уровне (все последующие у меня принимают универсальный интерфейс), потому что наверху "оркестратор" решает: удачно закоммитился, то создаем сообщение об обновлении. Если транзакция ниже слоем, то возникает сложность вернуть в предыдущее состояние.

Ну моя статья не про разработку структуры данных ). Вы пишете о зацикленности в структуре данных (моделях, таблицах БД). Это отдельная тема.
Стучаться минуя слои - нехорошо, ИМХО. Лишние три строчки кода на вызов - не такая большая издержка чтобы ломать идеологию. Ну и как аналогия - клавиатура ПК не может обратиться к жесткому диску, минуя процессор ))
Есть такая штука Теория разбитых окон. Одно окно разобьешь - со временем все повыбивают ))

Уточню, что не структуры данных. А цикл импортов. Сущности великолепно себя чувствуют по отдельности - разные ветки и ок. А есть бизнес-слои "одного уровня", где они встречаются. И когда два слоя entity стучатся друг к другу - цикл. И не получается дублировать структуры, как в typescript - в го это разные структуры. Придумываются всякие DTO, которые идентичны оригиналу, но есть экспортируемые поля через методы (или дубликат структуры с преобразованием 1-к-1) - вот это костыль, имхо.
Я порешал тему, подняв логику на обработчики - вообще 0 циклов. И хорошо читается код - например, всё про патч объекта ушло в http-обработчик. Вроде как не логично, но работать сильно проще. Оказалось, что запрос через очередь не будет пересекаться с запросом от сайта - разная логика и разные входящие данные. Потому за год не было конфликта в логике.

К сожалению, я не понимаю о чем вы говорите ((
"два слоя entity стучатся друг к другу - цикл" - какие слои entity? Зачем и как стучатся?... Извините.

Entity/use_cases - это популярные слои. Походу я напутал. Use_cases решает "сложные" операции - типа "умные". И может вызывать entity и db. И entity - слой только про одну сущность. Типа здесь должно быть просто CRUD-операции, но внезапно разрастается из-за логики проксирования - нельзя в db минуя entity. И разделение на модули выше уровнем - я такое видел.
И вот use_cases умеет всякие штуки, типа запрашивать данные из связанных сущностей. И один модуль запросто встретится с другим - функция из организаций вызывает список рабочих групп другого модуля. А рабочие группы вызывают полномочия участника в организации на переименование.
Логично выделить такие "сложные" функции в отдельный слой, который может. И это получается супер слой, который чересчур сложен. И выше пишу - этот супер слой отлично распиливается в обработчики, но уже не тупые.

То что описано в первом абзаце - и есть криво спроектированная архитектура, насколько я понимаю. То что используется в разных слоях/сервисах/пакетах, должно быть вынесено в отдельное место.
И если в use_cases есть несколько сервисов/пакетов, которые потенциально могу обращаться друг к дружке - это косяк, безусловно. Нужно выделить эту самую общую сущность из них.
Но это уже отдельная тема, ИМХО. Про более низкий уровень архитектуры. И да, она важная.
Сталкивался с таким. Когда в слое service (use_case) джун сделал тупо несколько CRUD-сервисов. Пока были обращения к одной таблице внутри метода - всё было ок. Типа записать/прочитать таблицу users... Ну а потом стало весело )))

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

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

А есть бизнес-слои "одного уровня"
Как это возможно?... Бизнес слой - это абстракция (папка service) и он один...

Думаю, что зависит от назначения приложения. У меня сервер обслуживает сайт. И умеет всё с данными - это бизнес. А из сервисов там cron, рассылка и sse...

Нет, я с вами не согласен. Слой use_cases это абстракция. И она одна в любом приложении. В случае с Go - папка/пакет. А уж внутри этого пакета может быть сколь-угодно сложная иерархия папок/пакетов, но они уже не называются слоями - это содержимое общего слоя use_cases (в моём случае я назвал его "service", но это не играет роли)

Потому я вас и не понимаю, видимо )) Под словом "слой" вы подразумеваете другое.

Мне кажется, кто тут ключ именно в

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

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

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

Какое направление зависимостей, в итоге получается, между слоями, в предложенном подходе?

Я добавил диаграмму в статью.

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

Я писал, что нейминг у меня спорный - не обращайте внимания. Контроллеры тут - про другое совсем. По поводу зависимости:
Когда ваша программа должна издать звук, вы дергаете у интерфейса аудиокарты например метод Beep(). Но аудиокарта не может обратиться к программе. Так и тут. Программа - слой service (use case), аудиокарта - слой contoller. По аналогии с компом, в слое контроллер - контроллер HDD, контроллер аудиокарты, контроллер видеокарты и т. п. Это слой "исполнителей", которые выполняют простые команды - запиши строку в БД, запиши сообщение в очередь NATS. Ну как-то так...

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации