Sử dụng state_machine và các event.
Bài đăng này đã không được cập nhật trong 7 năm
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 đó.
Mở đầ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
Tóm lại:
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.
All rights reserved