Sử dụng state_machine và các event.
Thông thường thì các đoạn code chúng ta viết ra để thưc hiện giải quyết vấn đề nào đó chẳng hạn như Raise lên một Exception thì không tránh khỏi việc phải sử dụng đến những câu điều kiện và làm cho đoạn code của chúng ta trở nên khá rối và khó đọc. Tôi tự hỏi có cách nào để loại bỏ những điều kiện ...
Thông thường thì các đoạn code chúng ta viết ra để thưc hiện giải quyết vấn đề nào đó chẳng hạn như Raise lên một Exception thì không tránh khỏi việc phải sử dụng đến những câu điều kiện và làm cho đoạn code của chúng ta trở nên khá rối và khó đọc. Tôi tự hỏi có cách nào để loại bỏ những điều kiện đó không, hay nói cách khác là nó sẽ thực hiện điều gì đó khi có sự thay đổi của 1 cái gì đó của 1 Object chẳng hạn. Điều đó là cho chúng ta có thế hình dung mã code của chúng ta rõ ràng hơn. Và state_machine đã cho tôi là được điều đó.
Để giải thích rõ hơn về nó chúng ta hãy băt đầu với đoạn code sau( tất nhiên là không phải tôi tạo ra nó, mà chỉ là lượm nhặt trên NET thôi. =)) ) :
class Order include AggregateRoot NotAllowed = Class.new(StandardError) Invalid = Class.new(StandardError) def initialize(number:) @number = number @state = :draft @items = [] end def add_item(sku:, quantity:, net_price:, vat_rate:) raise NotAllowed unless state == :draft raise ArgumentError unless sku.to_s.present? raise ArgumentError unless quantity > 0 raise ArgumentError unless net_price > 0 raise ArgumentError if vat_rate < 0 || vat_rate >= 100 # make changes and apply new state end def submit(customer_id:) raise NotAllowed unless state == :draft raise Invalid if items.empty? # make changes and apply new state end def cancel raise NotAllowed unless [:draft, :submitted].include?(state) apply(OrderCancelled.strict(data: { order_number: number})) end def expire return if [:expired, :shipped].include?(state) apply(OrderExpired.strict(data: { order_number: number})) end def ship raise NotAllowed unless state == :submitted apply(OrderShipped.strict(data: { order_number: number, customer_id: customer_id, })) end private attr_reader :number, :state, :items, :fee_calculator, :customer_id def apply_strategy ->(_me, event) { { Orders::OrderItemAdded => method(:apply_item_added), Orders::OrderSubmitted => method(:apply_submitted), Orders::OrderCancelled => method(:apply_cancelled), Orders::OrderExpired => method(:apply_expired), Orders::OrderShipped => method(:apply_shipped), }.fetch(event.class).call(event) } end def apply_item_added(ev) # ... end def apply_submitted(ev) @state = :submitted @customer_id = ev.data[:customer_id] end def apply_cancelled(ev) @state = :cancelled end def apply_expired(ev) @state = :expired end def apply_shipped(ev) @state = :shipped end end
Như các bạn có thế thấy các method thưởng bắt đầu bằng việc kiểm tra state. Kiểm tra 1 state thì như thế này:
def ship raise NotAllowed unless state == :submitted # ... end
Kiểm tra với 2 state thì như thế này:
def cancel raise NotAllowed unless [:draft, :submitted].include?(state) # ... end
Thỉnh thoảng thì chúng ta lại muốn xử lý hay không làm gì thay vì phải raise error như thế này:
def expire return if [:expired, :shipped].include?(state) # ... end
Vì thế khi tôi sử dụng state_machine thì tôi ko cần phải tạo ra nhiều rules giống nhau. mà chỉ quan tâm nó như thế nào mà thôi. Điều đó làm cho mã code rõ ràng và tường minh hơn.
Khi tôi apply state_machine vào thì nó sẽ như thế này:
class Order include AggregateRoot NotAllowed = Class.new(StandardError) Invalid = Class.new(StandardError) def initialize(number:) @number = number @state = 'draft' @items = [] end state_machine :state do state 'draft' do def add_item(sku:, quantity:, net_price:, vat_rate:) raise ArgumentError unless sku.to_s.present? raise ArgumentError unless quantity > 0 raise ArgumentError unless net_price > 0 raise ArgumentError if vat_rate < 0 || vat_rate >= 100 # ... end def submit(customer_id:) raise Invalid if items.empty? # ... end end state 'submitted' do def ship apply(OrderShipped.strict(data: { order_number: number, customer_id: customer_id, })) end end state 'expired' do def expire; end end state 'cancelled' do def cancel; end end state all - %w(expired shipped) do def expire apply(OrderExpired.strict(data: { order_number: number })) end end state *%w(draft submitted) do def cancel apply(OrderCancelled.strict(data: { order_number: number })) end end end
Trông khá tuyệt đúng ko. clean hơn hẵn. Nếu một method có thể chỉ được gọi trong một state duy nhất thì bạn có thể định nghĩa như sau:
state 'submitted' do def ship # ... end end
Nếu bạn thử gọi method đó ở 1 state khác thì nó sẽ get một NoMethodError. Nó còn có thể định nghĩa một method được gọi trong chỉ 2 state:
state *%w(draft submitted) do def cancel # ... end end
với đoạn mà này thì method cancel sẽ thực hiện khi state mà 1 trong 2 state draft và submitted
Hơn thế nữa chúng ta còn có thể thực hiện một method với các states khác trừ một số states chúng ta chỉ định:
state all - %w(expired shipped) do def expire # ... end end
Với đoạn mã trên nó method expire sẽ được gọi khi state được thay đổi và không phải là 2 states là expired và shipped
Tôi đã tìm thấy ở đây event transition và transition callback. Nó có thể giúp tôi chuyển đổi từ state này sáng state khác khi thực hiện 1 event nào đó do tôi định nghĩa.
event :expire do transition all - %w(expired shipped) => :expired end
với đoạn mã trên tôi sẽ chuyển tất cả các state trừ expired và shipped thành state expired
Ngoài ra chúng ta còn có thể sử dụng các callback của nó như là before_transition và after_transition
Chúng ta có thể sử dụng state_machine cho việc xử lý các state và sự thay đổi của chúng để thực hiện những việc chúng ta muốn thay vì cứ phải định nghĩa các rules để xử lý. Điều đó làm cho code trở nên rõ ràng và tưởng minh hơn.