Pull to refresh

Comments 265

UFO just landed and posted this here
Я не писал в статье о том, что мы должны отказаться от сервисов, мапперов и прочего.
Я видел контроллеры с большим и кол-вом кода, разделив его на несколько контроллеров, проблема решается.
Спрятав говно в другое место, и обернув его для вызова в одну строку, ситуация ни как не изменится.

И статья описывает ситуации где силы вкладывают в то что б в контроллере было не больше чем 1 строка в каждом методе — тоесть делать и контроллера тупой враппер.
UFO just landed and posted this here
может будет много строк, а может и не будет. Это гадание на кофейной гуще. Если обработчик укладывается в простейшую декларативную логику, то вынос его в сервисы просто потому что так надо это лишняя работа. Я думаю автор об этом. Нет никакой проблемы, чтобы простые действия сначала писать в контроллерах, а потом вернувшись к ним за масштабными усовершенствованиями вынести их в сервис. Все должно происходить по требованию, а не ради фапа на идеальную архитектуру.
Спрятав говно в другое место, и обернув его для вызова в одну строку, ситуация ни как не изменится.
Ну, начнем с того, что если писать говно то его хоть как размазывай, а суть не изменится.

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

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

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

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

Личный опыт в подобных вопросах это все равно что «Синдром выжившего». И до тех пор пока вы не сталкнетесь с бизнес логикой уровня чуть выше чем «за что купил — за то продал» вряд ли удастся осознать зачем напридумали всяких там шаблонов, SOLID и написали несколько толстых книжек такие товариши как Макконел, Фаулер, Мартин.

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

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

Ну вот в данных примерах контроллер не тупо переадресует, а, средствами фреймворка, преобразует http-запросы во что-то более-менее нейтральное, независимое от протокола.

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

Потому что если сложность вашего приложения превосходит туду приложение, то ваши контроллеры превращаются в помойку.
эээм, вы не поняли что я говрю
Чем код с первой реализации лучше второй?
Я показал наглядный пример, была одна строка, под не логика
В чем выигрыш что ты спрятал все под одной строкой? (в контролере userService.Do....)
Меня тоже терзали эти мысли, но больше не терзают. Намучался называется…
У контролера и сервиса с бизнес логикой разные задачи. Задача контролера заключается в обработки запроса. Не важно это http или grpc. Для этого у него есть все необходимые данные. Тело запроса, параметры, куки и все остальное. Он их анализурует и решает что делать дальше. Сервис же обрабатывает бизнес задачу. Для этого у него есть свои инструменты. Это две разные абстракции. Смешивание двух абстракций в одном методе точно увеличивает его размер(нужно писать логику для двух сразу), что увеличивает вероятность что-то недопонять когда ты к нему вернешся. Если же используются отдельные сервисы, то будет фигурировать как минимум один метод с логичным названием, что существено упростит понимание и поиск это места в будущем(что наверно даже болие важно).

Сам все больше склоняюсь к использованию библиотек по типу MediatR. С ними искать и писать можно еще более точечно и весь контекст для выполнения задачи находится в одном месте. При этом исключается возможность случайно создать контролер с 10 зависимостями в конструкторе, что происходит почти всегда в особо больших проектах.
Рад что кто-то понял меня :)
Я к этому в статье и подводил, писать контроллеры на подобии хендлеров, то-есть тоже точечные
С медиатором контроллер выглядит
_mediator.Send(...)
Команда в основном биндится из тела запроса
UFO just landed and posted this here
таких полно, и во всех этих статьях контроллеры с одной строчкой кода
mediator.Send — опять таки простая прокся к нашей настоящей логике
UFO just landed and posted this here
При этом исключается возможность случайно создать контролер с 10 зависимостями в конструкторе, что происходит почти всегда в особо больших проектах.

Становится ли от этого легче? Теперь вместо 10 зависимостей у контроллера 1 зависимость медиатр и по конструктору нельзя даже предположить, что делает и чего не делает контроллер. Напоминает service locator, не находите?
Предположим, что вот вам нужно починить багу. Вы нашли роут с которым не все в порядке. Открываем контролер и видим следующий конструктор.
    public SomeController(
        ISearchResultManager searchResultManager, 
        IStorageManager storageManager, 
        IMediaManager mediaManager, 
        ISearchResultsExportManager searchResultsExportManager, 
        IElasticSearchManager elasticSearchManager, 
        IElasticSearchService elasticSearchService, 
        ISearchHistoryService searchHistoryService, 
        IAgentManager agentManager, 
        IElasticSearchOptionsCreator elasticSearchOptionsCreator, 
        ISearchResultCreator searchResultCreator, 
        IWorkInProgressManager workInProgressManager, 
        ICommonManager commonManager)
    {
        this.searchResultManager = searchResultManager;
        this.storageManager = storageManager;
        this.mediaManager = mediaManager;
        this.searchResultsExportManager = searchResultsExportManager;
        this.elasticSearchService = elasticSearchService;
        this.searchHistoryService = searchHistoryService;
        this.agentManager = agentManager;
        this.elasticSearchOptionsCreator = elasticSearchOptionsCreator;
        this.searchResultCreator = searchResultCreator;
        this.workInProgressManager = workInProgressManager;
        this.commonManager = commonManager;
        this.elasticSearchManager = elasticSearchManager;
    }


Вам понятно что делает следующий контроллер? Я бы сказал что он может делать что угодно. Ладно, посмотрим сам обработчик нужного роута, там одна строка:
searchResultManager.GetSearchesByAgentId(agentId);

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

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

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

UFO just landed and posted this here
Ваше предложение? Убить всех джунов, «не совсем професионалов с большим опытом работы» и индусов?
UFO just landed and posted this here
MediatR позволяет локализовать проблему. Пускай у нас будет один роут с 6+ зависимостями, а не 10 роутов с 10+, только из-за того, что они случайно оказались в одном контролере.
Первый, зачем было инициализировать все остальные зависимости? Все ресурсы на их инициализацию были потрачены в пустую

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

Ну, у меня логика простая — явное лучше неявного. Соответственно я вижу 10 зависимостей и понимаю:
1. Возможно, контроллер плоховато написан и его стоит переписать.
2. Да, у него 10 зависимостей, но у него нет 11 и 12.
В случае с медиатр вы видите красивое IMediatr, контроллер до сих пор делает кучу вещей, только это абсолютно не очевидно.
Про Service Locator:
Не совсем понял что имеется виду. Можете уточнить?

