Created
December 9, 2011 21:25
-
-
Save erikrozendaal/1453364 to your computer and use it in GitHub Desktop.
Simple Ruby DSL for CQRS+ES aggregates
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# CQRS+ES Domain DSL | |
class Symbol | |
def snake_case | |
to_s.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2'). | |
gsub(/([a-z\d])([A-Z])/, '\1_\2'). | |
tr("-", "_"). | |
downcase.to_sym | |
end | |
def camel_case | |
to_s.split('_').map(&:capitalize).join.to_sym | |
end | |
end | |
module DomainSupport | |
module Events | |
class Event | |
attr_reader :aggregate_id | |
def self.simple_name | |
name.split("::").last.to_sym | |
end | |
def initialize(aggregate_id, params={}) | |
@aggregate_id = aggregate_id | |
params.each do |name, value| | |
instance_variable_set("@#{name}", value) | |
end | |
end | |
# Add ==, hash, type, payload, etc. | |
end | |
def self.included(target) | |
raise "#{self} can only be included into a module" unless target.is_a?(Module) && !target.is_a?(Class) | |
target.instance_eval do | |
extend ModuleMethods | |
@events = [] | |
def self.included(aggregate) | |
raise "#{self} can only be included into an AggregateRoot" unless aggregate.ancestors.include?(AggregateRoot) | |
end | |
end | |
end | |
module ModuleMethods | |
attr_reader :events | |
def define_event(name, *fields) | |
result = Class.new(Event) do | |
fields.each do |field| | |
attr_reader field | |
end | |
end | |
const_set(name, result) | |
@events << result | |
define_method(name.snake_case) do |*args| | |
puts "Generating #{result} with #{args.inspect}" | |
record result.new(@id, *args) | |
end | |
private(name.snake_case) | |
yield result if block_given? | |
result | |
end | |
end | |
end | |
class AggregateRoot | |
attr_reader :id, :uncommitted_events | |
def initialize(id) | |
@id = id | |
@uncommitted_events = [] | |
end | |
def self.load_from_history(history) | |
raise "empty history" if history.empty? | |
result = allocate() | |
result.instance_eval do | |
@id = history.first.aggregate_id | |
@uncommitted_events = [] | |
history.each do |event| | |
apply(event) | |
end | |
end | |
result | |
end | |
def clear_uncommitted_events | |
@uncommitted_events = [] | |
nil | |
end | |
private | |
def record(event) | |
apply(event) | |
@uncommitted_events << event | |
nil | |
end | |
def apply(event) | |
puts "Applying #{event.inspect} to #{inspect}" | |
simple_name = event.class.simple_name.snake_case | |
send("on_#{simple_name}", event) | |
nil | |
end | |
end | |
end | |
module OrganizationEvents | |
include DomainSupport::Events | |
define_event :Created, :name | |
end | |
module InvoiceEvents | |
include DomainSupport::Events | |
define_event :Created | |
define_event :LineItemAdded, :line_item_id, :description, :amount | |
define_event :Sent, :sent_date, :due_date do |event| | |
puts "#{event} defined!" | |
end | |
end | |
class Invoice < DomainSupport::AggregateRoot | |
include InvoiceEvents | |
def initialize(id) | |
super(id) | |
created | |
end | |
def add_line_item(description, amount) | |
raise "Cannot add items to sent invoices" if @sent | |
line_item_added line_item_id: @next_line_item_id, description: description, amount: amount | |
end | |
def do_send | |
sent sent_date: "now" | |
end | |
private | |
def on_created(event) | |
@next_line_item_id = 0 | |
@sent = false | |
end | |
def on_line_item_added(event) | |
@next_line_item_id = event.line_item_id + 1 | |
end | |
def on_sent(event) | |
@sent = true | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment