聚合为服务

Aggregate as a service

假设服务需要一些全局配置来处理一些请求的场景。

例如,当用户想要做某事时,它需要一些全局配置来检查用户是否被允许这样做。

我意识到在 axon 中我可以有命令处理程序来处理没有指定目标集合的命令,因此处理部分不是问题。

问题是我希望在尝试更改配置时在此之上有持久存储和一些不变量。配置的整个想法是它应该像轴突中的聚合一样一致。

ConfigService {
@Inject
configRepository;
@Inject
eventGateway;

@CommandHandler
handle(changeConfig){
let current = configRepository.loadCurrent;
//some checks

//persist here?
eventGateway.send(confgChanged)
}

@EventHandler
on(configChanged){
//or persist here?
configRepository.saveCurrent(configChanged.data)
}

}

如果我对命令处理程序进行持久化处理,我认为我不应该使用事件处理程序,因为它会保存两次。但是当我以某种方式丢失配置存储库数据时,我可以根据事件重建它。

我不确定我在理解 DDD 概念时缺少什么,简单地说,我想知道在哪里放置命令处理程序来处理既不是聚合也不是实体的东西。 也许我应该创建调用配置服务的命令处理程序,而不是让配置服务成为命令处理程序。

在我看来,您的 全局配置 要么是 specification or a set of rules like in a rules engine

GOF book 中描述的模式不同,在 DDD 中,一些建筑 blocks/patterns 更通用,可以应用于您拥有的不同类型的对象。

例如,实体是具有生命周期和身份的东西。生命周期中的阶段通常是:创建、持久化、从存储中重建、修改,然后它的生命周期以删除、归档、完成等结束。

A Value Object 是没有身份的东西,(大部分时间)是不可变的,两个实例可以是通过他们的属性的平等来比较。 Value Object 代表我们领域中的重要概念,例如:Money 在处理会计、银行等的系统中., Vector3Matrix3 在做数学运算的系统中计算和模拟,如建模系统(3dsMax、Maya)、视频游戏等。它们包含重要的行为。

因此,您需要跟踪并具有身份的所有内容都可以是 实体

你可以有一个规范,一个规则 是一个实体,一个 Event 也可以是一个实体,如果它有一个分配给它的唯一 ID。在这种情况下,您可以像对待任何其他实体一样对待它们。您可以形成聚合,拥有存储库和服务,并在必要时使用 EventSourcing

另一方面,规范规则事件命令也可以是 Value Objects.

规格规则也可以是域服务.

这里还有一个重要的事情是限界上下文。更新这些规则的系统可能与应用那里规则的系统处于不同的限界上下文。也有可能不是这样。

这是一个例子。

让我们有一个系统,顾客可以买东西。此系统还将在 订单 上提供 折扣具体规则.

假设我们有这样的规则:如果 客户 做出了 Order 超过 5 LineItems 他得到一个折扣。如果 Order 的总价为一定金额(例如 1000 美元),他将获得折扣。

销售团队可以更改折扣百分比。 销售系统OrderDicountPolicy聚合,它可以修改.另一方面,订购系统只读取OrderDicountPolicy汇总并且无法修改它们,因为这是 销售团队.

的职责

销售系统订购系统 可以是两个单独的 限界上下文 的一部分:Sales订单订单限界上下文取决于销售限界上下文

注意:我将跳过大部分实现细节,只添加相关内容以缩短和简化此示例。如果它的意图不清楚,我会编辑并添加更多细节。 UUID, DiscountPercentageMoney 是我将跳过的值对象。

public interface OrderDiscountPolicy {

    public UUID getID();

    public DiscountPercentage getDiscountPercentage();
    public void changeDiscountPercentage(DiscountPercentage percentage);

    public bool canApplyDiscount(Order order);
}

public class LineItemsCountOrderDiscountPolicy implements OrderDiscountPolicy {

    public int getLineItemsCount() { }

    public void changeLineItemsCount(int count) { }

    public bool canApplyDiscount(Order order) { 
        return order.getLineItemsCount() > this.getLineItemsCount();
    }

    // other stuff from interface implementation
}

public class PriceThresholdOrderDiscountPolicy implements OrderDiscountPolicy {

    public Money getPriceThreshold() { }

    public void changePriceThreshold(Money threshold) { }

    public bool canApplyDiscount(Order order) { 
        return order.getTotalPriceWithoutDiscount() > this.getPriceThreshold();
    }

    // other stuff from interface implementation
}

public class LineItem {

    public UUID getOrderID() { }
    public UUID getProductID() { }
    public Quantity getQuantity { }
    public Money getProductPrice() { } 

    public Money getTotalPrice() {
        return getProductPrice().multiply(getQuantity());
    }
}

public enum OrderStatus { Pending, Placed, Approced, Rejected, Shipped, Finalized }

public class Order {

    private UUID mID;
    private OrderStatus mStatus;
    private List<LineItem> mLineItems;
    private DscountPercentage mDiscountPercentage;

    public UUID getID() { }
    public OrderStatus getStatus() { }
    public DscountPercentage getDiscountPercentage() { };

    public Money getTotalPriceWithoutDiscount() { 
        // return sum of all line items
    }

    public Money getTotalPrice() { 
        // return sum of all line items + discount percentage
    }

    public void changeStatus(OrderStatus newStatus) { }

    public List<LineItem> getLineItems() {
        return Collections.unmodifiableList(mLineItems);
    }

