Comments 16
никто не говорит, что делать
Врать не буду: я тоже не скажу, что делать. Самому приходится сталкиваться с похожими проблемами, но внятное решение пока не осознал. Напишу сумбурно, как проблема в целом видится мне.
Продолжительное время намеренно не читал книги, и до них мыслить о работе явно было проще. Полагаю, что это произрастает из привычки мыслить понятием "учебника", в котором зачастую было написано: "вот так делай, а вот так не делай, и вот тебе ещё полный набор исключений вдогонку". И до знакомства с новыми правилами старые покрывали абсолютное большинство встречаемых задач. В технической же литературе обычно даются не правила, а принципы, под которыми подразумеваются своего рода оси и вдоль которых предлагается двигаться, чтобы избежать тех или иных проблем, которые конкретный автор мог счесть наиболее частыми или критичными.
Парадигмы, методологии, практики - все они по своей сути отражают ограниченные наборы таких проблем и опыт конкретных компаний/команд/людей по их решению. Применимое для одних не обязательно будет одинаково понятным и полезным для всех остальных - универсальных решений, как известно, не бывает. Если в рамках конкретной парадигмы или даже конкретной книге, описывающей её принципы, описан набор понятий и способов их применения, думаю, не стоит их воспринимать как т.н. "истину в последней инстанции". Могут существовать дополнительные книги, продолжающие и дополняющие идею предыдущих; можно до этих самых дополнений попытаться добраться и самостоятельно, поскольку они зачастую вытекают из уже ранее предоставленной основы; какие-то нюансы применения, вероятно, можно будет понять, только столкнувшись с ними в схожей ситуации (как сейчас). Для более полноценного понимания нужны не только сами знания, но и практика их применения (в том числе безуспешная).
Единственное решение проблемы в целом, которое вижу для себя: находить время, чтобы вдоволь напробоваться намеренно отходить от тех или иных принципов в рамках одного и того же типового проекта, реализованного несколько раз. Не вижу лучшего способа понять, когда кода будет меньше, в каком случае последующие изменения вносить сложнее, чья парадигма круче.
Да, я с удивлением понимаю, что в приложении можно быть множество архитектурных паттернов, которые на курсах предлагают "выбрать". А статьи, скажем, про микросервисы в 90% случаев говорят как делать, но не говорят зачем.
Притом, основная ценность архитектора не сказать как, а сказать зачем, выбрать и обосновать.
Мы привыкли видеть домены как главную сущность, а взаимодействие между ними как некое дополнение. На самом же деле именно взаимодействие является основной сущностью, а домены это сгустки, фрагменты, стабильные участки такого взаимодействия.
Между доменами только события или вызовы API c JSON (т.е. типы на границе теряются, значит бизнес правила границу не переходят)
Подскажите, а это где такое написано?
Логика вынесена на уровень компиляции
Где код? Где проверки? Где валидация? Где обработка ошибок?
Вам можно создавать объект, валидировать входные данные в конструкторе и у вас всегда будет валидный объект либо ошибка...
Вам можно создавать объект, валидировать входные данные в конструкторе и у вас всегда будет валидный объект либо ошибка
Не ошибка, а исключение
В C#: `new Money(-10)` - `throw`- может упасть в любом слое
Но можно сделать пустой конструктор и функцию TryCreate или TryFrom, которая вернёт Result<T,Err>
В Rust: `Money::try_from(-10.0)` -`Result<Money, Error>` - компилятор заставляет обработать
Подскажите, а это где такое написано?
Между контекстами в DDD нет общей модели, чтобы не создавать зависимостей
Между контекстами в DDD нет общей модели, чтобы не создавать зависимостей
А инверсия зависимостей которую описывает DDD это?..
Вы буквально описали создание обычного сценария, который призван оркестрировать вызовы разных доменных сервисов, обеспечивать общую транзакционность, вызывать сервисы из разных доменов и так далее.
Все учебники лишь показывают, как можно делать, но конечное решение всегда будет состоять из какой-либо смеси практик, принципов и паттернов, а качество и наполнение смеси зависит от вашего опыта. DDD это в первую очередь не про написание кода, а про способ мышления при проектировании системы (под системой я понимаю не только код, но и все процессы в компании), способ общения с бизнесом.
Это как регламент взаимодействия подразделений компании, который не принадлежит нет одному отделу.
Мне кажется наиболее логичным вложить функционал, реализованный в CrossDomainCoordinator, в юз кейс "Отменить заказ" в виде метода или вложенного объекта.
юз кейс принадлежит к домену, а домену это знать / зависеть плохо
Но можно, часто так делают
Юз кейс принадлежит слою application logic, который находится выше слоя доменной логики. Домен ничего не знает про юз кейс, который его использует.
Короче, я хочу вынести из usecase, абстрагировать и переиспользовать следующее:
Кросс-доменных инвариантов и правил
Конечных автоматов процессов (state machines)
Механизмов retry, timeout, compensation (откатов), хотя тут лучше использовать готовые библиотеки
Абстракцию логики оркестрации, которую можно заменить на специализированные движки (Temporal, Camunda и т.п.)
То есть то что уже используется в микросервисах, но без собственно микросервисов
// ===== 10 строк: всё понятно =====
if order.status == "pending" {
order.status = "cancelled";
db.save(order);
}
// ===== 100 строк: добавляем structure =====
class OrderService {
fun cancel(order: Order) {
if (order.canCancel()) {
order.cancel();
repository.save(order);
eventBus.publish(OrderCancelled(order.id));
}
}
}
// ===== 1000 строк: появляются Policy, Strategy =====
interface CancellationPolicy {
fun evaluate(order: Order, context: Context): Decision
}
class OrderWorkflow {
policies: List<CancellationPolicy>
saga: SagaOrchestrator
fun cancel(orderId: OrderId) {
// Уже не просто if, а цепочка абстракций
}
}
// ===== 10000 строк: метапрограммирование =====
@ProcessDefinition("order-cancellation-v3")
@CompensationStrategy(TwoPhaseCommit)
class OrderCancellationProcess {
@Step(retry = @Retry(3, backoff = EXPONENTIAL))
@Timeout(5, SECONDS)
fun validateCancellation(
@Inject policies: PolicyEngine,
@EventSourcing order: Order
): ValidationResult
@CompensationFor("validateCancellation")
fun rollbackValidation() { }
}
// ===== 100000 строк: DSL и кодогенерация =====
cancellation_rules.yaml:
vip_customer:
when: customer.tier == VIP && order.age < 7d
allow: true
compensation: RETURN_SHIPPING
standard:
when: order.status in [PENDING, PAID]
allow: true
compensation: FULL_REFUND
// Генерируется Rust код с типами
Примерно так раскидал ответственность по частям
Приходится вводить абстракции (Policy, Saga, DSL)
// ===== DOMAIN (Orders) - только свой домен =====
mod orders_domain {
// Состояния заказа - чисто доменные
pub struct Pending;
pub struct Paid { payment_id: PaymentId } // ID, не детали!
pub struct Shipped { tracking: TrackingNumber }
// Переходы внутри домена
pub struct Order<S> {
id: OrderId,
items: Vec<OrderItem>,
state: PhantomData<S>,
}
impl Order<Pending> {
// Только переходы своего домена
pub fn mark_paid(self, payment_id: PaymentId) -> Order<Paid> {
Order {
id: self.id,
items: self.items,
state: PhantomData::<Paid>
}
}
}
impl Order<Paid> {
pub fn can_cancel(&self) -> bool {
true // доменное правило
}
}
impl Order<Shipped> {
pub fn can_cancel(&self) -> bool {
false // доменное правило
}
}
}
// ===== DOMAIN (Payments) - только свой домен =====
mod payments_domain {
pub trait PaymentMethod {
fn supports_refund(&self) -> bool;
fn refund_deadline(&self) -> Option<Duration>;
}
pub struct CardPayment;
impl PaymentMethod for CardPayment {
fn supports_refund(&self) -> bool { true }
fn refund_deadline(&self) -> Option<Duration> { None }
}
pub struct CryptoPayment;
impl PaymentMethod for CryptoPayment {
fn supports_refund(&self) -> bool { true }
fn refund_deadline(&self) -> Option<Duration> {
Some(Duration::hours(1)) // доменное правило крипты
}
}
}
// ===== CROSS-DOMAIN - связывает домены =====
mod cancellation_policy {
use orders_domain::{Order, Paid, Shipped};
use payments_domain::PaymentMethod;
// Кросс-доменное правило: какой заказ КАК отменять
pub trait CancellationPolicy {
type RequiredSteps: SagaSteps;
}
// Paid заказ требует возврата денег
impl<P: PaymentMethod> CancellationPolicy for Order<Paid>
where
P: PaymentMethod
{
type RequiredSteps = If<
P::supports_refund(),
Chain<RefundStep, InventoryReturnStep>,
Just<InventoryReturnStep>
>;
}
// Shipped - запрещено (нет impl)
// impl CancellationPolicy for Order<Shipped> - НЕТ!
// Сага знает про несколько доменов
pub struct RefundStep {
payment_id: PaymentId, // из payments
order_id: OrderId, // из orders
}
pub struct InventoryReturnStep {
items: Vec<Sku>, // из inventory
order_id: OrderId, // из orders
}
}
// ===== USECASE - тонкий, запускает процесс =====
mod application {
use cancellation_policy::CancellationPolicy;
pub struct CancelOrderUseCase {
orders: Box<dyn OrderRepository>,
orchestrator: Box<dyn ProcessOrchestrator>,
}
impl CancelOrderUseCase {
pub fn execute<S>(&self, id: OrderId) -> Result<()>
where
Order<S>: CancellationPolicy
{
let order = self.orders.get::<S>(id)?;
// UseCase не знает КАКИЕ шаги, просто запускает
let steps = <Order<S> as CancellationPolicy>::RequiredSteps::new(&order);
self.orchestrator.start_saga(steps);
Ok(())
}
}
}
И композиция политик понятная
let decision = PolicyChain::new()
.add(StandardCancellationPolicy)
.add(VipShippedCancellationPolicy) // явно видно что применяется
.add(BlackFridaySpecialPolicy)
.evaluate(context);
Междоменные (процессные) инварианты