展示一段示例代码 - Order 类。此类在我们的示例 DDD/CQRS/ES 应用程序中使用。我们正在改进此应用程序,因此这是记录某些意见和更改的好机会。
module Ordering class Order include AggregateRoot
AlreadySubmitted = Class.new(StandardError) AlreadyPaid = Class.new(StandardError) NotSubmitted = Class.new(StandardError) OrderHasExpired = Class.new(StandardError) MissingCustomer = Class.new(StandardError)
def initialize(id) @id = id @state = :draft end
def submit(order_number, customer_id) raise AlreadySubmitted if @state.equal?(:submitted) raise OrderHasExpired if @state.equal?(:expired) raise MissingCustomer unless customer_id apply OrderSubmitted.new(data: {order_id: @id, order_number: order_number, customer_id: customer_id}) end
def confirm(transaction_id) raise OrderHasExpired if @state.equal?(:expired) raise NotSubmitted unless @state.equal?(:submitted) apply OrderPaid.new(data: {order_id: @id, transaction_id: transaction_id}) end
def expire raise AlreadyPaid if @state.equal?(:paid) apply OrderExpired.new(data: {order_id: @id}) end
def add_item(product_id) raise AlreadySubmitted unless @state.equal?(:draft) apply ItemAddedToBasket.new(data: {order_id: @id, product_id: product_id}) end
def remove_item(product_id) raise AlreadySubmitted unless @state.equal?(:draft) apply ItemRemovedFromBasket.new(data: {order_id: @id, product_id: product_id}) end
def cancel raise OrderHasExpired if @state.equal?(:expired) raise NotSubmitted unless @state.equal?(:submitted) apply OrderCancelled.new(data: {order_id: @id}) end
on OrderSubmitted do |event| @customer_id = event.data[:customer_id] @number = event.data[:order_number] @state = :submitted end
on OrderPaid do |event| @state = :paid end
on OrderExpired do |event| @state = :expired end
on OrderCancelled do |event| @state = :cancelled end
on ItemAddedToBasket do |event| end
on ItemRemovedFromBasket do |event| end end end
|
一如既往,这个类一开始很好很简单,但随着时间的推移,它变得越来越不可读。
这个类现在有几个职责。其中一些我将留待另一次讨论(例如将域代码与事件代码耦合)。今天我想重点讨论状态机的概念。
此 Order 对象现在有 5 种可能的状态。怎么长到这么大了?
如果我们进一步扩展增长状态会发生什么?
状态机:
- draft
- submitted
- paid
- expired
- cancelled
draft是初始状态。然后幸福路径切换到submitted,然后切换到paid
不太愉快的路径包括expired和cancelled,两者都是叶状态。问题:
状态机的挑战在于,以可读的方式在代码中表示它们并不容易。每当状态和转换的数量增加时,阅读此类代码就会变得更加困难。
状态机由状态和转换组成。我们需要以某种方式在代码中表示它们。在这个实现中,我们把过渡作为主要的“维度”。方法名称显示了可能的转换。然而,它们显示了所有状态的可能转变。这导致了一个问题,在每种方法中,我们现在都需要“禁用”不可能的转换。在这种情况下,我们可以通过提前返回而不使用异常来做到这一点。这段代码的问题在于,很难轻易地说出这个状态机中可能的流程是什么。该代码受到其他职责的影响,使其可读性降低。
为什么我们在这里使用异常?
因为该对象的职责之一是传达“为什么”某种更改是不可能的。提前返回仅传达布尔信息 - 可能或不可能。自定义异常会带来更多上下文。
这里有哪些可能的改进方向?
- 减小该状态机的大小
- 将对象解耦以解释为什么无法从代码中进行更改,而代码只是说不可能
- 根据可能的状态提取一个新对象
- 从此类中提取事件逻辑