    public LineItem addLineItem(UUID productID, Quantity quantity, Money price) {
        LineItem item = new LineItem(this.getID(), productID, quantity, price);
        mLineItems.add(item);
        return item;
    }

    public void applyDiscount(DiscountPercentage discountPercentage) {
        mDiscountPercentage = discountPercentage;
    }
}

public class PlaceOrderCommandHandler {

    public void handle(PlaceOrderCommand cmd) {

        Order order = mOrderRepository.getByID(cmd.getOrderID());

        List<OrderDiscountPolicy> discountPolicies =
             mOrderDiscountPolicyRepository.getAll();

        for (OrderDiscountPolicy policy : discountPolicies) { 

            if (policy.canApplyDiscount(order)) {
                order.applyDiscount(policy.getDiscountPercentage());
            }
        }

        order.changeStatus(OrderStatus.Placed);

        mOrderRepository.save(order);
    }
}

public class ChangeOrderDiscountPolicyPercentageHandler {

    public void handle(ChangeOrderDiscountPolicyPercentage cmd) {

        OrderDiscountPolicy policy = 
            mOrderDiscountRepository.getByID(cmd.getPolicyID());

        policy.changePercentage(cmd.getDiscountPercentage());

        mOrderDiscountRepository.save(policy);
    }
}

如果您认为适合某些聚合,可以使用 EventSourcingDDD book 有一章介绍 全局规则 规范

让我们看看在分布式应用程序(例如使用微服务)的情况下我们应该做什么。

假设我们有 2 个服务:OrdersServiceOrdersDiscountService .

有几种方法可以实现这个操作。我们可以使用:

  • 事件编排
  • 编排与显式 SagaProcess Manager

如果我们将 Choreography 与 Events 结合使用,那么我们可以这样做。

  1. CreateOrderCommand -> OrdersService -> OrderCreatedEvent

  2. OrderCreatedEvent -> OrdersDiscountService -> OrderDiscountAvailableEventOrderDiscountNotAvailableEvent

  3. OrderDiscountAvailableEventOrderDiscountNotAvailableEvent -> OrdersService -> OrderPlacedEvent

在此示例中下订单 OrdersService 将等待 OrderDiscountNotAvailableEventOrderDiscountNotAvailableEvent 因此它可以在将订单状态更改为 [= 之前​​应用折扣97=]已下订单.

我们还可以使用显式的Saga在服务之间进行Orchestration

此 Saga 将包含流程的步骤序列,以便它可以执行。

  1. PlaceOrderCommand -> Saga
  2. Saga 询问 OrdersDiscountService 查看如果该订单.
  3. 有折扣
  4. 如果有折扣,Saga调用OrdersService 申请折扣
  5. Saga调用OrdersService进行设置Order 的状态变为 OrderPlaced

注意:步骤 3 和 4 可以合并

这提出了问题:*"How OrdersDiscountService get's all the necessary information for the Order to calculate discounts?"*

这可以通过在该服务将收到的 Event 中添加订单的所有信息或通过让OrdersDiscountService 调用 OrdersService 获取信息。

这里有一篇关于事件驱动架构的文章 Great video from Martin Folwer,其中讨论了这些方法。

Orchestration with a Saga 的优点是明确定义了确切的流程在 Saga 中可以找到、理解和调试。

Choreography with Events 这样的隐式流程可能更难理解、调试和维护。

Sagas 的缺点是我们确实定义了更多的东西。

就个人而言,我倾向于使用显式的 Saga,尤其是对于复杂的流程,但我工作和看到的大多数系统都使用两种方法。

这里有一些额外的资源:

https://blog.couchbase.com/saga-pattern-implement-business-transactions-using-microservices-part/

https://blog.couchbase.com/saga-pattern-implement-business-transactions-using-microservices-part-2/

https://microservices.io/patterns/data/saga.html

The LMAX Architecture 读起来很有趣。它不是分布式系统,而是事件驱动的并记录传入 events/commands 和传出事件。这是一种捕获系统或服务中发生的一切的有趣方式。

您在这里使用的是没有事件源的 Axon 吗?

在 Axon 框架中,仅使用事件更改聚合状态通常是一种很好的做法。如果您要混合从存储库加载的状态或配置与事件存储的状态,您将如何保证当您重播相同的事件时,结果状态将是相同的?下次加载聚合时,您的 configRepository 中可能存在不同的状态,从而导致聚合的不同状态和不同行为。

为什么这么糟糕?好吧,那些相同的事件可能已经由事件处理器处理过,它们可能已经填充了查询表,它们可能已经将消息发送到其他系统或根据系统当时的状态完成了其他工作。您的查询数据库和聚合之间会有分歧。

一个具体的例子:假设您的聚合处理了一个打开电子邮件服务的命令。聚合通过应用 EmailServiceEnabledEvent 并将其自己的状态更改为 'boolean emailEnabled = true' 来完成此操作。一段时间后,聚合从内存中卸载。现在您更改该 configurationRepository 以禁用打开电子邮件服务。再次加载聚合时,会应用事件存储中的事件,但这次它会从您的存储库中加载配置,该配置表明它不应打开电子邮件服务。 'boolean emailEnabled' 状态为假。您向聚合发送禁用电子邮件服务命令,但聚合中的命令处理程序认为电子邮件已被禁用,并且不应用 EmailServiceDisabledEvent。电子邮件服务保持打开状态。

简而言之:我建议使用命令来更改聚合的配置。