Created
January 11, 2013 23:14
-
-
Save nicholasjhenry/4514772 to your computer and use it in GitHub Desktop.
DCI example from Lean Architecture
This file contains hidden or 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
| #!/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