Есть такой паттерн, который предполагает что вместо того, чтобы брать зависимость от сервиса, вы берёте зависимость от «локатора», который умеет эту зависимость доставать. В итоге с точки зрения конструкторов (наиболее частый способ DI в моей практике) мы имеет IServiceLocator везде, но зато в каждом методе мы можем волшебно забрать себе хоть 10 сервисов.
Подробнее можно глянуть, например, тут.
Тут не будет никакого IServiceLocator. Все зависимости буду зарезовлены через DI во внутрь конструктора. Вот пример обработчика MediatorR:
public class Import : IRequestHandler<ImportRequest>
{
    public Import(ISomeRepositoty repository, ISomeService service)
    {
        // ...
    }

    public async Task<Unit> Handle(ImportRequest request, CancellationToken cancellationToken)
    {
        // ...
    }
}

Вот только сам MediatorR в качестве сервис локатора и выступает.

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

Во-первых, потому что все его зависимости статически декларированы (ну. обычно). Во-вторых, потому что его вызывает не пользовательский код, а ASP.NET.

В случае с медиатр вы видите красивое IMediatr, контроллер до сих пор делает кучу вещей, только это абсолютно не очевидно. 

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

Угу. И вы не видите, что контроллер отвечает за десяток бизнес-функций, хотя должен бы отвечать только за одну.

В случае с медиатр все очевидно, контролер не делает ничего связаного с бизнес логикой

Этого можно добиться и без него. Достаточно обернуть ваш хендлер из примера выше в абстракцию и брать зависимость в контроллере на неё, а не на ISomeRepositoty и ISomeService. Таким образом, контроллер будет явно в своём конструкторе говорить, сколько бизнес-операций он совершает. Если много — дробим.
На моей практике контроллеры с 5+ операциями встречались относительно редко, а если и встречались — эта проблема решается не тем, что мы просто скрыли весь код за медиатором.
Не холивора ради, я искренне не понимаю, почему скрыв код за неявной фабрикой хендлеров мы считаем, что он стал лучше. Сложность класса-то от этого не поменялась, он как делал Х вещей, так и делает их, только теперь об этом не узнать.
Напоминает service locator, не находите?

Не совсем понял что имеется виду. Можете уточнить?

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


Поступила задача, продолжая поддерживать REST-ish API, реализовать GraphQL API (пример). Как будете делать?

Добавлю GraphQLMiddleware и с небольшими изменениями моделей будут так же валидно обрабатывать запрос

Изменять модели под протокол клиента? Ох.


Да и конвертация из одного протокола в концептуально другой без крайней необходимости (адаптер к closed source) — это, как бы, попахивает. Уже вижу костыли.

тут приходят третий, четвертый и пятый интеграторы и последовательно заявляют: вебсокеты, TCP, UDP, GRPC, да хоть через AMPQ интегровать будем.

Ну сокеты уже реализованы и поставив один нагет мы запросто получаем «контролеры» для сокетов
Вы имеете в виду из-за Project Bedrock можно реализовать такие же «контроллеры» поверх любого транспорта?

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

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

А типичная реализация UserService так же имеет кучу лишних методов.
Флоу регистраци
Флоу авторизаци и тд

Подобные "сервисы" — это тоже антипаттерн.

Но ведь они есть, не везде используют CQS подход

И без CQS можно. Главное — отказаться от анемичных моделей.


А что где "есть" — я уж в курсе, я многое повидал. Хорошее редко увидишь.

Лично мне нравятся анемичные модели, чтобы entity не выглядел как зверь с тысячью зависимостей

Если тысяча зависимостей, то, скорее всего, нарушается SRP

Значит, по SRP, Entity должна быть анемичной. Она должна уметь только храниться в базе и всё, не должна иметь методов для обработки бизнес-процесса перемещения денег с одного аккаунта на другой, отправки оповещений по SMS и E-Mail, и т.п.

ActiveRecord, да, нарушает SRP by design. DataMapper — не требует от Entity уметь храниться, это отвественность ORM.


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

Да, UserService довольно сложен. Вообще AspNet Identity не самая прозрачная библиотека. Спасает только то, что есть код из примеров.
Я правда не понимаю, зачем оправдывать усложнение кода другим сложным кодом. От того что в nuget на пакете написано microsoft легче читать его не становится.

А когда в контроллере куча вспомогательного кода, и он перемешан из разных сценариев, оно просто не помещается в голове и я начинаю думать о том, что не контролирую происходящее.
А в чем проблема вынести часть логики в сервис, когда если в контроллере накопится куча вспомогательного кода?
Автор статьи хочет писать код в контроллерах. Вынесение кода в сервис или в хэндлер будет противоречить его основной идее.
Я то как раз за тонкий контроллер. И за инкапсуляцию логики в отдельных сервисах\обработчиках в зависимости от ситуации.
Повторюсь, если у Вас есть вьюшки, тогда в контроллерах лучше писать код только для View, а в сервисах писать переиспользуемые методы для получения данных для View. Но в текущих реалиях все чаще у нас API + SPA.
Автор статьи хочет писать код в контроллерах. Вынесение кода в сервис или в хэндлер будет противоречить его основной идее.
Автор не против вынесения кода в сервис. Идея не в том, чтобы весь код фигачить в контроллерах. Идея в том, чтобы выделять сервисы только при необходисти, а не по умолчанию.

А если по умолчанию считать, что http надо отделять от BL? Что код, который знает что-то об http не должен находиться в одном классе с BL?

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

UFO just landed and posted this here

… непонятно тогда, почему этот код все еще контроллер.

В .Net Http.post("login") не для работы с http используется?

Да, именно это хотел донести, спасибо! :)

Идея в том, чтобы выделять сервисы только при необходимости, а не по умолчанию.
Это хорошо, когда у проекта 1-2 разработчика. А если их 5, и у каждого своё мнение насчёт «необходимости». Тут лучше ввести правила, как code style, чётко им следовать, и не надо будет ломать голову: тут уже надо вводить сервис, или пока не надо (а это тоже время)
Да, именно это хотел донести, спасибо! :)
Я занимался сопровождением проекта, создатель которого выбрал как раз такой подход к разработке. Распухание контроллеров это первая проблема, но не единсвтенная. В какой-то момент от нас потребовали добавить поддержку очередей, но в коде «сервисов» оказалось много зависимостей от HttpContext, и обрабатывая сообщение из очереди они часто падали в не предсказуемых местах.
Почти все комментарии твердят о распухании контроллеров, а почему не кого не заботит что они оставили худой контроллер, и распухает BL?

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


