Skip to content

Instantly share code, notes, and snippets.

@nicholasjhenry
Created January 11, 2013 23:14
Show Gist options
  • Select an option

  • Save nicholasjhenry/4514772 to your computer and use it in GitHub Desktop.

Select an option

Save nicholasjhenry/4514772 to your computer and use it in GitHub Desktop.
DCI example from Lean Architecture
#!/usr/bin/env ruby
# Lean Architecture example in Ruby -
# with ContextAccessor
# Module that can be mixed in to any class
# that needs access to the current context. It is
# implemented as a thread-local variable.
#
module ContextAccessor
def context
Thread.current[:context]
end
def context=(ctx)
Thread.current[:context] = ctx
end
def execute_in_context
old_context = self.context
self.context = self
yield
self.context = old_context
end
end
# This is the base class (common code) for all
# Account domain classes.
#
class Account
attr_reader :account_id, :balance
def initialize(account_id)
@account_id = account_id
@balance = 0
end
def decrease_balance(amount)
raise "Bad argument to withdraw" if amount < 0
raise "Insufficient funds" if amount > balance
@balance -= amount
end
def increase_balance(amount)
@balance += amount
end
def update_log(msg, date, amount)
puts "Account: #{inspect}, #{msg}, #{date.to_s}, #{amount}"
end
def self.find(account_id)
@@store ||= Hash.new
return @@store[account_id] if @@store.has_key?(account_id)
if :savings == account_id
account = SavingsAccount.new(account_id)
account.increase_balance(100000)
elsif :checking == account_id
account = CheckingAccount.new(account_id)
else
account = Account.new(account_id)
end
@@store[account_id] = account
account
end
end
# This module is the methodless role type. Since
# we don’t really use types to declare identifiers,
# it’s kind of a hobby horse. We preserve those APIs
# for consistency with the other languages. This also
# provides a single common place to create aliases
# for the role bindings
#
module MethodlessMoneySource # the API only
def transfer_out; end
def pay_bills; end
# Role aliases for use by the methodful role
#
def destination_account; context.destination_account end
def creditors; context.creditors end
def amount; context.amount end
end
module MethodlessMoneySink # the API only
def transfer_in; end
# Role aliases for use by the methodful role
#
def amount; context.amount end
end
# Here are the real methodful roles
#
module MoneySink
include MethodlessMoneySink, ContextAccessor
def transfer_in
self.increase_balance(amount)
self.update_log "Transfer In", Time.now, amount
end
end
module MoneySource
include MethodlessMoneySource, ContextAccessor
def transfer_out
raise "Insufficient funds" if balance < amount
self.decrease_balance(amount)
destination_account.transfer_in
self.update_log "Transfer Out", Time.now, amount
end
def pay_bills
creditors = context.creditors.dup
creditors.each do |creditor|
TransferMoneyContext.execute(
creditor.amount_owed,
account_id,
creditor.account.account_id)
end
end
end
# Creditor is an actor in the use case, and is
# represented by an object of this class
#
class Creditor
attr_accessor :amount_owed, :account
# The "find" method is set up just for demonstration
# purposes. A real one would search a database for a
# particular creditor, based on more meaningful
# search criteria
#
def self.find(name)
@@store ||= Hash.new
return @@store[name] if @@store.has_key?(name)
if :baker == name
creditor = Creditor.new
creditor.amount_owed = 50
creditor.account = Account.find(:baker_account)
elsif :butcher == name
creditor = Creditor.new
creditor.amount_owed = 90
creditor.account = Account.find(:butcher_account)
end
creditor
end
end
# Implementation of Transfer Money use case
#
class TransferMoneyContext
attr_reader :source_account, :destination_account, :amount
include ContextAccessor
def self.execute(amt, source_account_id, destination_account_id)
TransferMoneyContext.new(amt,
source_account_id,
destination_account_id).execute
end
def initialize(amt, source_account_id, destination_account_id)
@source_account = Account.find(source_account_id)
@source_account.extend MoneySource
@destination_account = Account.find(destination_account_id)
@destination_account.extend MoneySink
@amount = amt
end
def execute
execute_in_context do
source_account.transfer_out
end
end
end
# This is the Context for the PayBills use case
#
class PayBillsContext
attr_reader :source_account, :creditors
include ContextAccessor
# This is the class method which sets up to
# execute the instance method. For more details,
# see the text of CHAPTER 9 (page 342)
def self.execute(source_account_id,creditor_names)
PayBillsContext.new(source_account_id,creditor_names).execute
end
def initialize(source_account_id, creditor_names)
@source_account = Account.find(source_account_id)
@creditors = creditor_names.map do |name|
Creditor.find(name)
end
end
def execute
execute_in_context do
source_account.pay_bills
end
end
end
# The accounts are pretty stupid, with most of
# the logic in the base class
#
class SavingsAccount < Account
include MoneySink
end
class CheckingAccount < Account
include MoneySink
end
# Test drivers. First, transfer some money
#
TransferMoneyContext.execute(300, :savings, :checking)
TransferMoneyContext.execute(100, :checking, :savings)
puts "Savings: #{Account.find(:savings).balance}, " \
"Checking: #{Account.find(:checking).balance}"
# Now pay some bills
#
PayBillsContext.execute(:checking, [:baker, :butcher])
puts "After paying bills, checking has: " \
"#{Account.find(:checking).balance}"
puts "Baker and butcher have " \
"#{Account.find(:baker_account).balance}, " \
"#{Account.find(:butcher_account).balance}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment