Архитектура Вертикальных Срезов. Где Живёт Общая Логика? Окончание
Начало
Продолжение
Совместное использование между функциями
Что насчёт совместного использования кода между несвязанными функциями в VSA? CreateOrder должен проверять наличие клиента. GenerateInvoice должен рассчитывать налог. И Orders и Customers должны форматировать уведомления. Это не совсем вписывается в папку Shared определённой функции. Куда деть эту логику?
1. Действительно ли нужен общий код?
Большинство случаев «совместного использования» между функциями — это просто замаскированный доступ к данным. Если CreateOrder нужны данные о клиентах, он напрямую обращается к БД. Он не обращается к функции Customers. Каждый срез владеет своим доступом к данным. Сущность Customer является общей (находится в домене), но Orders и Customers не имеют общего сервиса.
2. Когда действительно нужна общая логика
- Логика домена (бизнес-правила, калькуляции) → Domain/Services
- Инфраструктура (внешние API, форматирование) → Infrastructure/Services
// Domain/Services/TaxCalculator.cs
public class TaxCalculator
{
public decimal CalculateTax(
Address address, decimal subtotal)
{
var rate = GetTaxRate(address.State, address.Country);
return subtotal * rate;
}
}
CreateOrder и GenerateInvoice могут использовать этот код без привязки друг к другу.
Прежде чем создавать какой-либо кросс-функциональный сервис, спросите себя: может ли эта логика размещаться в доменной сущности? Большая часть «общей бизнес-логики» на самом деле представляет собой доступ к данным, доменную логику, принадлежащую сущности, или преждевременную абстракцию.
Если нужно вызвать побочный эффект в другой функции, лучше использовать сообщения и события. В качестве альтернативы, функция, которую вы хотите вызвать, может использовать фасад (открытый API) для этой операции.
3. Когда дублирование — правильный выбор
Иногда «общий» код только кажется общим:
// Features/Orders/GetOrder
public record GetOrderResponse(Guid Id, decimal Total, string Status);
// Features/Orders/CreateOrder
public record CreateOrderResponse(Guid Id, decimal Total, string Status);
Они идентичны. Возникает соблазн создать SharedOrderDto. Не поддавайтесь ему.
На следующей неделе в GetOrder понадобится URL трекинга. Но CreateOrder выполняется до отправки, поэтому не имеет URL. Если бы используете общий класс DTO, у вас появится свойство, допускающее NULL, которое в половине случаев остаётся пустым, что сбивает с толку. Дублирование дешевле, чем неправильная абстракция.
Структура Проекта
Вот как может выглядеть структура проекта VSA:
📂 src
└📂 Features
│ ├📂 Orders
│ │ ├📂 CreateOrder
│ │ ├📂 UpdateOrder
│ │ └📂 Shared
│ ├📂 Customers
│ │ ├📂 GetCustomer
│ │ └📂 Shared
│ └📂 Invoices
│ └📂 GenerateInvoice
└📂 Domain
│ ├📂 Entities
│ ├📂 ValueObjects
│ └📂 Services
└📂 Infrastructure
│ ├📂 Persistence
│ └📂 Services
└📂 Shared
└📂 Behaviors
-
Features — Изолированные срезы. Каждый со своими моделями запроса/ответа. -
Features/[Name]/Shared — Локальный общий код между связанными срезами.-
Domain — Сущности, объекты-значения и доменные сервисы. Общая бизнес-логика живёт здесь.-
Infrastructure — Технические вопросы.-
Shared — Только общее поведение.Правила
1. Функции владеют своими моделями запросов/ответов. Исключений нет.
2. Перемещайте бизнес-логику в домен. Сущности и объекты-значения — лучшее место для совместного использования бизнес-правил.
3. Сохраняйте совместное использование семейств функций локальным. Если это нужно только срезам Order, храните его в Features/Orders/Shared (не стесняйтесь найти более подходящее название, чем Shared).
4. Инфраструктура по умолчанию является общей. Контексты БД, HTTP-клиенты, ведение журнала. Это технические вопросы.
5. Применяйте правило трёх. Не извлекайте данные, пока не найдёте 3 реальных использования идентичной, стабильной логики.
Источник: https://www.milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live