Рекомендую ознакомиться с подходами:


  • Hexagonal Architecture,
  • Domain Driven Development,
  • CQRS.
Мой комментарий не про распухание класса, а про то что у контроллеров есть особенности которых не должно быть у сервисов. Использование HttContext, правила авторизации, маршрутизации и тд. А чистые сервисы могут иметь фишки которых не будет в контроллере, например быть обощенными.

Распухание бизнес-логики при усложнении бизнес-требований — это как раз нормально, разве нет? Хуже, если со временем распухание инфраструктурный слой без добавления бизнес-функций, просто чтоб было.


Если по теме — то ваш подход может быть вполне оправдан для проектов, время жизни которых заведомо меньше времени устаревания фреймворка. В иных случаях может оказаться, что бизнес-логику написали в коде windows forms компонентов (потому что зачем усложнять, выделяя BL?), и теперь дешевле все выкинуть и начать сначала. Аналогично, через 10 лет такую же брезгливость будет вызывать бизнес-логика в контроллера, прибитых гвоздями к допотопному ASP. Net Core 3.1.

UFO just landed and posted this here
UFO just landed and posted this here

Хорошая статья! Спасибо!
Бэст практисес в бизнес реалиях в большинстве случаев не работают.
В наших реалиях говнокод делает деньги. Особенно для энтерпрайз галёр. И такой подход тут — в самый раз. Для mvp некоторых тоже подойдёт. Только нужно покрывать тестами всё, чтобы можно было быстро отрефачить. :)

UFO just landed and posted this here
Контроллер нужен для того, чтобы преобразовывать HTTP-запрос в POCO и вызывать метод сервиса/команду, а затем возвращать результат в виде HTTP-ответа


Это он все делает сам, и код мы этот даже не видим. Мы уже достаточно хорошо абстрагировались от самого HTTP, но в принципе как я сказал, если у вас есть код для работы с вьюшками, или как вы сказали с http кодами и тд, то конечно стоит отделить контроллер и логику.
Я пишу такие API где принимаю модели и возвращаю модели, любые исключение ловит ExceptionHandler, то-есть все либо пройдет хорошо, либо вылетит в 500 ошибкой.
Если тип исключения домменный, то есть мой, я вывожу его пользователю
UFO just landed and posted this here
Я никогда так API не пишу.
Всегда принимаю модель, возвращаю модель.
По умолчанию всегда 200\201 код, исключения ловит exceptionHandler который может поставить другой код в зависимости от типа ошибки.
Все такие вещи настраиваю немного выше, ведь у меня есть пайплайн
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
А как настроили swagger чтобы он знал про коды в обработчиках исключений?
И еще на счет gRPC все что поменяется это класс наследник, вместо ControllerBase будет ServiceBase котоый генерится на основании proto файла
Кажется, что в микросервисах такой подход предпочтителен
И мне, но почему-то хотят чтоб все были как один, и большие сервисы с кучей оберток, и маленькие
Потому что микромонолиты
Привет! Спасибо за статью. Очень смело.

Вы доказываете, что механическое перетаскивание кода из класса А в класс Б не приносит пользы. И Вы правы!

Потому что в Вашем коде нет никакого BL. И все слои вместе взятые — это одно сплошное императивное редактирование ячеек БД через HTTP запросы. (Как и в большинстве проектов, которые я видел).

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

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

// я про эти строчки
fromUser.Balance -= amount;
toUser.Balance += amount;


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

> механическое перетаскивание кода из класса А в класс Б не приносит пользы

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

А с другой стороны — все-таки это правильное разделение и нужно найти какую-то конкретную грань когда есть смысл писать БЛ в контроллере, а когда в сервисе

Скорее, БЛ никогда не нужно писать в контроллерах, просто нужно найти грань, где БЛ, а где нет.

Интересная мысль — я с ней согласен, но где проходит эта грань? Например редактирование справочников это БЛ?
Как по мне, найти грань очень просто. Если это REST API, то это все, что может работать без веб сервера. Это та часть приложения что может жить в сфечисеком вакуме. Ты только дай ей данные и она сделает с ними все необходимое.

Исходя из этого, редактирование справочников это точно бизнес логика. REST API получает запрос, преобзовывает его согласно принятым протоколам, отдает на откуп бизнес логике.
> REST API получает запрос, преобзовывает его согласно принятым протоколам, отдает на откуп бизнес логике.
Практически все это делается автоматически фреймворком и мы возвращаемся к ситуации в начале статьи. Вот как вариант habr.com/ru/post/505708/#comment_21713234

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


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

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

Потому что в Вашем коде нет никакого BL. И все слои вместе взятые — это одно сплошное императивное редактирование ячеек БД через HTTP запросы. (Как и в большинстве проектов, которые я видел).

я тут недавно прям начал углубляться в ООП, книжки читаю, чтобы прям мышление поменять, так вот, я как бы сторонник идеи data-first т.е. сначала данные, разрабатывая что-то, думаю сначала о данных, а потом уже о том, как что-то с этими данными будет работать. Так вот, в одной из книг, я увидел такое описание что такое класс, что такое объект.
Там привели аналогию с базой данных. Что класс — это таблица, со всеми полями. А объект, это строка данных в этой таблице. И я сейчас еще больше БД воспринимаю как часть бизнес логики. а не просто хранилище данных. Да, логика работы в коде, но данные, как данные класса, они в базе.
Не стоит так воспринимать БД. Аналогия для понимания классов и объектов может и удобная, но работа с БД кардинально отличается от работы с обычными объектами: вытащить строку из БД гораздо накладнее, чем создать новый объект в памяти. Работу с БД лучше воспринимать как работу с внешним сервисом со специфическим интерфейсом (чем она технически и является) и инкапсулировать ее в отдельный сервис/репозиторий, тем более что «завтра» эти прямые запросы к БД и правда могут превратиться в запрос внешнего сервиса.

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

может быть, все же зависит от контекста, если это веб-проект, и все объекты все равно создаются из данных, которые лежат в базе данных. То что база может поменяться, сегодня это MySQL, завтра PostgreSQL послезавтра это какой нибудь Mongo или Ellastic да, согласен полностью, это вопрос к создателю объекта из базы он будет создавать объект или из внешнего сервиса или из кеша. в целом конечно согласен.
Не могли бы вы дать ссылку на пример подобной реализации, либо самому написать статью) После вашего комментария открылись глаза, прошерстил большую часть примеров «Правильной реализации веб-приложений на asp.net», и везде сервисы лишь являются переносом кода из контроллера в отдельную библиотеку, в которой используется вся доступная логика приложения.

