Когда проект молодой, писать «всё в одном классе» кажется быстрее. Создать отдельный интерфейс, вынести слой — это же лишняя минута! Но эта минута потом стоит часов: код невозможно покрыть тестами, замена ORM превращается в переписывание бизнес-логики, а новый человек в команде тратит день только на то, чтобы понять, где в методе заканчивается выборка из базы и начинается бизнес-правило.
Эти антипаттерны не привязаны к языку программирования — они встречаются и в Java, и в Go, и в Python. Примеры покажу на C#/.NET, но суть та же для любого стека. Три конкретных случая из реальных проектов — и как их исправить.
Антипаттерн 1. Всезнающий класс (God Object)
public OrderService( IImageService imageService, IDataProvider dbProvider, ILocalizer localizer, IMapper mapper, IDocumentService documentService, IRoleService roleService, IExternalApi externalApi, IProfileService profileService, EventProfileChangedRaiser eventRaiser, ICompanyInfoService companyInfoService, IAuthenticationService authenticationService, IEventSystemRaiser eventSystemRaiser, PagedResultService pagedResultService, IMessageOutbox outboxService, IHtmlSanitizer htmlSanitizer) { _imageService = imageService ?? throw new ArgumentNullException(nameof(imageService)); _dbProvider = dbProvider ?? throw new ArgumentNullException(nameof(dbProvider)); _localizer = localizer ?? throw new ArgumentNullException(nameof(localizer)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _documentService = documentService ?? throw new ArgumentNullException(nameof(documentService)); _roleService = roleService ?? throw new ArgumentNullException(nameof(roleService)); _externalApi = externalApi ?? throw new ArgumentNullException(nameof(externalApi)); _profileService = profileService ?? throw new ArgumentNullException(nameof(profileService)); _eventRaiser = eventRaiser ?? throw new ArgumentNullException(nameof(eventRaiser)); _companyInfoService = companyInfoService ?? throw new ArgumentNullException(nameof(companyInfoService)); _authenticationService = authenticationService ?? throw new ArgumentNullException(nameof(authenticationService)); _eventSystemRaiser = eventSystemRaiser ?? throw new ArgumentNullException(nameof(eventSystemRaiser)); _pagedResultService = pagedResultService ?? throw new ArgumentNullException(nameof(pagedResultService)); _outbox = outboxService ?? throw new ArgumentNullException(nameof(outboxService)); _htmlSanitizer = htmlSanitizer ?? throw new ArgumentNullException(nameof(htmlSanitizer)); }
15 зависимостей в конструкторе. Этот класс знает обо всём: и про картинки, и про аутентификацию, и про публикацию событий, и про HTML-санитизацию.
Почему это плохо
Тестирование. Чтобы написать один юнит-тест, нужно замокать 15 зависимостей. Настройка моков может занять больше времени, чем написание самого теста.
15 причин для изменения. Изменилась работа с изображениями — меняем этот класс. Изменился внешний API — опять этот класс. Любое изменение в любой части системы с высокой вероятностью затронет этот сервис.
Невозможно понять ответственность. Класс, который умеет и санитизировать HTML, и аутентифицировать, и публиковать события — не имеет чёткой зоны ответственности. Это God Object.
Как исправить
Разделить по ответственности. Если в классе есть работа с заказами, работа с профилями и публикация событий — это три разных сервиса. Каждый со своими 2-3 зависимостями, каждый легко тестируемый.
// Было: один класс на 15 зависимостей public class OrderService { /* всё */ } // Стало: каждый класс отвечает за своё public class OrderProcessor(IOrderRepository orders, IPricingService pricing) { /* бизнес-логика заказа */ } public class OrderNotifier(IEventSystemRaiser events, IOutboxLogWrapper outbox) { /* публикация событий */ } public class ProfileUpdater(IProfileService profiles, IMapper mapper) { /* работа с профилями */ }
Антипаттерн 2. Бизнес-логика знает о формате хранения (Leaky Abstraction)
public class UserRatingCalculator : IUserRatingCalculator { private const int DefaultRating = 10; private const int ImageRating = 50; public int Calculate(UserDb user) { if (user == null) throw new ArgumentNullException(nameof(user)); if (user.JobExperiences == null) throw new ArgumentNullException(nameof(user.JobExperiences)); var rating = 0; if (!string.IsNullOrEmpty(user.ImageUrl)) rating += ImageRating; if (!string.IsNullOrEmpty(user.ParentName)) rating += DefaultRating; if (!user.JobExperiences.IsNullOrEmpty()) rating += DefaultRating; return rating; } }
На первый взгляд — чистый, маленький класс. Приятно читать. Но посмотрите на входной параметр: UserDb. Это объект базы данных, сущность Entity Framework.
Почему это плохо
Изменение схемы БД ломает бизнес-логику. Переименовали колонку ImageUrl в AvatarPath — бизнес-калькулятор рейтинга перестал компилироваться. Хотя бизнес-правило «если у человека есть фото — добавить баллов» не изменилось.
Артефакты ORM просачиваются в домен. Обратите внимание на две строки:
if (user.JobExperiences == null) throw new ArgumentNullException(nameof(user.JobExperiences)); // ... if (!user.JobExperiences.IsNullOrEmpty()) rating += DefaultRating;
Для первой строки null — ошибка. Для второй — один из допустимых исходов. Это костыль из-за lazy loading в EF: если навигационное свойство не загружено — оно null, и это не значит «нет данных», это значит «забыли сделать Include». Чисто инфраструктурная проблема, которая просочилась в бизнес-логику и создала код, противоречащий сам себе.
Невозможно сменить хранилище. Захотели перейти с реляционной БД на document store — нужно переписывать калькулятор рейтинга, хотя бизнес-правила не менялись.
Как исправить
Ввести доменную модель, которая ничего не знает о базе данных:
// Доменная модель — только бизнес-данные public interface IUser { bool HasImage { get; } bool HasParentName { get; } bool HasJobExperience { get; } } // Калькулятор работает с доменной моделью public class UserRatingCalculator : IUserRatingCalculator { private const int DefaultRating = 10; private const int ImageRating = 50; public int Calculate(IUser user) { var rating = 0; if (user.HasImage) rating += ImageRating; if (user.HasParentName) rating += DefaultRating; if (user.HasJobExperience) rating += DefaultRating; return rating; } }
Маппинг из UserDb в IUser — задача инфраструктурного слоя. Костыль с проверкой lazy loading уходит туда же — калькулятор больше не знает, что такое EF.
В идеале стоит пойти дальше — к полноценной ООП-модели, где логика расчёта рейтинга инкапсулирована в самом объекте User. Но даже предложенный вариант с интерфейсом — это бесплатный рефакторинг: он не требует менять архитектуру, только ввести прослойку между БД и бизнес-логикой.
Антипаттерн 3. Доменная логика прибита к фреймворку (Dependency Inversion violation)
public async Task<PaymentResult> ProcessPayment(long orderId, CancellationToken ct) { var result = new PaymentResult { OrderId = orderId }; // 1. Инфраструктура: выборка из БД var dbOrder = await _dbService.GetOrderById(orderId, ct); if (dbOrder == null) throw new OrderNotFoundException(orderId.ToString()); // 2. Бизнес-логика: проверка статуса var status = DataHelper.GetPaymentStatus(dbOrder.Payment); if (status != PaymentStatus.Denied) throw new StatusNotAllowedException(orderId.ToString(), status); // 3. Инфраструктура: вызов внешнего API var orderResponse = await _externalApi.GetOrderDetails(orderId); result.PayStatus = orderResponse.Status; // 4. Бизнес-логика: обработка статусов switch (orderResponse.Status) { case OrderStatus.NotFound: case OrderStatus.AlreadyPaid: case OrderStatus.Locked: return result; case OrderStatus.Pending: if (!orderResponse.OrderId.HasValue) throw new InvalidDataException("Order id was null"); result.OrderId = (long)orderResponse.OrderId; result.Amount = orderResponse.Amount; break; } // 5. Инфраструктура: создание записи в БД var payment = new PaymentDb { OriginalOrderId = orderId, PaymentOrderId = orderResponse.OrderId.Value, Description = $"Payment for order {orderId}", Amount = orderResponse.Amount.Value }; _dbContext.Add(payment); // 6. Инфраструктура: генерация URL платёжной системы result.PayUrl = GeneratePayUrl(payment.Id.ToString("N")); // 7. Инфраструктура: сохранение await _dbContext.SaveChangesAsync(ct); // 8. Инфраструктура: логирование await _logService.SaveProcessLog(payment.Id, $"Payment started, amount: {orderResponse.Amount}"); return result; }
56 строк, в которых бизнес-логика, работа с БД, вызов внешнего API и генерация URL платёжной системы перемешаны в одну кашу.
Почему это плохо
Невозможно протестировать бизнес-правила отдельно. Чтобы проверить логику обработки статусов (switch по OrderStatus, строки 20–33), нужно замокать и БД, и внешний API, и dbContext, и логгер.
Смена платёжной системы ломает бизнес-логику. Метод GeneratePayUrl — это деталь конкретного платёжного провайдера. Если завтра мы переходим на другой банк, придётся лезть в тот же метод, где живёт бизнес-логика обработки статусов.
Смена ORM ломает бизнес-логику. Прямая работа с _dbContext.Add() и SaveChangesAsync() — это привязка к EF. Решили перейти на Dapper или вообще на нереляционную базу — переписываем метод целиком, хотя бизнес-правила не менялись.
Как исправить
Разделить на слои. Бизнес-логика обработки статусов — в доменный слой. Работа с БД, внешним API и платёжной системой — в инфраструктурный. Координация между ними — в слой приложения.
// Доменный слой — чистая бизнес-логика public class PaymentProcessor { public PaymentDecision Evaluate(OrderInfo order) { return order.Status switch { OrderStatus.NotFound or OrderStatus.AlreadyPaid or OrderStatus.Locked => PaymentDecision.Skip(order.Status), OrderStatus.Pending when !order.OrderId.HasValue => throw new InvalidDataException("Order id was null"), OrderStatus.Pending => PaymentDecision.Proceed(order.OrderId.Value, order.Amount), _ => throw new InvalidDataException($"Unexpected status: {order.Status}") }; } } // Слой приложения — координация public class PayOrderHandler { public async Task<PaymentResult> Handle(long orderId, CancellationToken ct) { var order = await _orderRepository.GetById(orderId, ct); _statusValidator.EnsurePaymentAllowed(order); var orderInfo = await _orderService.GetOrderDetails(orderId); var decision = _paymentProcessor.Evaluate(orderInfo); if (decision.ShouldSkip) return PaymentResult.From(decision); var paymentId = await _paymentRepository.Create(order, decision); var payUrl = _paymentGateway.GenerateUrl(paymentId); return PaymentResult.Success(paymentId, payUrl, decision.Amount); } }
Бизнес-логика обработки статусов — в PaymentProcessor. Ноль зависимостей, тестируется тремя строчками. Работа с БД — за интерфейсом IOrderRepository. Платёжная система — за IPaymentGateway. Завтра меняем банк — трогаем только реализацию IPaymentGateway.
Правило, которое всё это объединяет
Все три антипаттерна нарушают одно правило — Dependency Rule из Clean Architecture (Robert C. Martin):
Зависимости в коде должны идти только внутрь — к бизнес-логике.
Бизнес-логика не должна знать, как хранятся данные. Не должна знать, какой у нас ORM. Не должна знать, какую платёжную систему мы используем. Всё это — детали реализации, которые живут снаружи.
На практике это означает разделение на слои:
Доменный слой — бизнес-сущности, бизнес-правила, интерфейсы. Не зависит ни от чего. Здесь живут IUser, PaymentProcessor, enum-ы статусов. Ни EF, ни ASP.NET, ни внешних API.
Слой приложения — координация и агрегация. Зависит только от домена. Здесь живёт PayOrderHandler, который достаёт данные из нескольких источников и передаёт их в доменную логику. Сегодня данные приходят из трёх API, завтра закэшировали в одну базу — бизнес-логика не меняется.
Инфраструктурный слой — самый «грязный». EF, контроллеры, внешние API, маппинг, DI-контейнер. Знает обо всём. Но от него зависит только точка входа.
Слоёв нужно столько, сколько нужно. Для MailingService хватит двух. Для сложного платёжного сервиса — три. Для простого CRUD — может, и одного достаточно. Не надо городить архитектуру ради архитектуры.
«Но так же быстрее!»
Да, написать всё в одном классе — быстрее. На минуту. Создать интерфейс, вынести слой — это 60 секунд, которые ты экономишь сейчас. А потом:
Новый человек в команде тратит день, чтобы разобраться, где в методе бизнес-логика, а где выборка из базы
Замена платёжной системы превращается в рефакторинг бизнес-логики
Юнит-тест на бизнес-правило требует мока 15 зависимостей
Оценка «добавить новый статус — 2 часа» превращается в «2 дня, потому что я не понимаю, что тут происходит»
Эти антипаттерны не появляются сразу. Они накапливаются постепенно — «ну ладно, один раз можно», «тут всего одна зависимость лишняя», «потом отрефакторим». Именно поэтому их так сложно заметить вовремя.
Недели кодирования сохраняют часы проектирования.
Комментарии (26)

MikeIva
08.04.2026 08:34Кажется, это можно сформулировать проще: каждая функция должна работать с одним объектом. Если нужно работать с еще одним объектом, вместо этого нужно вызывать соответствующую функцию.
И с практической точки зрения - если код вашей функции занимает больше экрана, вероятно, что она плохо декомпозирована.
illusionist1nemo Автор
08.04.2026 08:34«Функция больше экрана — повод задуматься» — хорошая маркер. Но, бывает и наоборот: функция в 10 строк, но класс с 15 зависимостями в конструкторе — функция формально короткая, а на деле God Object.
Но как первый фильтр — работает.

ProgerMan
08.04.2026 08:34Я бы добавил 4-ю: на каждый чих выкидывать исключение вместо возвращения значения.
Например, если DI настроен нормально, то строки типа
_imageService = imageService ?? throw new ArgumentNullException(nameof(imageService));
не имеют смысла. Там всегда не null.А когда исключения лезут в бизнес-логику, то из-за этого обычно код обрастает множеством try-catch на всех уровнях и никогда не знаешь, где именно это исключение будет перехвачено и как обработано. Более того, оно ещё по-разному может обрабатываться в разных местах, что приводит к созданию отдельных исключений и их обработке для какого-то отдельного случая. В одном проекте я со временем удалил с десяток таких одноразовых исключений.
Сейчас один сервис рефакторил. Так там чуть ли не каждый метод обёрнут в исключения, большинство из которых никогда не сработают. Даже вокруг такого кода, который никогда не выбросит исключения.
Если очень надо, а рефакторить такой код дорого, можно сделать PublicException, который будет показывать пользователю ошибку как есть. А всё остальное всё равно должно быть обработано. Желательно в одном месте (
ExceptionHandlerMiddleware). Исключение должно сообщать о проблеме в коде/логике, а не быть способом передачи информации.Тот же необработанный ArgumentException сообщает пользователю название параметра с неверными данными. Зачем это надо? (вопрос риторический)

illusionist1nemo Автор
08.04.2026 08:34Например, если DI настроен нормально, то строки типа _imageService = imageService ?? throw new ArgumentNullException(nameof(imageService)); не имеют смысла. Там всегда не null.
Про
ArgumentNullExceptionв конструкторах — если следовать принципу, что слой приложения не знает об инфраструктуре, то он не может полагаться на то, что DI-контейнер всё правильно зарегистрировал. Проверка на null — это контракт метода, а не дублирование работы DI. К тому же сервисы покрываются юнит-тестами, где DI нет — и эти проверки защищают от неожиданного поведения в тестах.А когда исключения лезут в бизнес-логику, то из-за этого обычно код обрастает множеством try-catch на всех уровнях и никогда не знаешь, где именно это исключение будет перехвачено и как обработано. Более того, оно ещё по-разному может обрабатываться в разных местах, что приводит к созданию отдельных исключений и их обработке для какого-то отдельного случая. В одном проекте я со временем удалил с десяток таких одноразовых исключений.
Про исключения vs Result-паттерн — тема холиварная. Мой подход: исключения — нормальный механизм, но они не должны перехватываться на каждом уровне. Бизнес-исключение (
OrderNotFoundException,StatusNotAllowedException) должно долетать до единого обработчика (middleware), который формирует ответ клиенту. Если вместо этого на каждом слоеtry-catchс разной логикой — тогда да, проблема. Но это проблема не самих исключений, а их неправильной обработки.
ProgerMan
08.04.2026 08:34Если я пишу какой-то сервис. для API, то я уже знаю о работе DI и что он мне вернёт. Нет смысла придумывать, что там что-то внезапно поменяется и из-за этого засорять конструкторы и тесты кодом, который никогда не будет выполнен и тестирует бесполезные вещи. Это может подойти для библиотеки, но не для класса внутри сервиса с известными параметрами работы. Иначе "на всякий случай" можно и код для .NET Core 3 версии написать, имея сервис на 10-й. Вдруг чего изменится.

illusionist1nemo Автор
08.04.2026 08:34Справедливо, для внутреннего сервиса это вопрос конвенции команды. Я привык ставить — дешёвая страховка, но понимаю аргумент про лишний шум.

illusionist1nemo Автор
08.04.2026 08:34Ещё добавлю про Result vs исключения: Result-паттерн хорошо работает в языках, где он встроен в систему типов — Rust, F#. Там компилятор заставляет обработать все варианты, и забыть не получится. В C# он добавляет церемонию: каждый вызывающий метод должен проверить
result.IsSuccess, и если забыл — ошибка молча проглатывается. С исключениями наоборот — если не обработал, оно само долетит до middleware. В новом C# обещают discriminated unions, возможно с ними стоит пересмотреть подход.
monco83
08.04.2026 08:34Тут поддержу. Когда у тебя в методе один сервис возвращает "монаду", другой метод возвращает "монаду", но уже другую, то как-то грустно становится со всеми этими Optional. Несколько раз пытался перейти на этот паттерн и всякий раз бросал.
P.S. Правда это про общий случай. Почему в ваших примерах исключения вызывают у читателей вопросы я, в целом, понимаю.

monco83
08.04.2026 08:34>Тот же необработанный ArgumentException сообщает пользователю название параметра с неверными данными. Зачем это надо? (вопрос риторический)
Пользователь тут - инженер тех. поддержки, а надо это для того, чтобы дать больше контекста для фикса бага.

Kerman
08.04.2026 08:34Изменение схемы БД ломает бизнес-логику.
Так и должно быть, если трогались используемые поля. А если используемые поля не трогались, то ничего не сломается. Или я не пойму, вы хотите убрать используемые поля и получить запускающийся сервис? А что такой сервис вам насчитает?
Переименовали колонку
ImageUrlвAvatarPath— бизнес-калькулятор рейтинга перестал компилироваться.Во-первых, если вы уверены в том, что смысл не поменялся, переименуйте в модели, IDE вам всё переименует, всё скомпилируется.
Если не уверены - идите по каждому месту с ошибкой и глазами проверяйте корректность использования. Это гораздо лучше, чем получать ошибки в рантайме.
И я не пойму, как вам интерфейс поможет. Ну вынесли вы ImageUrl в интерфейс. Ну поменяли в БД на AvatarPath и что? Ну теперь вам нужно всё то же самое, только ещё и интерфейс поменять.

illusionist1nemo Автор
08.04.2026 08:34Да, при переименовании IDE покажет все места — и это хорошо.
Но не все изменения в БД должны триггерить изменения в бизнес-логике. Простое переименование — не должно. Изменение типа — тоже не всегда. Например, ImageUrl была строкой-ссылкой, а стала коллекцией файлов с метаинформацией. Бизнес-правило «если у пользователя есть фото — добавить баллов» не изменилось. Но без абстракции калькулятор рейтинга ломается: вместо
string.IsNullOrEmpty(user.ImageUrl)теперь нужноuser.Images.Any(). С интерфейсомIUserкалькулятор по-прежнему проверяетHasImage— и ему всё равно, строка там внутри или коллекция.Суть не в том, чтобы спрятать ошибку компиляции, а в том, чтобы изменение структуры хранения не создавало diff в бизнес-логике.

GarfieldX
08.04.2026 08:34При чем тут Шарпей то? :)
Эту дичь ведь можно сотворить на любом языке.
У меня вот всё описанное имеется в проекте, но иначе не получалось. Всю эту опу прекрасно вижу и понимаю. Когда удастся выдохнуть проект ждёт мега рефакторинг. И это не Шарпей и не джава.

illusionist1nemo Автор
08.04.2026 08:34Эти антипаттерны не привязаны к языку программирования — они встречаются и в Java, и в Go, и в Python. Примеры покажу на C#/.NET, но суть та же для любого стека. Три конкретных случая из реальных проектов — и как их исправить.

fuggy
08.04.2026 08:34Ошибка которую считают стандартом из-за удобства — DI. Конечно это не так плохо как ServiceLocator, но проблема в том что она делает много неявного и скрывает инициализацию. Явно лучше неявного, чем потом удивляться, почему в синглтоне вызывается Scope сервис.

monco83
08.04.2026 08:34>В идеале стоит пойти дальше — к полноценной ООП-модели, где логика расчёта рейтинга инкапсулирована в самом объекте
User.
Ага, рейтинг пользователя с точки зрения какого процесса, с точки зрения какого представления о пользователе? Почему этот рейтинг гвоздями должен быть прибит к основной модели? Пихайте тогда всё, что относится к пользователю в "модель" пользователя и получите God-object, с которым вы так успешно боролись в пункте первом.
>Ввести доменную модель, которая ничего не знает о базе данных
Большая ложь. Если вы модель EF прикроете фиговым листочком интерфейса вы никуда не уйдёте от зависимости ваших моделей от фреймворков. Весь классический DDD намертво прибит гвоздями к используемой в проекте ORM, мнимая абстракция от персистентности - ложь, которая только запутывает дело.

monco83
08.04.2026 08:34>Смена ORM ломает бизнес-логику. Прямая работа с
_dbContext.Add()иSaveChangesAsync()— это привязка к EF. Решили перейти на Dapper или вообще на нереляционную базу — переписываем метод целиком, хотя бизнес-правила не менялись.
Вы не сможете перейти на Dapper c EF, какими бы слоями абстракций не обмазывались. EF умеет в ChangeTrackker, Dapper - нет. Весь этот "бизнес-код", отделённый от "инфраструктуры" посредством "абстракции"-репозитория написан так, как написан, именно потому, что знает, что изменения в модели могут быть сохранены в персистентном слое, т.е., бизнес-слой неявно много знает о том, что стоит за IOrderRepository и за его SaveChanges().
Можно, конечно, пойти другим путём, как вы примерно и переделали пример. Не использовать паттерны вида UnitOfWork (SaveChanges()), сделать операции сохранения более гранулярными. Но это означает:
1. Ради абстракции лишать себя удобства, которые предоставляют фреймворки.
2. ChangeTracker для Dapper вы в рукопашку всё равно не забабахаете, поэтому ваша имплементация неизбежно будет пытаться сохранить агрегат целиком, вместо того, чтобы одну строчку в табличку добавить. Со всеми вытекающими проблемами производительности и конфликтов изменений.
3. Опять же приходим к тому, что интерфейс репозитория диктуется не одной только бизнес логикой (сверху-вниз), но и соображениями о персистентности. Если вас (зря) волнует перспектива перехода с EF на Dapper или на произвольную NoSQL, то и в интерфейс вы должны закладывать возможность простой имплементации на любом из этих движков. Это намного труднее, чем просто выбрать один framework.
TrueRomanus
Если с первыми двумя антипаттернами я согласен правда непонятно почему это архитектурные проблемы то вот третий стоит обсудить. Название "Доменная логика прибита к фреймворку" и Вы демонстрируете как в сложном примере просто вынесли часть кода куда-то еще, причем тут вообще неважно куда, можно было вынести и в приватные (или публичные если надо их тестировать) методы. Я к тому что во первых на архитектурную проблему это снова не тянет. По факту если на проекте хоть в каком-то виде пишутся тесты то такого кода скорей всего в принципе не будет написано. А в случае когда тестирование только ручное то когда наоборот алгоритм целиком в одном месте и все понятно сразу из одного места, в некоторых случаях это скорее хорошо чем плохо, почему потому что легко понять горизонт логики. Я видел обратную сторону такого подхода когда открываешь метод, там 3 строчки но чтобы понять логику что происходит надо прыгать по сотне других методов по перекрестным ссылкам.
illusionist1nemo Автор
По поводу «вынести в приватные методы» — если вынести switch в приватный метод того же класса, класс всё равно зависит от
dbContext,externalApiи_logService. Тестировать бизнес-правило отдельно от инфраструктуры не получится — моки тех же 5 зависимостей. Суть не в том, чтобы код стал короче, а в том, чтобы бизнес-логика не зависела от инфраструктуры.Про обратную сторону — это может быть проблемой. Именно поэтому в статье написано «Слоёв нужно столько, сколько нужно. Для простого CRUD — может, и одного достаточно.» Если алгоритм линейный и тестов нет — вынос в 10 классов сделает только хуже. Но когда появляются тесты или меняется инфраструктура (другой банк, другой ORM) — цена «всё в одном месте» резко растёт.
TrueRomanus
Ну хорошо можно вынести switch в статичный метод и тогда зависимости и мокинг перестают быть проблемой.
"Слоёв нужно столько, сколько нужно" да вот только как понять сколько нужно? Это как раз фундаментальная проблема всех архитектур, когда люди начинающие проект думают что их слишком много или слишком мало а потом оказывается что наоборот, тут просело а там наоборот вылезло. И для архитектуры это выглядит как совет "не делай плохо, делай хорошо". Я к тому что что такие советы слишком абстрактны и очень вероятно что человек прочитавший такой совет не сможет применить его в своем проекте потому что ту грань о которой Вы говорите определить очень сложно.
illusionist1nemo Автор
Со статическим методом — для чистого switch это сработает, но это процедурный подход. В ООП логичнее инкапсулировать логику в сам объект. Логика принятия решения живёт в самом объекте решения — ни зависимостей, ни моков, ни статических утилит.
Про «сколько слоёв нужно» — для большинства задач классических трёх слоёв из Clean Architecture хватает. Если становится тесно — стоит делить слой на фичи (vertical slices), а не добавлять новые слои. Да если весь сервис — это «взять из базы, вернуть клиенту» — одного слоя достаточно.
monco83
>Со статическим методом — для чистого switch это сработает, но это процедурный подход. В ООП логичнее инкапсулировать логику в сам объект. Логика принятия решения живёт в самом объекте решения — ни зависимостей, ни моков, ни статических утилит.
А если подход назвать не процедурным, а функциональным, то чем плохо?
alixa
Тесты могут быть интеграционными через API например, я вот модульные не уважаю от слова совсем, только для в определенных кейса[, интеграционные через API отлично все могут проверить и не заставляют что-то менять и выносить, реже ломаются и быстрее пишутся
illusionist1nemo Автор
Юнит-тесты и интеграционные — не конкуренты, а дополняют друг друга. Юнит-тесты быстрые, запускаю десятки раз во время разработки — поменял код, прогнал, увидел результат. Интеграционные через API проверяют систему целиком, но полный прогон на полчаса+ каждый раз не запустишь. И разделение на слои помогает не только с тестированием: когда бизнес-логика в отдельном классе без зависимостей — её проще читать, проще менять, проще передать другому разработчику. Тестируемость — одна из целей, но далеко не единственная.
alixa
На текущем проекте интеграционные за минуту всю систему проверяют, тут видимо от прямоты рук все зависит и от контекста проекта. Я не писал что против слоев (горизонтальных), DDD, вертикальных, хотя думаю что все это шляпа, надо что-то новенькое и более красивое )
ИМХО
Разделение на слои, микросервисные и любое другое разделение помогает потом когда делаешь фичу как оленю скакать между тонной файлов и проектов и добавлять по одной строке кода и еще тренирует память, чтобы нигде и ничего не забыть )
А ну и локально потом минут 20 все это хозяйство собирать, на медленных обычных ПК до часа и более..
illusionist1nemo Автор
Минута на всю систему — отлично, значит проект позволяет. На более крупных проектах полный прогон занимает десятки минут, и без быстрых юнит-тестов цикл разработки сильно замедляется. А долгая сборка — это вопрос настройки инкрементальной компиляции, не архитектуры.
alixa
Для примера, сегодня написал два модульных теста, примерно по 100 строк каждый. Проверил позитивный и негативный сценарии. Запустил — вроде всё good. Затем написал интеграционный тест строк на 60 (проверяет туже функцию, только уже через контроллер), и вуаля — именно интеграционный тест отловил багу, поскольку как оказалось перепутал в рабочем коде идентификаторы сущностей.