All streams
Search
Write a publication
Pull to refresh

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 это?..

Подожди, какая нафиг инверсия зависимостей? это между классами. а между доменами что?

Связь через события, DTO

А домен это по вашему не классы? Как у вас вообще формируется домен?)

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

Все учебники лишь показывают, как можно делать, но конечное решение всегда будет состоять из какой-либо смеси практик, принципов и паттернов, а качество и наполнение смеси зависит от вашего опыта. 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);
Sign up to leave a comment.

Articles