… вы, я так понимаю, ни PoEAA, ни DDD не читали?

UFO just landed and posted this here

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

Справедливости ради, автор, рассматривает и тестирование
Тестирование? Контроллеры точно так же тестируются, а подняв TestServer вы практически напишите end2end тесты.
.
Вы ведь ходили по ссылке и смотрели на WebApplicationFactory, не так ли?

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

храним модель базы в .DAC файле, которая сама делает миграции базы в ci/cd без потери данных.
https://docs.microsoft.com/en-us/sql/relational-databases/data-tier-applications/data-tier-applications?view=sql-server-ver15


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


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

но там тяжело что-то зафакапить

Т.е. unit тестов нет?

Я считаю что подход уже не актуален, и мы шарперы вообще не любим вылазить за сам C#, потому всегда используем возможность не писать код на чем то другом. Пример попытки WebForms, Blazor, .razor файлы

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

Я работал в фирме где так был написан проект, дебажить такие хранимки на 3 экрана это боль.

хранимые процедуры, которые и содержат всю бизнес логику?

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

Да, при написании простых CRUDов таких мысли приходят в голову. Но как только возникает необходимость работы с HTTP — контроллер перестаёт быть простым "почтальоном" и у него появляется своя ответственность, очевидно отделяемая от ответственности сервиса.


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

Если вы гиузите файл то в параметрах метода появится модель IFormFile, и в ней уже будет и файл, и имя, и тип

Это всё равно часть HTTP/ Для нормального разделения отвественности передавать её из контроллера дальше — не комильфо.

Это если "ловить" html-форму. Но бывает ведь ещё и вариант прямой передачи файла, без формы, когда содержимое файла без изысков лежит в Request.Body. А ещё иногда оказывается файл проще закодировать в base64 и передать как часть JSON или XML.


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

Но как только возникает необходимость работы с HTTP — контроллер перестаёт быть простым «почтальоном» и у него появляется своя ответственность, очевидно отделяемая от ответственности сервиса.
Ваш комментарий отлично согласуется со статьей.
Как только возникает необходимость работы с HTTP — отделяем сервис. До того — пользуемся только контроллером.
Как только контроллер перегружается логикой — отделяем сервис. До того — пользуемся только контроллером.
Как только появляется переиспользуемый код — отделяем сервис. До того — пользуемся только контроллером.
Как только появляется требование GraphQL — отделяем сервис. До того — пользуемся только контроллером.

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

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


Вот и приходится в начале каждого нового проекта играть в "архитектурную угадайку", понадобится — не понадобится :-)

UFO just landed and posted this here

На активных проектах — есть. А вот на саппорте, когда на проекте половина разработчика...

А [Http:post(login)] или как там — это не работа с http?

Последний проект делаем по данному принципу (уже год как в проде). Проект живет, постоянно добавляем страны, с кучей локальной логики. И… все нормально)
По моим прикидкам у нас более половины контроллеров вообще не менялись с момента написания. Кода там мало и он понятный. Да и наши сервисы отличаются от контроллеров наличием пары аннотаций на методах\исключениях. Вынести сервис из контроллера — просто. Рефакторинг работает.

Не вижу причин вводить дополнительный уровень сервиса, если он не нужен и не будет отдельно использоваться. Радуют фразы: «а вдруг новый протокол понадобится». Всего не предусмотреть. В нашем проекте гораздо веротнее, что придется переписывать пару сервисов на GO или Kotlin.
Маленькие контроллеры хороши тем, что когда кому-то надо посмотреть список методов, выставленных наружу, с их роутами и параметрами, гораздо легче прочитать 100-строчный файл, чем продираться через 5000-строчный.

Для этой цели замечательно подходит Swagger UI.

для решения этой проблемы используем интерфейсы)
На них не видно роутов.

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

Потому что атрибуты [Route], [HttpPost] должны быть применены к экземпляру.
Если их навесить на интерфейс, они работать не будут?

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

гораздо легче прочитать 100-строчный файл, чем продираться через 5000-строчный.

Разве современные ide не умеют в сворачивание тела? С трудом представляю себе, чтобы я хотел посмотреть на список методов контроллера и доблестно скроллил через код этих методов.

Умеют, но каждый сворачивать скорее всего придётся вручную и отдельно. Скроллить — быстрее.

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

Но главная проблема в изменчивости требований. С опытом начинаешь понимать, что есть неиллюзорный шанс однажды все переписать. Очень не хочется этого делать. Ни мне, как программисту, ни бизнесу. Отсюда все эти — «На одну кнопку две недели? Как??!!!». И поэтому подкладываешь соломку заранее. Где-то по простому — сразу не пишешь код в контроллерах или хранимках, потому что это дешево. Где-то оставляешь задел на вероятное будущее, которое может и не наступить. А заранее это делать дорого, да и есть более важные задачи. Все эти решения принимаются на основе опыта, видимого потенциала конкретного проекта и деадлайнов.

Пожалуй единственная категория проектов, где было все (или почти все) заранее известно — это для гос. учреждений по водопаду. На моей практике — такие проекты никогда серьезно не развивались. Их либо выкидывали совсем, либо заменяли другими. Я там мог сложить бизнес логику в хранимки и 5-10 лет это никого бы не парило.
Если мне скажут что у нас может появиться ещё один канал, к примеру через очередь, я просто могу получить экземпляр контроллера и использовать его в другом канале. Этот контроллер легко резолвится из DI.
Восемь костылей из десяти.

Этот подход работает, но работает он грязно. Вместо того, чтобы иметь изолированную бизнес-логику и несколько параллельно существующих адаптеров для ее использования (http api, rmq, ...), вы прибили логику гвоздями к одному из них и теперь он будет тянуться везде — в другие адаптеры, в тесты. Тем самым связность системы и сложность ее понимания возрастает. Зачем модулю RMQ вообще знать что-то про HTTP API. Когда выйдет новая версия ASP.NET Core и вы захотите на нее перейти — вам придется обновлять и перетестировать не только http-модуль, а вообще все приложение целиком.

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

Просто в тот момент, когда вы вынесли из контроллера все, что связано с HTTP (биндинг в обе стороны, низкоуровневую валидацию, ответы с заголовками и прочим счастьем, роутинг и так далее, далее, далее), он перестал быть UI-контроллером, вот и все.

