Comments 265
Я видел контроллеры с большим и кол-вом кода, разделив его на несколько контроллеров, проблема решается.
Спрятав говно в другое место, и обернув его для вызова в одну строку, ситуация ни как не изменится.
И статья описывает ситуации где силы вкладывают в то что б в контроллере было не больше чем 1 строка в каждом методе — тоесть делать и контроллера тупой враппер.
Спрятав говно в другое место, и обернув его для вызова в одну строку, ситуация ни как не изменится.Ну, начнем с того, что если писать говно то его хоть как размазывай, а суть не изменится.
Я видел контроллеры с большим и кол-вом кода, разделив его на несколько контроллеров, проблема решается.Аналогично, могу сказать что такой подход совершенно не решает проблемы. Не то что видел такое, я с таким работаю в данный конкретный момент. Один метод может занимать по четыре сотни строк кода и это при учете что внутри вызывается пара десятков других сервисов. Что уже используется валидация через FluentValidator. Что логирование и обработка ошибок централировано вынесена в filters. Что используется мапер.
Я в свою очередь сомневаюсь в том, что польза от этого так уж великаМы как инженеры работаем со сложностью от которой невозможно избавиться. Ей можно пытаться манипулировать. Можно написать один метод на тысячу строк, а можно структурировать код на уровни. Вынести логику работы с базой, выделить бизнес логику, подготовку данных для отображение и закрыть все это абстракциями. Поддерживать общность и консистенцию кода, слабую связность между реализациями различных уровней. Что бы каждый кусок был прост в понимании и редактировании, но разобрав как работает один контроллер было легко понять как работают остальные.
Первый способ работы с которым учат работать в любом вузе — декомпозиция. Потому что работать с большим количеством простых вещей гораздо проще чем с чем то одним но переусложненным.
Действительно. Если проект будет жить месяц, если его не планируется поддерживать и развивать то конечно проще и целесообразнее описать весь код в контроллере и не париться. Но если планируется расширение то очень скоро вы придете к тому что разница между реализациями отдельных контроллеров начнет душить вашу архитекруту. Потому что не будет единства, не будет возможности легко вносить общность в код. Понимать каждый контролер придется с нуля потому что каждый член команды пишет по своему и смотря с какой ноги встал сегодня утром. И поверьте мне, люди чаще всего встают не с той ноги.
Личный опыт в подобных вопросах это все равно что «Синдром выжившего». И до тех пор пока вы не сталкнетесь с бизнес логикой уровня чуть выше чем «за что купил — за то продал» вряд ли удастся осознать зачем напридумали всяких там шаблонов, SOLID и написали несколько толстых книжек такие товариши как Макконел, Фаулер, Мартин.
Еще раз подчеркну: Много простого — хорошо! Сложно, но Мало — очень очень плохо! В первом случае каждый конкретный момент вы будете работать с емкой, конкретной функциональностью отдельного уровня абстрации. Во втором случае в любой момент вы будете иметь честь по локоть возиться в том самом, что вы так настойчиво не хотели скрыть за абстракциями.
Сомневаюсь, что если его разнести, он станет понятнее, если там реально сложная бизнес логика, то все равно так или иначе придется в нее въехать, а создание контроллеров, которые всегда тупо переадресуют логику в другой метод это уже какой-то карго культ.
Чтобы разгрести помойку нужны другие средства, чем чистенький код контроллеров.
Ну вот в данных примерах контроллер не тупо переадресует, а, средствами фреймворка, преобразует http-запросы во что-то более-менее нейтральное, независимое от протокола.
У контролера и сервиса с бизнес логикой разные задачи. Задача контролера заключается в обработки запроса. Не важно это http или grpc. Для этого у него есть все необходимые данные. Тело запроса, параметры, куки и все остальное. Он их анализурует и решает что делать дальше. Сервис же обрабатывает бизнес задачу. Для этого у него есть свои инструменты. Это две разные абстракции. Смешивание двух абстракций в одном методе точно увеличивает его размер(нужно писать логику для двух сразу), что увеличивает вероятность что-то недопонять когда ты к нему вернешся. Если же используются отдельные сервисы, то будет фигурировать как минимум один метод с логичным названием, что существено упростит понимание и поиск это места в будущем(что наверно даже болие важно).
Сам все больше склоняюсь к использованию библиотек по типу MediatR. С ними искать и писать можно еще более точечно и весь контекст для выполнения задачи находится в одном месте. При этом исключается возможность
Я к этому в статье и подводил, писать контроллеры на подобии хендлеров, то-есть тоже точечные
С медиатором контроллер выглядит
_mediator.Send(...)
Команда в основном биндится из тела запроса
При этом исключается возможность случайно создать контролер с 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 специфичного, то в обработчике будет одна строка.
Вам понятно что делает следующий контроллер? Я бы сказал что он может делать что угодно.
Ну так он и правда делает что угодно, вы совершенно правильно подумали. Его давно пора декомпоновать.
Первый, зачем было инициализировать все остальные зависимости? Все ресурсы на их инициализацию были потрачены в пустую
Если дело именно в оптимизации инициализации — я бы рассматривал это в последнюю очередь. Красивый и производительный код по моему опыту часто противоречат друг другу. Если сильно хотите — эти зависимости можно сделать ленивыми.
Второй, как мне помогло знание того, что этот контролер требует 12 зависимостей? Как по мне, оно только ввело в заблуждение.
Ну, у меня логика простая — явное лучше неявного. Соответственно я вижу 10 зависимостей и понимаю:
1. Возможно, контроллер плоховато написан и его стоит переписать.
2. Да, у него 10 зависимостей, но у него нет 11 и 12.
В случае с медиатр вы видите красивое IMediatr, контроллер до сих пор делает кучу вещей, только это абсолютно не очевидно.
Про Service Locator:
Не совсем понял что имеется виду. Можете уточнить?
Есть такой паттерн, который предполагает что вместо того, чтобы брать зависимость от сервиса, вы берёте зависимость от «локатора», который умеет эту зависимость доставать. В итоге с точки зрения конструкторов (наиболее частый способ DI в моей практике) мы имеет IServiceLocator везде, но зато в каждом методе мы можем волшебно забрать себе хоть 10 сервисов.
Подробнее можно глянуть, например, тут.
public class Import : IRequestHandler<ImportRequest>
{
public Import(ISomeRepositoty repository, ISomeService service)
{
// ...
}
public async Task<Unit> Handle(ImportRequest request, CancellationToken cancellationToken)
{
// ...
}
}
В случае с медиатр вы видите красивое IMediatr, контроллер до сих пор делает кучу вещей, только это абсолютно не очевидно.
В случае с медиатр все очевидно, контролер не делает ничего связаного с бизнес логикой. За нее отвечает медиатр и для каждого отдельного вызова будут свои собственые зависимости, которые можно посмотреть в обработчике для медиатр.
В случае с медиатр все очевидно, контролер не делает ничего связаного с бизнес логикой.
Угу. И вы не видите, что контроллер отвечает за десяток бизнес-функций, хотя должен бы отвечать только за одну.
В случае с медиатр все очевидно, контролер не делает ничего связаного с бизнес логикой
Этого можно добиться и без него. Достаточно обернуть ваш хендлер из примера выше в абстракцию и брать зависимость в контроллере на неё, а не на ISomeRepositoty и ISomeService. Таким образом, контроллер будет явно в своём конструкторе говорить, сколько бизнес-операций он совершает. Если много — дробим.
На моей практике контроллеры с 5+ операциями встречались относительно редко, а если и встречались — эта проблема решается не тем, что мы просто скрыли весь код за медиатором.
Не холивора ради, я искренне не понимаю, почему скрыв код за неявной фабрикой хендлеров мы считаем, что он стал лучше. Сложность класса-то от этого не поменялась, он как делал Х вещей, так и делает их, только теперь об этом не узнать.
Напоминает service locator, не находите?
Не совсем понял что имеется виду. Можете уточнить?
Изменять модели под протокол клиента? Ох.
Да и конвертация из одного протокола в концептуально другой без крайней необходимости (адаптер к closed source) — это, как бы, попахивает. Уже вижу костыли.
тут приходят третий, четвертый и пятый интеграторы и последовательно заявляют: вебсокеты, TCP, UDP, GRPC, да хоть через AMPQ интегровать будем.
Если бы количество транспортов не было бы так сильно ограничено и сложность реализации своего транспорта была не так велика, то я бы с удовольствием использовал такое решение.
На мой взгляд, если в контроллере больше одного метода, то ваш подход существенно ухудшает варианты снижения сложности через инкапсуляцию. Допустим у вас метод контроллера разросся и вы хотите вынести часть логики в приватный метод. В этот момент вы получаете кусок кода который не имеет отношения к другим методам контроллера. И таких куски кода накапливаются в контроллере производя кашу. Для меня важно, чтобы область кода с которой я работаю могла поместиться в мою голову чтобы можно было понимать, что там происходит. А когда в контроллере куча вспомогательного кода, и он перемешан из разных сценариев, оно просто не помещается в голове и я начинаю думать о том, что не контролирую происходящее.
Флоу регистраци
Флоу авторизаци и тд
Подобные "сервисы" — это тоже антипаттерн.
И без CQS можно. Главное — отказаться от анемичных моделей.
А что где "есть" — я уж в курсе, я многое повидал. Хорошее редко увидишь.
Если тысяча зависимостей, то, скорее всего, нарушается SRP
ActiveRecord, да, нарушает SRP by design. DataMapper — не требует от Entity уметь храниться, это отвественность ORM.
Перемещение денег с одного аккаунта на другой — ответственность Entity навскидку. Отправка оповещений — скорее нет, максимум — генерация доменного события "деньги перемещены", а уж сервисы приложения, слушая это событие, вызывают инфраструктурные сервисы оповещения.
Да, UserService довольно сложен. Вообще AspNet Identity не самая прозрачная библиотека. Спасает только то, что есть код из примеров.
Я правда не понимаю, зачем оправдывать усложнение кода другим сложным кодом. От того что в nuget на пакете написано microsoft легче читать его не становится.
А когда в контроллере куча вспомогательного кода, и он перемешан из разных сценариев, оно просто не помещается в голове и я начинаю думать о том, что не контролирую происходящее.А в чем проблема вынести часть логики в сервис,
Я то как раз за тонкий контроллер. И за инкапсуляцию логики в отдельных сервисах\обработчиках в зависимости от ситуации.
Повторюсь, если у Вас есть вьюшки, тогда в контроллерах лучше писать код только для View, а в сервисах писать переиспользуемые методы для получения данных для View. Но в текущих реалиях все чаще у нас API + SPA.
Автор статьи хочет писать код в контроллерах. Вынесение кода в сервис или в хэндлер будет противоречить его основной идее.Автор не против вынесения кода в сервис. Идея не в том, чтобы весь код фигачить в контроллерах. Идея в том, чтобы выделять сервисы только при необходисти, а не по умолчанию.
А если по умолчанию считать, что http надо отделять от BL? Что код, который знает что-то об http не должен находиться в одном классе с BL?
Да, именно это хотел донести, спасибо! :)
Идея в том, чтобы выделять сервисы только при необходимости, а не по умолчанию.Это хорошо, когда у проекта 1-2 разработчика. А если их 5, и у каждого своё мнение насчёт «необходимости». Тут лучше ввести правила, как code style, чётко им следовать, и не надо будет ломать голову: тут уже надо вводить сервис, или пока не надо (а это тоже время)
Если заниматься по сути процедурным программированием, то, да, все равно, в какой псевдокласс, выполняющий по сути функцию неймспейса, сунуть функцию. В этом смысле между "контроллерами" и "бизнес-логикой на сервисах" нет особой разницы, тут действует аксиома Эскобара.
Рекомендую ознакомиться с подходами:
- Hexagonal Architecture,
- Domain Driven Development,
- CQRS.
Распухание бизнес-логики при усложнении бизнес-требований — это как раз нормально, разве нет? Хуже, если со временем распухание инфраструктурный слой без добавления бизнес-функций, просто чтоб было.
Если по теме — то ваш подход может быть вполне оправдан для проектов, время жизни которых заведомо меньше времени устаревания фреймворка. В иных случаях может оказаться, что бизнес-логику написали в коде windows forms компонентов (потому что зачем усложнять, выделяя BL?), и теперь дешевле все выкинуть и начать сначала. Аналогично, через 10 лет такую же брезгливость будет вызывать бизнес-логика в контроллера, прибитых гвоздями к допотопному ASP. Net Core 3.1.
Хорошая статья! Спасибо!
Бэст практисес в бизнес реалиях в большинстве случаев не работают.
В наших реалиях говнокод делает деньги. Особенно для энтерпрайз галёр. И такой подход тут — в самый раз. Для mvp некоторых тоже подойдёт. Только нужно покрывать тестами всё, чтобы можно было быстро отрефачить. :)
Контроллер нужен для того, чтобы преобразовывать HTTP-запрос в POCO и вызывать метод сервиса/команду, а затем возвращать результат в виде HTTP-ответа
Это он все делает сам, и код мы этот даже не видим. Мы уже достаточно хорошо абстрагировались от самого HTTP, но в принципе как я сказал, если у вас есть код для работы с вьюшками, или как вы сказали с http кодами и тд, то конечно стоит отделить контроллер и логику.
Я пишу такие API где принимаю модели и возвращаю модели, любые исключение ловит ExceptionHandler, то-есть все либо пройдет хорошо, либо вылетит в 500 ошибкой.
Если тип исключения домменный, то есть мой, я вывожу его пользователю
Всегда принимаю модель, возвращаю модель.
По умолчанию всегда 200\201 код, исключения ловит exceptionHandler который может поставить другой код в зависимости от типа ошибки.
Все такие вещи настраиваю немного выше, ведь у меня есть пайплайн
Вы доказываете, что механическое перетаскивание кода из класса А в класс Б не приносит пользы. И Вы правы!
Потому что в Вашем коде нет никакого BL. И все слои вместе взятые — это одно сплошное императивное редактирование ячеек БД через HTTP запросы. (Как и в большинстве проектов, которые я видел).
Я сам противник суеверного разбиения кода на слои. Но советую Вам попробовать выделить реальный BL, и посмотреть на эффект.
Если брать первую картинку под заголовком, то операция изменения баланса двух пользователей — это и есть BL. Вытащите эти две строчки в отдельный класс — вот это и будет началом вынесения кода из контроллера.
// я про эти строчки
fromUser.Balance -= amount;
toUser.Balance += amount;
Смысл не в том, чтобы назвать слои А, Б, В и менять местами код. Смысл в том, чтобы как можно бОльшая часть задачи решалась на высоком уровне абстракции, удобном для понимания. Но если эта абстракция отсутствует, то как Вы и говорите, толку в перетаскивании кода мало.
Это вы хорошо сформулировали, довольно часто именно это и происходит вместо разделения логики. Просто соблюдается некий карго-культ в котором должны быть контроллер и сервис — и это автоматически считается хорошей архитектурой, как будто что-то поменялось от того что бардак в контроллерах переехал в сервисы.
А с другой стороны — все-таки это правильное разделение и нужно найти какую-то конкретную грань когда есть смысл писать БЛ в контроллере, а когда в сервисе
Скорее, БЛ никогда не нужно писать в контроллерах, просто нужно найти грань, где БЛ, а где нет.
Исходя из этого, редактирование справочников это точно бизнес логика. REST API получает запрос, преобзовывает его согласно принятым протоколам, отдает на откуп бизнес логике.
Практически все это делается автоматически фреймворком и мы возвращаемся к ситуации в начале статьи. Вот как вариант habr.com/ru/post/505708/#comment_21713234
Не автоматически, а путём добавления http и подобных аннотаций к коду сервиса, не так ли? В таких приложениях необходимость работы с http возникает с самого начала ведь.
Пока контроллер какой-то асбтрактный, для тестов чисто, что ли, то, да, не имеет смысла выносить код в отдельный сервис. Но как только добавили http аннотацию, значит по этим правилам пора выносить в сервис его бизнес-логику, а абстрактный контроллер превратить в http-контроллер, с ответственностью распарсить запрос, делегировать выполнение сервису и сформировать ответ. руками это делать или аннотациями — деталь реализации контроллёра.
В последнее время я изучаю дополнительно ФП, и мой код уже поменялся как раз в эту сторону.
Я пишу логику в отдельных чистых функциях, они работаю только с данными, а эти функции уже использую в сервисе дабы описывать UseCase и дать этим функциям те данные которые им нужны.
Потому что в Вашем коде нет никакого BL. И все слои вместе взятые — это одно сплошное императивное редактирование ячеек БД через HTTP запросы. (Как и в большинстве проектов, которые я видел).
я тут недавно прям начал углубляться в ООП, книжки читаю, чтобы прям мышление поменять, так вот, я как бы сторонник идеи data-first т.е. сначала данные, разрабатывая что-то, думаю сначала о данных, а потом уже о том, как что-то с этими данными будет работать. Так вот, в одной из книг, я увидел такое описание что такое класс, что такое объект.
Там привели аналогию с базой данных. Что класс — это таблица, со всеми полями. А объект, это строка данных в этой таблице. И я сейчас еще больше БД воспринимаю как часть бизнес логики. а не просто хранилище данных. Да, логика работы в коде, но данные, как данные класса, они в базе.
Или, наоборот, будет принято решение хранить горячие данные в памяти процесса, чтобы избавиться от межпроцессных издержек.
Чисто теоретически, для любителей писать бизнес логику в контроллере, есть ICurrentHttpContextAccessor.
https://docs.microsoft.com/ru-ru/aspnet/core/fundamentals/http-context?view=aspnetcore-3.1
Я предпочитаю тестировать чистые функции, для этого я пишу в сервисах код который оборачивает их и используя репозитории и тд дает данные чистым функциям, я просто в статье эту тему не поднимал. А на счет 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 и вызывал правильную хранимую процедуру и с правильными аргументами, т.е. все очень примитивно и трудно зафакапить
Я считаю что подход уже не актуален, и мы шарперы вообще не любим вылазить за сам 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 не всегда читают, а потому наличие бизнес-логики в контроллере воспринимается ими как "на этом проекте принято писать логику в контроллере", соответственно момента отделения сервиса никогда не наступает. А ещё я сам пару раз оказался тем самым коллегой, который не понял гениальных умолчаний...
Вот и приходится в начале каждого нового проекта играть в "архитектурную угадайку", понадобится — не понадобится :-)
А [Http:post(login)]
или как там — это не работа с http?
По моим прикидкам у нас более половины контроллеров вообще не менялись с момента написания. Кода там мало и он понятный. Да и наши сервисы отличаются от контроллеров наличием пары аннотаций на методах\исключениях. Вынести сервис из контроллера — просто. Рефакторинг работает.
Не вижу причин вводить дополнительный уровень сервиса, если он не нужен и не будет отдельно использоваться. Радуют фразы: «а вдруг новый протокол понадобится». Всего не предусмотреть. В нашем проекте гораздо веротнее, что придется переписывать пару сервисов на GO или Kotlin.
Для этой цели замечательно подходит Swagger UI.
Если роутить через метаинформацию (например, аннотации), то нет проблем. С другой стороны, почему не видно роутинг у интерфейса, если видно у экземпляра?
Если их навесить на интерфейс, они работать не будут?
В принципе, можно сделать и так, чтобы они заработали. Но придётся написать свои реализации .AddControllers()
и .MapControllers()
, потому что стандартные через интерфейсы и правда не работают.
гораздо легче прочитать 100-строчный файл, чем продираться через 5000-строчный.
Разве современные ide не умеют в сворачивание тела? С трудом представляю себе, чтобы я хотел посмотреть на список методов контроллера и доблестно скроллил через код этих методов.
Умеют, но каждый сворачивать скорее всего придётся вручную и отдельно. Скроллить — быстрее.
Но главная проблема в изменчивости требований. С опытом начинаешь понимать, что есть неиллюзорный шанс однажды все переписать. Очень не хочется этого делать. Ни мне, как программисту, ни бизнесу. Отсюда все эти — «На одну кнопку две недели? Как??!!!». И поэтому подкладываешь соломку заранее. Где-то по простому — сразу не пишешь код в контроллерах или хранимках, потому что это дешево. Где-то оставляешь задел на вероятное будущее, которое может и не наступить. А заранее это делать дорого, да и есть более важные задачи. Все эти решения принимаются на основе опыта, видимого потенциала конкретного проекта и деадлайнов.
Пожалуй единственная категория проектов, где было все (или почти все) заранее известно — это для гос. учреждений по водопаду. На моей практике — такие проекты никогда серьезно не развивались. Их либо выкидывали совсем, либо заменяли другими. Я там мог сложить бизнес логику в хранимки и 5-10 лет это никого бы не парило.
Если мне скажут что у нас может появиться ещё один канал, к примеру через очередь, я просто могу получить экземпляр контроллера и использовать его в другом канале. Этот контроллер легко резолвится из DI.Восемь костылей из десяти.
Этот подход работает, но работает он грязно. Вместо того, чтобы иметь изолированную бизнес-логику и несколько параллельно существующих адаптеров для ее использования (http api, rmq, ...), вы прибили логику гвоздями к одному из них и теперь он будет тянуться везде — в другие адаптеры, в тесты. Тем самым связность системы и сложность ее понимания возрастает. Зачем модулю RMQ вообще знать что-то про HTTP API. Когда выйдет новая версия ASP.NET Core и вы захотите на нее перейти — вам придется обновлять и перетестировать не только http-модуль, а вообще все приложение целиком.
Так что для быстрого наколеночного проекта это нормальное решение (что никто и не оспаривал), а вот доказать, что это хороший универсальный подход у вас не получилось.
Просто в тот момент, когда вы вынесли из контроллера все, что связано с HTTP (биндинг в обе стороны, низкоуровневую валидацию, ответы с заголовками и прочим счастьем, роутинг и так далее, далее, далее), он перестал быть UI-контроллером, вот и все.
Если мне скажут что у нас может появиться ещё один канал, к примеру через очередь, я просто могу получить экземпляр контроллера и использовать его в другом канале. Этот контроллер легко резолвится из DI.
Как бы вам сказать… ваш контроллер все еще напрямую зависит от Microsoft.AspNetCore
. А откуда у меня этот пакет в бэкенд-сервисе, который крутится себе тихо в демоне и очередь разбирает?
столько примеров кода и не одной схемы, хоть бы в итог добавили
Меня немного смущает то, что существование юзера в коде проверяется по имени и!!! паролю. Т.е. два одинаковых юзера с разными паролями — это ок? Наверное проверка существования должна быть только по имени.
[Это был сарказм]
Ну а если по делу то цель создание сервисов бизнес логики равно как и репозиториев это разделение ответственности. Вы ведь упоминали в статье про трехслойную архитектуру (кстати слоев вполне может быть больше чем три, взять хотя бы тот же DDD) суть разделение по слоям не в следовании каким-то каргокультам внушенным нам рептилоидами а в разделении ответственности. Контроллеры отвечают за HTTP (ну или другой протокол обмена сообщениями), репозитории за взаимодействие с базой данных… Реализация IEmailService например за отправку почты. Я обычно бизнеслогику засовываю в .Net Standard либы чтоб не было соблазна «платформо-зависимый код» в них писать.
Слышали наверняка что слои верхнего уровня не должны знать о реализации нижнего уровня? Кто знает что там у него под капотом у реализации IEmailService, может быть он запускает механическую ногу, которая пинает спящего возле офиса бомжа, который несет вашему юзеру бумажку с распечатанным сообщением. В любом случае автор ведь использует интерфейс сервиса вместо того чтобы на прямую реализацию вставить туда где она используется будь то сервисы или контроллеры (т.е. подсознательно понимает что разделение ответственности не такая плохая идея).
Собственно к чему я это все тут написал — писать код в котроллере технически возможно, равно как и писать запросы к базе на прямую, или пинать бомжа напрямую без использования механической ноги. Но правильно ли это? Если этот проект планируется развивать и поддерживать многие годы то думаю писать все в контроллере плохая идею. Времена идут, все меняется, сегодня вы реквесты/респонсы через ASP.NET обрабатываете, завтра вместо контроллеров решите например Azure Functions с их HttpTriggers использовать после завтра например в Лямбды все завернете или еще чего другое придумается (тут уже упоминали Graph QL)… Подлодку делят на отсеки потому что если в одном из них пробоина, можно просто перекрыть перегородки и подлодка не потонет. Так же и тут, слои нужны для того чтобы в случае изменения реализации одного из них все остальные слои не пришлось переделывать, или вообще весь проект переписывать.
P.S. Ничего не имею против бомжей и не призываю их пинать. Считаю что этим людям просто где-то в жизни не повезло и надо помогать вернуться в социум.
Я наверное отстал от жизни, не слежу за трендами итд.
Но если мне программист покажет код, где контроллер не делает ничего, а просто вызывает сервис через ссылку на интерфейс, то я заставлю такой код переписать. Потому что он наблодил сущности без нужды.
Если есть два куска кода, которые делают одно и то же, с одинаковым быстродействием, одинаковыми сайд эффектами, то надо выбрать самый короткий. Его будет проще править, отлаживать, в нем будет меньше ошибок, и, самое главное, чем код короче, тем быстрее он будет написан.
Ну, тут на самом деле, есть интересный вопрос: а как переписать? Предположим, для интереса, что сервис, который он вызывает, используется и в других местах, поэтому заинлайнить код сервиса в контроллер не выйдет. Значит, надо избавляться от контроллера… но как, если фреймворк требует такой сущности?
Если сервис вызывается более чем в одном месте, то очевидно, что для сокращения кода, упрощения отладки и внесения изменений надо вызывать сервис.
Но в приведенном примере как раз сервис вызывается в одном месте.
Более того, в подавляющем большинстве случаев такие сервисы вызываются ровно в одном месте, поэтому создавать подобный код не нужно. Можно все написать в контроллере.
вызываются ровно в одном месте, поэтому создавать подобный код не нужно
Может быть не нужно быть таким категоричным?
Разные случаи бывают.
Например, пускай код и вызывается в одном месте, но он не влезает в 4 монитора. Что — оставлять в контроллере или все-таки попытаться перенести в другие классы?
Да много примеров таких есть.
По моему опыту, когда пишутся первые строчки, программист на 99% не знает будут они последними или нет. Так что лучше с самого начала держать все под контролем и следовать принципу SRP — контроллер отвечает за преобразование запросов в Dto и вызывает сторонние методы, преобразует exception в соответствующие коды возврата.
Когда в будущем мне понадобится переиспользовать бизнес логику (например в новом транспорте — консьюмере очереди), я просто вызову соответствующие бизнес сервисы и не буду трогать контроллеры (т.к. добавление нового транспорта не должно затрагивать существующие). Тем более, очень часто, нужно сделать быстро и нет времени на рефакторинг и разделение «лапшекода».
Если код не влезает на экран, то можно его побить на отдельные методы для улучшения читаемости. Так как отдельные методы не создают дополнительных сущнойстей и не увеличивают количество строк кода, а также положительно сказываются на отладке, так как при падении по стектрейсу проще определить где была ошибка.
Причем заранее создавать методы не обязательно, IDE сейчас хорошо умеют в Extract Method рефакторинг.
Есл программист не знает будущую структуру программы, то ему категорически не надо создавать дополнительные сервисы. Когда в будущем понадобится переписывать, то чем меньше кода будет, тем проще будет переписывать.
Я наверное пошатну ваше мироздание,"технический долг" не существует. Это красивая фраза чтобы программисты оправдывали бесполезное раздувание кода.
Я еще раз повторю, что более короткий кусок кода, содержащий меньше сущностей (классов) лучше по всем возможным метрикам, сколько бы вы не говорили про "технический долг".
И самое главное, ваше оправдание "технического долга" приводит к затртам реальных денег потому что программисты больше работают.
приводит к затртам реальных денег потому что программисты больше работают
Эта палка о двух концах, если все время упрощать, то мы можем придти к моменту, когда добавить новую фичу будет затратней чем сделать новую версию продукта. Так что технический долг имеет право на существование.
Это очередная сказка.
Чем меньше кода, тем проще его поменять. Писать что-то чтобы в будущем можно было поменять нечто, это тратить сейчас деньги и надеяться, что в будущем не придется платить.
Только в будущем все равно придется платить за изменения, потому что поменяются требования к программе. И вы в итоге заплатите два раза.
"Технический долг" существует только когда вы делаете заведомо недостаточно для удовлетворения требований, и выпускаете версию с недоработками, но она будет устраивать пользователей в какой-то мере и достигнет целей бизнеса.
Чем меньше кода, тем проще его поменять
Ну я могу ответить следующим — чем меньше грануляция участков кода, тем проще его переиспользовать. И задача инженера, на мой взгляд, как раз и в нахождении «золотой середины» не в падаясь в крайности.
"проще переиспользовать" это не метрика.
Если код функций вызывается в нескольких местах, то это сокращает объем кода. Если функция вызывается в одном месте, то неважно насколько просто её переиспользовать. на объем кода и плотность ошибок это не влияет.
это не метрика.
Для меня — метрика и еще какая:
Количество рефакторинга при добавлении новых фич, если у меня код нормально гранулирован, я не выделяю новые функции из существующих, а использую те блоки, которые я до этого создал.
Т.е. при идеальном коммите у меня будет Lines Changed:0 Lines Added:[количество строк для новой фичи].
А тепрь давайте посмотрим на коммитлог вашего релаьного проекта ;)
В мире розовых единорогов чего только не бывает.
коммитлог вашего релаьного проекта
Шутку оценил:)
Но я все же о том, что
задача инженера, на мой взгляд, как раз и в нахождении «золотой середины», без впадения в крайности.
Проблема "золотой середины" в том что ты заранее не можешь предугадать где эта середина. То что было "построено на века и с запасом" может завтра оказаться не нужным. А то, что казалсь мелкой фишкой может окааться самой важной фукциональностью.
Поэтому существует принцип LEAN — делать как можно меньше работы до того как появилось подтверждение её нужности.
предугадать где эта середина
В этом помогает «опыт».
LEAN — делать как можно меньше работы
Ага, а потом, когда нужно сделать какую либо работу «еще вчера» но требуется очень много времени, а его нет и городятся разного рода костыли и копипастинг.
А то и хуже — бизнесу говорят, что приложение превратилось в «кусок» и что-то делать новое, просто легче переписать.
Я не видел ни одного случая, чтобы разработчик угадал куда проект будет развиваться, кроме случаев когда он сам себе product manager.
Если нужно вчера, а еще очень много работы, то это значит что вы потратили время не на то, что нужно было делать или у вас очень нужная программа.
Про переписыване говорят слабаки, которые не умеют программировать. посмотрите на опыт microsoft с "переписыванием IE".
Напомню: группа программистов в редмонде сказала что IE (на тот момент версии 9 или 10) преватися в «кусок» и надо переписать.
В это время другая группа выпустила вполне пристойно (для IE) работающий IE11, который не только умел в достаточно современные на тот момент стандарты, а еще и умел эмулировать баги старых версий, чтобы говносайты корпоративные приложения продолжали работать в новой винде.
Родившийся параллельно edge пытался походить на Chrome поддержкой передовых, еще не до конца оформившихся, стандартов, но страдал от детских болезней и некоторых унаследованных, так как даже при "полно переписывании" умельцы тянули куски кода из старого IE.
Спустя несколько лет Edge сдох, а MS выкатил новую версию на базе Chromium. Так что переписываие любого большого проекта это путь в никуда. Рано или поздно менеджеры поймут, что проще переписывающих уволить, а устаревший продукт заменить УЖЕ СУЩЕСТВУЮЩИМ современным.
Можно пример?
Вы серьезно сравниваете длину кода на разных языках?
Так как можно написать на TS нельзя написать на JS. В этом и есть разница. разные языки — разные возможности — разные плотности ошибок.
Я даже больше скажу, если JS файл прогнать через TS компилятор можно много ошибок отловить.
Вообще магия, код вроде не поменялся, а плотность ошибок упала.
Поэтому нет смысл на разных языках саравнивать код.
То есть все-таки можем создать сколько захотим?
Не смущает что править придется в двух местах если будет не 9, а 10 мест?
А самое главное нет смысла так раздувать код ведь количество мест все равно определяется числом в одном месте (константой).
Если число мест — не константа, то вы еще 100500 ошибок совершите из-за преобразования типов и получении значения извне.
А чем простая проверка предусловия if (count in [2, 6, 9]) throw new Error(`count must be in [2, 6, 9], ${count} given`)
не угодила? С ним вы так же не сможете создать неверноый стол.
Можно, но при этом вы 100% залезете в технический долг, о котором даже не будете подозревать.
Неа, в описанных условиях никакого долга нет.
что делать, если сервис, реализуемый контроллером, должен быть Singleton либо Transient?
… какой-такой сервис? Написано же: не надо никакого сервиса, потому что код вызывается в одном месте.
Напомню, что в статье автор предлагает, с одной стороны, имплементировать интерфейс контроллером, и резолвить его из контейнера
Автор предлагает фигню, прямо скажем, и об этом я тут уже где-то писал.
т. е. я не могу запустить его по таймеру, по событию, либо из другого, например, мобильного интерфейса. Не могу (в общем, опять же, случае) запустить из Blazor'а, если использую SignalR, так как Scope SignalR не на один запрос.
Не можете. Потому что это не надо.
Я не могу сказать, хороший это поход или нет, но я точно знаю, что он, в долгосрочной перспективе, совсем не бесплатный.
Бесплатных подходов вообще нет, вам в любом случае придется что-то тратить. В данном случае вы не тратите на то, что вам не нужно.
Разделение на более мелкие сущности придумали не только для того, чтобы легче тестироваться и соблюдать DRY, его придумали еще и для того чтобы отделять «мух от котлет». Тут вот мы обрабатываем http запрос, тут мы его валидируем, тут работаем с очищенными данными, а тут — формируем ответ. И если уж так вышло, что что-то из этого выполнено на аннотациях, что-то автоматически выполняется до вызова контроллера, что-то после и на бизнес-логику остается только 1 строка — штош поделать, придется смириться с тем, что у нас экшн контроллера состоит из одной строки.
В моей практике даже был случай, когда подобное разделение на слои и следование общим правилам внутри всего проекта в итоге позволило генерировать экшны контроллеров и документацию к ним (и после безболезненно перегенерировать в случае необходимости).
А в общем случае, если отойти от примера с 1 строкой, я думаю вы со мной спорить не будете, что легче работать с функциями по 10-30 строк и классами по 300-500 строк с не большим количеством методов, чем с монстрами. Естественно, увлекаться разбиением сущностей тоже не стоит, создавая по сервису не каждый чих — всё хорошо в меру
Я не говорил что надо писать код без разделения на функции. Это вы уже сами придумали. Я говорил про дополнительные сущности (классы) в коде и создание "водопроводного кода".
Разделение кода на функции это хорошо. Создание классов, которые занимаются в основном вызовом других классов — плохо.
Если вы создали что-то что позволяет сократить объем написанного кода, это хорошо. Но это вовсе не означает что надо создавать классы без необходимости в других проектаъ, где подобной системы с генерацией кода нет.
Да какой-же водопроводный класс контроллера получается, если он преобразует http-запрос в dto? Или вы аннотации принципиально кодом не считаете?
HTTP запрос в DTO преобразует ASP.NET.
Или вопрос в чем-то другом?
Да какой-же водопроводный класс контроллера получается, если он преобразует http-запрос в dto?
Вот собственно этот и получается. Когда экшн в контроллере сводится к Action(HttpRequest request) => request |> bind<ActionInput> |> _service.Action |> bind<HttpResponse>
— это типичная сантехника, от которой лучше (именно что "лучше", а не "обязательно надо", потому что далеко не всегда надо или возможно) избавиться.
Если я правильно понял, то вы о случае когда объект http запроса передаётся в сервис? Я же не о нём, а том, когда этот объект, преобразуется (руками или аннотациями фреймворка — не суть) в какой-то DTO, который об http не знает ничего, а уж передаётся в сервис
Нет. Там же явно bind
написан, вот оно, ваше преобразование.
Вот если бы значки с непонятным смыслом (ассоциации с Хаскелем) словами написали, то было бы понятнее, возможно. bind в программировании для меня значит что-то к чему-то привязать, например, конкретное значение к плейсхолдеру в SQL запросе, или значение к переменной в замыкании.
Но, допустим, понял. Так чем плохо разбивать процесс преобразования данных на чётко выделенные этапы:
- принимаем http-запрос параметром в контроллере
- преобразуем в методе контроллера нужные понятия http в понятия какого-то сервиса, реализующего прикладную логику и ничего об http не знающего
- вызываем этот сервис
- его результат преобразуем в http ответ
- возвращаем ответ
Чёткое разделение ответственностей между контроллером и сервисом. Простота тестирования и контроллера, и сервиса по отдельности. Посадить писать сервис можно человека, ничего об 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
Человеческая фантазия довольно богатая, можно придумать любое обоснование любому созданному артефакту. Но на ситуацию с количеством строк, сущностей, плотностью ошибок и затратами на поддержку это никак не влияет.
С чего вы взяли что не имеет? Есть исследования, что очень даже имеет. Есть исседования плотности ошибок и влияния метрик на них, увеличение объема кода, показателя связности, глубины вызовов и цикломатической сложности прямо влияют на плотность ошибок.
Если вам нужно написать код на одну страницу, то надо написать код на страницу. Можно написать в одном месте, можно в другом, можно побить на функции, можно побить на несколько классов, которые вызывают друг друга. В любом случае чем меньше будет в совокупности классов и строк кода, тем код будет лучше.
В любом случае чем меньше будет в совокупности классов и строк кода, тем код будет лучше.
Нет. Есть и другие метрики "лучшести" кода, такие, как, например, связность и связанность, максимальный уровень вложенности и множество других. И некоторые из них часто противоречат "чем меньше будет в совокупности классов и строк кода, тем код будет лучше", поскольку прямо подразумевают введение новых классов и прочих абстракций для их улучшения.
Среди известных мне метрик нет ни одной, основанной на количестве классов и связей между ними, которая давала бы обратную корелляцию с плотностью ошибок.
Введние новых абстракций (классов) без уменьшения количества строк гарантироанно сделают код хуже. Это именно то, что я в своем первом комментарии сказал.
Я так понимаю вы примерно то же самое хотите сказать, но стесняетесь или просто не понимаете.
Нет, я хочу сказать другое: введение новых классов и связанное с этим увеличение количества строк может сделать код лучше. Его может стать проще понимать, проще тестировать, проще изменять без страха сломать всё приложение.
Это утверждение не подтверждается на практике. Это вера.
Да, сервис плодить ради лишнего класса плохая практика.
Но надо сразу решить, останется ли оно таким в обозримом будущем. Иначе при изменении архитектурных/бизнес-требований вам придётся его переписать примерно полностью, потому как повытаскивать сервисы из сотни методов контроллеров будет не простой задачей.
И да, положить тестирование почти всей бизнес-логики на e2e тестирование, тоже так себе затея…
У меня на прошлой работе тоже были любители писать код в контроллерах. Им так было удобно и понятно. Но всё стало плохо, когда оказалось, что кроме веба, им нужны ещё несколько интеграционных api. И, внезапно, у каждого апи свои требования и ограничения, да даже логика обработки запросов отличается.
Кончилось тем, что мне таки пришлось всё это переписать. После этого вся логика работы системы оказалась в сервисах, а в разнородных контроллерах осталась только специфичная логика (например конвертация входных данных во внутреннее представление и специфичная обработка ответа сервиса)
Сейчас у меня код бизнес-логики или обработки данных в контроллерах бывает только в набросках для проверки гипотез.
Абстракции позволяют писать легко тестируемый код за счет того, что модули (классы) становятся взаимозаменяемые.
Один раз написав интерфейс для работы с данными, мы можем реализовывать данный интерфейс в своих классах, где будет основная логика работы с конкретными хранилищами данных. Таким образом мы можем использовать mysql, а затем, если нам необходимо будет, мы напишем новый класс, где будет общение с другой бд, или для тестов мы можем написать датасурс, где данные будут сохраняться в переменные.
Так же автор в своём примере превращает DI в сервис локатор, нарушая принципы инкапсуляции
К примеру, когда я создаю что-то с нуля (те же петы), я не пишу сразу класс для работы с бд.
Вместо этого я создаю интерфейс и класс, который реализует данный интерфейс. При этом данный класс не общается с бд, а сохраняет в свои локальные переменные.
Это позволяет мне на этапе разработки совершенно не зависеть от бд и легко делать правки. Затем, когда будет готов модуль, я создаю новый класс, где реализовано общение с бд. Но при этом другие части системы вообще ничего не почувствуют, т.к. они как общались с интерфейсом, так и продолжают общаться с интерфейсом
Сначала можно сделать хранение в памяти, а затем заменить код на хранение в БД.
Я так и делаю =)
Создаю интерфейс UserDataasource
, в котором описываю методы.
Затем создаю класс UserDatasourceImpl
, в котором реализую предыдущий интерфейс и при этом сохраняю данные в переменных.
Затем, когда я завершаю разработку, я либо переписываю методы, либо создаю новый класс (Данный подход не сильно отличается от переписывания методов).
Но чаще я всё таки создаю вначале класс UserDatasourceImplMock
, для того, чтобы явно было видно, что это заглушка, а так же переиспользовать в тестах.
К тому же, общение с бд у меня чаще происходит через композицию в этих датасурсах (В приватную переменную кладу объект соединения при инициализации объекта датасурса).
Таким образом мы можем использовать mysql, а затем, если нам необходимо будет, мы напишем новый класс, где будет общение с другой бд
то-есть когда у Вас будет таска «переехать на другую бд» Вы сразу создадите новую реализацию и в DI её зарегистрируете вместо старой, а старая в свою очередь будет просто существовать? («А вдруг вернуться захотим?», «А вдруг захотим оба источника использовать? — тут придется ещё и интерфейс поменять»)
По мне так проще изменить сразу первую реализацию и всё.
Да, заменим в DI контейнере, и другие части этого не заметят.
Если вы захотите использовать оба — то пожалуйста, можете в одном датасурсе это реализовать, либо в разных, а доступ к ним осуществлять через репозиторий.
На случай "А вдруг вернуться захотим" есть инструменты контроля версий
Если что — у меня был (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. Здесь же ничего не мешает приделать атрибут, который будет указывать какой маршрут должен слушать этот реквест и, в этом случае, контроллеры становятся не нужны.
Да, на входе стоит fluent validator, который избавляет от необходимости проверять поля, пришедшие с фронтенда.
Да, реквест mutable. Это трейдофф, на который я сознательно иду, чтобы иметь возможность обогащать его по ходу пайплайна. При этом все обогащения контролируются внутри решения и покрыты тестами, поэтому я могу доверять реквесту.
Если я меняю реквест, то тут 2 варианта:
- Мне нужны дополнительные данные с фронта и больше мне их взять неоткуда. В этом случае фронт, как не крути, придется менять.
- Мне нужны дополнительные данные, которые я могу получить, например из сервиса авторизации. Тогда я просто изменю соответствующий PipelineBehavior и хендлер даже не узнает о том, что что-то изменилось.
По поводу зависимости домена от asp.net: хендлер — это не доменный сервис, а скорее application-сервис в контексте луковой архитектуры. Он использует доменные сущности и сервисы, но не является доменным сам по себе, поэтому домен независим от asp.net.
Handler, который принимает с фронта реквест уже и есть BL.
А как быть если нам нужно это же действие сделать в consumer workerа? Резолвить этот хэндлер?
структура которых сделана именно под очередь
Вот здесь я имею другую точку зрения. А архитектуре я стараюсь оперировать понятиями бизнес логики, которая практически никогда не должна пересекаться с физическим представлением (за исключением случаев когда мы занимаемся тюнингом производительности — тут, увы, делаем костыли).
Пускай в начале я для одного Http напишу больше кода, но потом, когда решим перебросить некоторые запросы на очередь (или что другое) — я все сделаю просто добавлением нового транспорта, без рефакторинга старого кода.
Это в общем-то уже спор не о логике в контроллерах, а о том, правильно ли переиспользовать код. Мой подход: важно различать действительно ли это одна и та же операция или просто сейчас так случайно получилось, что реализация у двух операций совпала. Ну и на мой взгляд не нужно ничего переиспользовать на перёд: всё-равно учесть всего, что случится в будущем не получится.
что случится в будущем
Да, к какому окончательному коду мы придем в будущем- точно никто не знает.
Но вот, то что в будущем, если проект «выстрелит» — у нас будет очень мало времени на рефакторинг и очень большой бэклог новых фич — вероятность такого стремиться к 100%.
И устранять «лапшенину» гораздо приятней в коде, где физический слой совершенно не пересекается с BL (как например в миграциях ASP.NET -> AspNet Core). И не забываем к концу года нам обещают .NET 5, у которого хз что будет с обратной совместимостью.
Я когда сам перешел на медиатор, мне контролеры с одной строкой казались ещё более бесполезными, я тогда написал решение похожее на эту библиотеку.
В итоге у меня появилась возможность вешать [Route] [Http...] на хендлеры медиатора.
Контроллер — это Interface Adapter. Таким образом, если библиотека выступает в роли Interface Adapter, то это не означает, что вы не используете данный слой.
Проблемы нет, если вы делегируете обязанности контроллера библиотеке, но будут проблемы, если вы будете писать бизнес логику в контроллерах.
Сейчас у меня флешбеки — пришел gRPC. И я хочу увидеть ваш код, как он справится с этой задачей.
Уже справлялся.
- Сгенерил BaseService на основании .proto
- Зашел в контроллер, выделил код в каждом методе
- CTRL + X
- Зашел в контроллер GRPC, сделал override каждого метода
- CTRL + V
И мы переехали на GRPC
Появится новый Pew Pew Done и болезненный процесс перехода MVC -> PPD.
А у вас уже будут выделенные сервисы.
Автору советую разобраться в принципах SOLID. Это даст понимание почему так делать не нужно.
Естественно данные принципы не являются догмой и рекомендуются для написания reusable кода. Если вы пишите финальный класс который никогда и ни кем не будет использоваться и поддерживаться то можно писать как вам хочется. Программирование профессия творческая. (Когда-то было модно писать весь код программы заглавными буквами)
Но честно говоря частое написание reusable кода вырабатывает стиль и через время подругому писать уже просто не получается.
Стиль написание кода очень хорошо показывает опыт человека его образ мышления и иногда даже характер :)
Мы в каждой задаче закладываем доп работу чтоб если-что, легче было потом, но ведь чаще это потом не наступает.
— А вдруг мы захотим сменить бд? — Камон, при смене бд самое просто поменять код под новую бд. А мигрировать данные, по мне так в разы сложнее.
— А вдруг мы захотим резко удалить наш проект и слиться? — Почему ж тут мы не делаем функционал для удаления всего по одной кнопке? (сарказм)
Базовые принципы солид (без фанатизма) — это чтобы легче было уже сейчас: написание кода, отладка, ревью, тестирование
Вы можете написать книгу одним предложением, а можете разбить ваш текст на абзацы, главы и иногда даже тома. При этом вряд ли у вас увеличится время написания книги если вы будите писать ее по главам а не сплошным монолитом. Я думаю вы не будете спорить что структурированный текст гораздо проще редактировать, тоже самое происходит и с кодом. Структурирование и декомпозиция кода не увеличивает трудозатраты если вы умеете это делать.
Подразумевается написание кода, не относящегося к ответственности контроллера, то есть нарушающего SRP, типа контроллер и http должен распарсить, и бизнес-действие соврешить, а не делегировать его совершение модели.
Основная мысль статьи — а получаем ли мы достаточно профита от соблюдения всех правил? Никто не покушается на правила как таковые — естественно надо разделять обязанности и все такое
Да почему же пустой? Он будет конвертировать http запрос в параметры вызова сервиса, опционально валидировать/санитайзить запрос, и потом ответ сериализовать или вью рендерить. По-моему, достаточно ответственностей, чтобы не говорить "пустой". Что всё это или почти всё реализуется через аннотации — дела не меняет.
P.S. KISS точно не нарушается, два класса (контроллер и сервис) для CRUD именно простыми становятся, проще некуда: глянул и сразу понятно за что отвечает один и другой. А если юнит-тесты пишите, то и YAGNI не нарушается.
Если применять принципы SOLID к коду приведенному автором то как минимум он нарушает S и D.
А почему мы не пишем код в контроллерах?