Если мне скажут что у нас может появиться ещё один канал, к примеру через очередь, я просто могу получить экземпляр контроллера и использовать его в другом канале. Этот контроллер легко резолвится из DI.

Как бы вам сказать… ваш контроллер все еще напрямую зависит от Microsoft.AspNetCore. А откуда у меня этот пакет в бэкенд-сервисе, который крутится себе тихо в демоне и очередь разбирает?

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

Немного оффтопика:
Меня немного смущает то, что существование юзера в коде проверяется по имени и!!! паролю. Т.е. два одинаковых юзера с разными паролями — это ок? Наверное проверка существования должна быть только по имени.

(убрал старый комментарий, вы правы)


Я думаю, что это просто ошибка копипасты из логина.

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

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

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

[Это был сарказм]

Ну а если по делу то цель создание сервисов бизнес логики равно как и репозиториев это разделение ответственности. Вы ведь упоминали в статье про трехслойную архитектуру (кстати слоев вполне может быть больше чем три, взять хотя бы тот же DDD) суть разделение по слоям не в следовании каким-то каргокультам внушенным нам рептилоидами а в разделении ответственности. Контроллеры отвечают за HTTP (ну или другой протокол обмена сообщениями), репозитории за взаимодействие с базой данных… Реализация IEmailService например за отправку почты. Я обычно бизнеслогику засовываю в .Net Standard либы чтоб не было соблазна «платформо-зависимый код» в них писать.

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

Собственно к чему я это все тут написал — писать код в котроллере технически возможно, равно как и писать запросы к базе на прямую, или пинать бомжа напрямую без использования механической ноги. Но правильно ли это? Если этот проект планируется развивать и поддерживать многие годы то думаю писать все в контроллере плохая идею. Времена идут, все меняется, сегодня вы реквесты/респонсы через ASP.NET обрабатываете, завтра вместо контроллеров решите например Azure Functions с их HttpTriggers использовать после завтра например в Лямбды все завернете или еще чего другое придумается (тут уже упоминали Graph QL)… Подлодку делят на отсеки потому что если в одном из них пробоина, можно просто перекрыть перегородки и подлодка не потонет. Так же и тут, слои нужны для того чтобы в случае изменения реализации одного из них все остальные слои не пришлось переделывать, или вообще весь проект переписывать.

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

Лет 7 был бомжом, работая программистом :)

зачем тогда использовать эту ненужную прослойку под названием репозиторий

тем более, что DbSet это и есть репозиторий — по крайней мере «крякает и плавает как утка» ©

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


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

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

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

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

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


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

вызываются ровно в одном месте, поэтому создавать подобный код не нужно

Может быть не нужно быть таким категоричным?
Разные случаи бывают.
Например, пускай код и вызывается в одном месте, но он не влезает в 4 монитора. Что — оставлять в контроллере или все-таки попытаться перенести в другие классы?
Да много примеров таких есть.
По моему опыту, когда пишутся первые строчки, программист на 99% не знает будут они последними или нет. Так что лучше с самого начала держать все под контролем и следовать принципу SRP — контроллер отвечает за преобразование запросов в Dto и вызывает сторонние методы, преобразует exception в соответствующие коды возврата.
Когда в будущем мне понадобится переиспользовать бизнес логику (например в новом транспорте — консьюмере очереди), я просто вызову соответствующие бизнес сервисы и не буду трогать контроллеры (т.к. добавление нового транспорта не должно затрагивать существующие). Тем более, очень часто, нужно сделать быстро и нет времени на рефакторинг и разделение «лапшекода».

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


Причем заранее создавать методы не обязательно, IDE сейчас хорошо умеют в Extract Method рефакторинг.


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

UFO just landed and posted this here

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


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


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

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

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

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


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


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

Чем меньше кода, тем проще его поменять

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

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

это не метрика.

Для меня — метрика и еще какая:
Количество рефакторинга при добавлении новых фич, если у меня код нормально гранулирован, я не выделяю новые функции из существующих, а использую те блоки, которые я до этого создал.
Т.е. при идеальном коммите у меня будет Lines Changed:0 Lines Added:[количество строк для новой фичи].

А тепрь давайте посмотрим на коммитлог вашего релаьного проекта ;)
В мире розовых единорогов чего только не бывает.

коммитлог вашего релаьного проекта

Шутку оценил:)
Но я все же о том, что
задача инженера, на мой взгляд, как раз и в нахождении «золотой середины», без впадения в крайности.

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


Поэтому существует принцип LEAN — делать как можно меньше работы до того как появилось подтверждение её нужности.

предугадать где эта середина

В этом помогает «опыт».
LEAN — делать как можно меньше работы

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

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


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


Про переписыване говорят слабаки, которые не умеют программировать. посмотрите на опыт microsoft с "переписыванием IE".


Напомню: группа программистов в редмонде сказала что IE (на тот момент версии 9 или 10) преватися в «кусок» и надо переписать.


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


Родившийся параллельно edge пытался походить на Chrome поддержкой передовых, еще не до конца оформившихся, стандартов, но страдал от детских болезней и некоторых унаследованных, так как даже при "полно переписывании" умельцы тянули куски кода из старого IE.


Спустя несколько лет Edge сдох, а MS выкатил новую версию на базе Chromium. Так что переписываие любого большого проекта это путь в никуда. Рано или поздно менеджеры поймут, что проще переписывающих уволить, а устаревший продукт заменить УЖЕ СУЩЕСТВУЮЩИМ современным.

UFO just landed and posted this here
Я не видел ни одного случая, чтобы разработчик угадал куда проект будет развиваться

А я не видел не одного случая, когда проект больше трех лет пишется с принципом «делай все проще» не превращается в сплошной копипаст и классы на не одну тысячу строк.
UFO just landed and posted this here
UFO just landed and posted this here

Вы серьезно сравниваете длину кода на разных языках?

UFO just landed and posted this here

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


Поэтому нет смысл на разных языках саравнивать код.

UFO just landed and posted this here

То есть все-таки можем создать сколько захотим?
Не смущает что править придется в двух местах если будет не 9, а 10 мест?


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


Если число мест — не константа, то вы еще 100500 ошибок совершите из-за преобразования типов и получении значения извне.

UFO just landed and posted this here

А чем простая проверка предусловия if (count in [2, 6, 9]) throw new Error(`count must be in [2, 6, 9], ${count} given`) не угодила? С ним вы так же не сможете создать неверноый стол.

UFO just landed and posted this here

То есть пишем больше кода, чтобы об ошибке узнать раньше?

UFO just landed and posted this here

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

UFO just landed and posted this here
Можно, но при этом вы 100% залезете в технический долг, о котором даже не будете подозревать.

Неа, в описанных условиях никакого долга нет.


что делать, если сервис, реализуемый контроллером, должен быть Singleton либо Transient?

… какой-такой сервис? Написано же: не надо никакого сервиса, потому что код вызывается в одном месте.

UFO just landed and posted this here
Напомню, что в статье автор предлагает, с одной стороны, имплементировать интерфейс контроллером, и резолвить его из контейнера

Автор предлагает фигню, прямо скажем, и об этом я тут уже где-то писал.


т. е. я не могу запустить его по таймеру, по событию, либо из другого, например, мобильного интерфейса. Не могу (в общем, опять же, случае) запустить из Blazor'а, если использую SignalR, так как Scope SignalR не на один запрос.

Не можете. Потому что это не надо.


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

Бесплатных подходов вообще нет, вам в любом случае придется что-то тратить. В данном случае вы не тратите на то, что вам не нужно.

UFO just landed and posted this here
У вас и функции, наверно, по 300-500 строк без логического разделения на приватные подфункции?

Разделение на более мелкие сущности придумали не только для того, чтобы легче тестироваться и соблюдать DRY, его придумали еще и для того чтобы отделять «мух от котлет». Тут вот мы обрабатываем http запрос, тут мы его валидируем, тут работаем с очищенными данными, а тут — формируем ответ. И если уж так вышло, что что-то из этого выполнено на аннотациях, что-то автоматически выполняется до вызова контроллера, что-то после и на бизнес-логику остается только 1 строка — штош поделать, придется смириться с тем, что у нас экшн контроллера состоит из одной строки.

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

А в общем случае, если отойти от примера с 1 строкой, я думаю вы со мной спорить не будете, что легче работать с функциями по 10-30 строк и классами по 300-500 строк с не большим количеством методов, чем с монстрами. Естественно, увлекаться разбиением сущностей тоже не стоит, создавая по сервису не каждый чих — всё хорошо в меру

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


Разделение кода на функции это хорошо. Создание классов, которые занимаются в основном вызовом других классов — плохо.


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

Да какой-же водопроводный класс контроллера получается, если он преобразует http-запрос в dto? Или вы аннотации принципиально кодом не считаете?

HTTP запрос в DTO преобразует ASP.NET.
Или вопрос в чем-то другом?

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

Аннотации не обязательные. ASP.NET может и без них разобраться.

Да какой-же водопроводный класс контроллера получается, если он преобразует http-запрос в dto?

Вот собственно этот и получается. Когда экшн в контроллере сводится к Action(HttpRequest request) => request |> bind<ActionInput> |> _service.Action |> bind<HttpResponse> — это типичная сантехника, от которой лучше (именно что "лучше", а не "обязательно надо", потому что далеко не всегда надо или возможно) избавиться.

Если я правильно понял, то вы о случае когда объект http запроса передаётся в сервис? Я же не о нём, а том, когда этот объект, преобразуется (руками или аннотациями фреймворка — не суть) в какой-то DTO, который об http не знает ничего, а уж передаётся в сервис

Нет. Там же явно bind написан, вот оно, ваше преобразование.

Вот если бы значки с непонятным смыслом (ассоциации с Хаскелем) словами написали, то было бы понятнее, возможно. bind в программировании для меня значит что-то к чему-то привязать, например, конкретное значение к плейсхолдеру в SQL запросе, или значение к переменной в замыкании.


Но, допустим, понял. Так чем плохо разбивать процесс преобразования данных на чётко выделенные этапы:


  1. принимаем http-запрос параметром в контроллере
  2. преобразуем в методе контроллера нужные понятия http в понятия какого-то сервиса, реализующего прикладную логику и ничего об http не знающего
  3. вызываем этот сервис
  4. его результат преобразуем в http ответ
  5. возвращаем ответ

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

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

… а в ASP.NET (Core), о котором речь в посте, model binding — это процесс привязки значений из HTTP request к конкретным типам параметров action. Я их просто вынес из аннотаций и магии в контроллер.


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

Я просто не о том подходе, о котором вы. Плохо не разбивать процесс преобразования, а писать рутинный код. Собственно, это было очевидно даже разработчикам ASP.NET WebAPI, поэтому, на самом деле, тот код, который я выше показал, можно написать вот так: Action(ActionInput request) => _service.Action(request) (преобразование типов на входе и выходе делается автоматически). В итоге, получаются вот такие контроллеры:


ActionA(A input) => _service.ActionA(input);
ActionB(B input) => _service.ActionB(input);
ActionC(C input) => _service.ActionC(input);

… что, лично мне, кажется скучным, рутинным, и, что самое главное, ненужным.


Главное в этот момент не подумать, что надо втянуть сервис в контроллер. Надо выкинуть контроллер:


webApi.RegisterProxyController<Service>()
Вот это меня отдельно возмущает. Из-за того, что «плохо писать БЛ в контроллере» напишем тоже самое в сервисе. То есть дело просто в названии класса? Название важная штука, но дает ли такой подход какую-то осмысленную выгоду?

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

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

Контроллер, по сути, это лишь специфичный сервис, знающий, в данном контексте, об HTTP. Вопрос стоит так: делаем два сервиса, один из которых содержит БЛ, а второй работу с HTTP и вызов второго или делаем один сервис, в котором HTTP и БЛ вместе, максимум БЛ в приватный метод вынесена.

Главное в этот момент не подумать, что надо втянуть сервис в контроллер. Надо выкинуть контроллер:

Классная идея! Даже немного завидую дотнетчикам, если так можно автоматически делать.

Печеньки у нас тоже есть.

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

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

UFO just landed and posted this here

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


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

В любом случае чем меньше будет в совокупности классов и строк кода, тем код будет лучше.

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

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


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


Я так понимаю вы примерно то же самое хотите сказать, но стесняетесь или просто не понимаете.

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

Это утверждение не подтверждается на практике. Это вера.

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

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

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

Можно. convertHttpRequestToUserEntityAndPersistIt :)

Неа, просто Persist. Конверсия имплицитна для контроллера.

Вот правильно уже выше написали — ваш подход хорош, пока у вас простое crud приложение.
Да, сервис плодить ради лишнего класса плохая практика.
Но надо сразу решить, останется ли оно таким в обозримом будущем. Иначе при изменении архитектурных/бизнес-требований вам придётся его переписать примерно полностью, потому как повытаскивать сервисы из сотни методов контроллеров будет не простой задачей.
И да, положить тестирование почти всей бизнес-логики на e2e тестирование, тоже так себе затея…

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

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

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

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


Так же автор в своём примере превращает DI в сервис локатор, нарушая принципы инкапсуляции

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

Конкретно в этом сценарии вам не нужно две реализации хранилища.
Сначала можно сделать хранение в памяти, а затем заменить код на хранение в БД.

Я так и делаю =)
Создаю интерфейс UserDataasource, в котором описываю методы.
Затем создаю класс UserDatasourceImpl, в котором реализую предыдущий интерфейс и при этом сохраняю данные в переменных.
Затем, когда я завершаю разработку, я либо переписываю методы, либо создаю новый класс (Данный подход не сильно отличается от переписывания методов).
Но чаще я всё таки создаю вначале класс UserDatasourceImplMock, для того, чтобы явно было видно, что это заглушка, а так же переиспользовать в тестах.
К тому же, общение с бд у меня чаще происходит через композицию в этих датасурсах (В приватную переменную кладу объект соединения при инициализации объекта датасурса).

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

то-есть когда у Вас будет таска «переехать на другую бд» Вы сразу создадите новую реализацию и в DI её зарегистрируете вместо старой, а старая в свою очередь будет просто существовать? («А вдруг вернуться захотим?», «А вдруг захотим оба источника использовать? — тут придется ещё и интерфейс поменять»)

По мне так проще изменить сразу первую реализацию и всё.

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


На случай "А вдруг вернуться захотим" есть инструменты контроля версий

Пожалуйста, давайте честно, у вас был опыт миграции с базы на базу при объемах выше 100гб данных, в хотя бы десятке табличек?

Если что — у меня был (ms-> pg). Подход «мы просто поменяем слой доступа к данным» — приводит к лютым просадкам производительности в неожиданных местах, потому что движки баз, надо же, очень разные. И если вы хотите пристойной производительности — модель данных всё равно придётся переделать.

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


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


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


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

Так же автор в своём примере превращает DI в сервис локатор, нарушая принципы инкапсуляции
Можно подробнее, где я такое сделал?

Такое чувство, что автор не пытался разобраться в вопросе, не искал статьи и книги для ответа на вопрос "Почему делают так?". Иначе автор спорил бы с аргументами, которые говорят "за" разделение, а не писал только свои мысли по этому поводу.

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


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


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


Чтобы переиспользовать код? — Нет, флоу перевода денег не подойдет для флоу начисления бонусов.

К тому-же если вы даже найдете возможность совместить 2 фичи, вы рискуете сломав одну — автоматически сломать другую

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

Отличная статья. У меня тоже возникают подобные мысли, но они больше о том, что контроллеры — это пережиток прошлого. Они были актуальны, когда мы писали с использованием MVC, а на сегодняшний день большие проекты это скорее отдельный бандл с фронтом и API например на ASP.NET. То есть вьюх больше нет, модель — это детали реализации, но контроллеры мы продолжаем фанатично использовать.
Но некоторое время назад мы стали переходить на Mediatr и писать однострочные контроллеры, которые вызывают всегда один и тот же метод mediator.Send(request) и в этот момент вопрос "А нужны ли нам контроллеры?" заиграл новыми красками. Такой вопрос появился ещё, как минимум, у одного человека, который сделал библиотеку https://smithgeek.com/announcing-voyager/. Она позволяет писать код для каждого обработчика в отдельном файле и управлять роутингом с помощью атрибутов для Реквеста, а не для метода контроллера.
По факту, люди использующие медиатр уже и так управляют пайплайном обработки путём навешивания на реквест атрибутов и интерфейсов, Voyager же перекладывает на реквест ещё и роутинг и контроллеры становятся не нужны.

роутингом с помощью атрибутов для Реквеста

Ага, а потом мы транспорт дополняем чем-нибудь и эти атрибуты тянутся в совершенно неожиданные места — скажем в очередь, где они никакой смысловой нагрузки не несут, просто по факту есть. Это все напоминает один проект в котором не было разделение Dto->EF class, а было все смешано. ужас.
Как по мне, у каждого должна быть своя ответственность. У endpoint (в нашем случае методы контроллера) — роутинг, а у запросов — перенос данных.

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

Значит тут мы множим объекты по каждому транспорту.
WebAPI: request для http=>request в BL
RabbitMQ: request для queue=>request в BL

Вместо:
Общий реквест->он же уходит в BL.
Но:
Http транспорт — свой ModelBinder у параметров метода контроллеров.
RabbitMQ — свой сериалайзер.

Когда нам нужно добавить новые поля — у нас просто добавляется в класс запроса и используется на стороне BL, и делаются при необходимости изменения в Binders. А при первом подходе, нам нужно их разнести по всем transport specific requests. И хорошо, если это настроено автоматически:)

Handler, который принимает с фронта реквест уже и есть BL. Это, как раз, возможно из-за отказа от контроллеров.


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

Handler, который принимает с фронта реквест уже и есть BL. Это, как раз, возможно из-за отказа от контроллеров.

Реквест в данном контексте — это dto, никак не привязанное к http? Или всё же какой-то объект из которого можно вытащить url, например, или куку?

Это очень правильный вопрос.
Реквест здесь — это DTO, который хранит в себе достаточно данных для обработки запроса. Эти данные могут быть получены из кук, url-параметров, body или даже из базы.
Этот объект собирается с помощью pipeline behavior'ов и приезжает в хендлер уже полностью заполненным.
Вот пример реквеста:


    [PublicAPI]
    public class Request : IRequest<Response>, IAuthorizableRequest
    {
        public SecretBuyerClaimsPrincipal SecretBuyer { get; set; }

        [FromRoute] public UUId CheckupId { get; set; }
        [FromRoute] public UUId QuestionId { get; set; }
        [FromHeader(Name = "Content-Length")] public int ContentLength { get; set; }
        [FromHeader(Name = "X-Filename")] public string FileName { get; set; }
    }

Здесь CheckupId, QuestionId, ContentLength и FileName заполняются обработчиком запросов asp.net.


В свою очередь SecretBuyerClaimsPrincipal — это реализация интерфейса IAuthorizableRequest. Это свойство заполняется хендлером авторизации.


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


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

UFO just landed and posted this here

Да, на входе стоит fluent validator, который избавляет от необходимости проверять поля, пришедшие с фронтенда.
Да, реквест mutable. Это трейдофф, на который я сознательно иду, чтобы иметь возможность обогащать его по ходу пайплайна. При этом все обогащения контролируются внутри решения и покрыты тестами, поэтому я могу доверять реквесту.
Если я меняю реквест, то тут 2 варианта:


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

По поводу зависимости домена от asp.net: хендлер — это не доменный сервис, а скорее application-сервис в контексте луковой архитектуры. Он использует доменные сущности и сервисы, но не является доменным сам по себе, поэтому домен независим от asp.net.

UFO just landed and posted this here
Handler, который принимает с фронта реквест уже и есть BL.

А как быть если нам нужно это же действие сделать в consumer workerа? Резолвить этот хэндлер?

структура которых сделана именно под очередь

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

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

что случится в будущем

Да, к какому окончательному коду мы придем в будущем- точно никто не знает.
Но вот, то что в будущем, если проект «выстрелит» — у нас будет очень мало времени на рефакторинг и очень большой бэклог новых фич — вероятность такого стремиться к 100%.
И устранять «лапшенину» гораздо приятней в коде, где физический слой совершенно не пересекается с BL (как например в миграциях ASP.NET -> AspNet Core). И не забываем к концу года нам обещают .NET 5, у которого хз что будет с обратной совместимостью.
классная библиотека.
Я когда сам перешел на медиатор, мне контролеры с одной строкой казались ещё более бесполезными, я тогда написал решение похожее на эту библиотеку.
В итоге у меня появилась возможность вешать [Route] [Http...] на хендлеры медиатора.

Контроллер — это Interface Adapter. Таким образом, если библиотека выступает в роли Interface Adapter, то это не означает, что вы не используете данный слой.


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

А почему вы не оформляете код тэгами code?
я не против такого подхода где это уместно. Малые-Средние по размерам проекты. Я такое местами сам делал. .NET, .NET Core, WebAPI — 15 лет.
Наверное еще нужно вспомнить, что могут быть разные реализации одного интерфейса бизнес логики.
Такие реализации скорее всего о другом к примеру для паттернов Стратегия, Фабрика и тд
Но разные реализации того же репозитория — никогда. (мок не считается)

Встречаются, если есть требование поддержки разных баз.

UFO just landed and posted this here
На хабре есть статья про то что люди уже боятся писать тестовое так как они считают лучше. Ибо я и многие другие понимают что хотят увидеть на тестовом — типичную 3-х слойную архитектуру, без кода в контролерах.
Сейчас у меня флешбеки — пришел gRPC. И я хочу увидеть ваш код, как он справится с этой задачей.

Уже справлялся.
  1. Сгенерил BaseService на основании .proto
  2. Зашел в контроллер, выделил код в каждом методе
  3. CTRL + X
  4. Зашел в контроллер GRPC, сделал override каждого метода
  5. CTRL + V


И мы переехали на GRPC
Писал код в контроллерах ещё до того, как появился пост на хабре!)
Код выделяют на случай, если не будет большего вашего MVC.
Появится новый Pew Pew Done и болезненный процесс перехода MVC -> PPD.
А у вас уже будут выделенные сервисы.
да, будут, но до этого я не хочу платить и делать что то на «а вдруг ...»
Прочитал заголовок — думаю, что не так? Вполне ведь себе пишем код в микроконтроллерах.

Автору советую разобраться в принципах SOLID. Это даст понимание почему так делать не нужно.


Естественно данные принципы не являются догмой и рекомендуются для написания reusable кода. Если вы пишите финальный класс который никогда и ни кем не будет использоваться и поддерживаться то можно писать как вам хочется. Программирование профессия творческая. (Когда-то было модно писать весь код программы заглавными буквами)


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


Стиль написание кода очень хорошо показывает опыт человека его образ мышления и иногда даже характер :)

Прекрасно понимаю SOLID. Код пишу уже более 5 лет, и появились размышления что мы делаем много на «а вдруг...». Мы платим сейчас чтоб ПОТОМ ВОЗМОЖНО заплатить меньше и мне кажется что тут больше убытка чем выгоды.
Мы в каждой задаче закладываем доп работу чтоб если-что, легче было потом, но ведь чаще это потом не наступает.
— А вдруг мы захотим сменить бд? — Камон, при смене бд самое просто поменять код под новую бд. А мигрировать данные, по мне так в разы сложнее.
— А вдруг мы захотим резко удалить наш проект и слиться? — Почему ж тут мы не делаем функционал для удаления всего по одной кнопке? (сарказм)

Базовые принципы солид (без фанатизма) — это чтобы легче было уже сейчас: написание кода, отладка, ревью, тестирование

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

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

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

Это здорово конечно, только что это дает в формах о которых говорит автор? Ну будет у вас пустой контроллер — который прокидывает вызов в сервис. Если поменяется что-то на форме вы будете менять сервис, а не контроллер. Получается сервис обслуживает только одну форму (и он по сути превращается в контроллер).

Основная мысль статьи — а получаем ли мы достаточно профита от соблюдения всех правил? Никто не покушается на правила как таковые — естественно надо разделять обязанности и все такое

Да почему же пустой? Он будет конвертировать http запрос в параметры вызова сервиса, опционально валидировать/санитайзить запрос, и потом ответ сериализовать или вью рендерить. По-моему, достаточно ответственностей, чтобы не говорить "пустой". Что всё это или почти всё реализуется через аннотации — дела не меняет.


P.S. KISS точно не нарушается, два класса (контроллер и сервис) для CRUD именно простыми становятся, проще некуда: глянул и сразу понятно за что отвечает один и другой. А если юнит-тесты пишите, то и YAGNI не нарушается.

Если уж на то пошло, то пустой контроллер и сервис к нему нарушает принцип YAGNI и KISS
UFO just landed and posted this here
Согласен. И это приводит нас к мысли, что использовать SOLID как истину в последней инстанции как минимум не честно. Должна быть какая-то рациональность, а не догма.
Как я писал выше — для каждого проекта набор принципов свой многие принципы и патерны навязываются фреймворками. Это позволяет человеку знающему фреймворк быстро ориентироваться в проекте. И кстати в примере который обсуждается это скорее патерн фреймворка.

Если применять принципы SOLID к коду приведенному автором то как минимум он нарушает S и D.
Sign up to leave a comment.

Articles