-
-
Save rbarraud/069ae2d03057bf961dbf to your computer and use it in GitHub Desktop.
Revisions
-
James Harton revised this gist
Jul 23, 2014 . 8 changed files with 27 additions and 21 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,7 +1,6 @@ # Our simple example of a Turnstile state machine. class Turnstile def initialize @state = "Locked" end 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 charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,5 @@ # This is the final version of our ShoppingCartState code as presented. module ShoppingCartState StateError = Class.new(RuntimeError) 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 charactersOriginal file line number Diff line number Diff line change @@ -1,21 +1,5 @@ require './03_shopping_cart_state' require './09_mock_update_attributes' describe ShoppingCartState do include MockUpdateAttributes 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 charactersOriginal file line number Diff line number Diff line change @@ -1,8 +1,8 @@ require './03_shopping_cart_state' require './99_ar' # The final version of our ShoppingCart class, as described in the talk. class ShoppingCart < AR::Base has_many :line_items 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 charactersOriginal file line number Diff line number Diff line change @@ -3,6 +3,8 @@ require './99_ar' # This is the ShoppingCart using the StateDelegator mixin. # As mentioned in the improvements section of the talk. class ShoppingCart < AR::Base include StateDelegator 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 charactersOriginal file line number Diff line number Diff line change @@ -1,5 +1,8 @@ require './09_shopping_cart.rb' # Exactly the same specs as in `06_shopping_cart_spec.rb` except we're testing # the StateDelegator version of the ShoppingCart. describe ShoppingCart do %w| line_items add_item! cancel! pay! ship! fulfil! state state= |.each do |method| it { should respond_to method } 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,16 @@ # This is code I pulled in from another project to mock the # `update_attributes!` method. Sorry about the noise. module MockUpdateAttributes def mock_update_attributes_on(model) update_proc = proc do |attrs| attrs.each do |attr,value| setter = "#{attr}=".to_sym expect(model).to respond_to attr expect(model).to respond_to setter model.public_send setter, value end end allow(model).to receive(:update_attributes!, &update_proc) model end end 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 charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,4 @@ # This is a fake AR base class sufficient for our purposes. module AR class Base -
James Harton revised this gist
Jul 23, 2014 . 6 changed files with 166 additions and 22 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,25 +1,7 @@ # This is a fake AR base class. require './03_shopping_cart_state' require './99_ar' class ShoppingCart < AR::Base @@ -52,7 +34,4 @@ def send_successful_shipping_email def current_state ShoppingCartState.const_get(state).new(self) end end 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,31 @@ # Mix this module into your model to automatically create delegates # to the relevant state class. # This module assumes that if you are mixing it into the `Order` class # then it will be delegating to the `OrderState` class heirarchy. module StateDelegator def self.included model_class model_name = model_class.to_s state_name = "#{model_name}State" state_base = const_get "#{state_name}::Base" state_methods = state_base.public_instance_methods(false) mixin = Module.new do state_methods.each do |state_method| define_method state_method do |*args| current_state.public_send(state_method, *args) end end define_method :current_state do Object.const_get("::#{state_name}").const_get(state).new(self) end private :current_state end model_class.send :include, mixin end end 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,48 @@ require './07_state_delegator' describe StateDelegator do before do class DemoState class Base def initialize stateful; end def foo?; false; end def bar?; false; end end end class Demo < Struct.new(:state) include StateDelegator end end after do DemoState.send :remove_const, :Base Object.send :remove_const, :DemoState Object.send :remove_const, :Demo end subject { Demo.new 'Base' } describe 'delegator definition' do %w| foo? bar? |.each do |predicate| describe "##{predicate}" do it { should respond_to predicate } it 'delegates to the state instance' do expect_any_instance_of(DemoState::Base).to receive(predicate) subject.public_send predicate end end end end describe 'state finder method' do it 'has the current_state method' do expect(subject.private_methods).to include(:current_state) end it 'delegates to the state class' do expect(subject.send :current_state).to be_a(DemoState::Base) end end end 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,28 @@ require './03_shopping_cart_state' require './07_state_delegator' require './99_ar' # This is the ShoppingCart using the StateDelegator mixin. class ShoppingCart < AR::Base include StateDelegator has_many :line_items def ship! current_state.ship! do get_tracking_ticket_no end send_successful_shipping_email end private def get_tracking_ticket_no # noop end def send_successful_shipping_email # noop end end 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,38 @@ require './09_shopping_cart.rb' describe ShoppingCart do %w| line_items add_item! cancel! pay! ship! fulfil! state state= |.each do |method| it { should respond_to method } end describe 'State delegation' do %w| add_item! cancel! pay! ship! fulfil! |.each do |action| let(:current_state) { double :current_state } before do allow(subject).to receive(:current_state).and_return(current_state) end describe "##{action}" do it 'delegates to current state' do expect(current_state).to receive(action) subject.public_send(action) end end end end describe '#ship!' do before { subject.state = 'ReadyToShip' } it 'retrieves tracking info' do expect(subject).to receive(:get_tracking_ticket_no) subject.ship! end it 'sends a shipping email' do expect(subject).to receive(:send_successful_shipping_email) subject.ship! end end end 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,20 @@ # This is a fake AR base class for our purposes.. module AR class Base attr_accessor :state, :line_items def initialize @state = 'New' @line_items = [] end def update_attributes! attrs={} attrs.each do |attr, value| public_send "#{attr}=", value end end def self.has_many _; end end end -
James Harton revised this gist
Jul 23, 2014 . 3 changed files with 97 additions and 0 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -47,6 +47,7 @@ def fulfil! class ReadyToShip < Cancellable def ship! yield if block_given? @cart.update_attributes! state: 'Shipped' end end 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,58 @@ # This is a fake AR base class. require './03_shopping_cart_state' module AR class Base attr_accessor :state, :line_items def initialize @state = 'New' @line_items = [] end def update_attributes! attrs={} attrs.each do |attr, value| public_send "#{attr}=", value end end def self.has_many _; end end end class ShoppingCart < AR::Base has_many :line_items %w| add_item! cancel! pay! fulfil! |.each do |action| define_method action do |*args| current_state.public_send action, *args end end def ship! current_state.ship! do get_tracking_ticket_no end send_successful_shipping_email end private def get_tracking_ticket_no # noop end def send_successful_shipping_email # noop end def current_state ShoppingCartState.const_get(state).new(self) end end 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,38 @@ require './05_shopping_cart.rb' describe ShoppingCart do %w| line_items add_item! cancel! pay! ship! fulfil! state state= |.each do |method| it { should respond_to method } end describe 'State delegation' do %w| add_item! cancel! pay! ship! fulfil! |.each do |action| let(:current_state) { double :current_state } before do allow(subject).to receive(:current_state).and_return(current_state) end describe "##{action}" do it 'delegates to current state' do expect(current_state).to receive(action) subject.public_send(action) end end end end describe '#ship!' do before { subject.state = 'ReadyToShip' } it 'retrieves tracking info' do expect(subject).to receive(:get_tracking_ticket_no) subject.ship! end it 'sends a shipping email' do expect(subject).to receive(:send_successful_shipping_email) subject.ship! end end end -
James Harton revised this gist
Jul 23, 2014 . 2 changed files with 173 additions and 0 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,59 @@ module ShoppingCartState StateError = Class.new(RuntimeError) class Base def initialize cart @cart = cart end %w| add_item! pay! cancel! ship! fulfil! |.each do |action| define_method action do |*_| raise StateError, "Can't call #{action} from #{@cart.state}" end end end class Cancellable < Base def cancel! @cart.update_attributes! state: 'Cancelled' end end class New < Cancellable def add_item! item @cart.line_items << item end def pay! @cart.update_attributes! state: 'Paid' end end class Paid < Cancellable def fulfil! if @cart.all_in_stock? @cart.update_attributes! state: 'ReadyToShip' else @cart.update_attributes! state: 'Backorder' end end end class Backorder < Cancellable def fulfil! @cart.update_attributes! state: 'ReadyToShip' end end class ReadyToShip < Cancellable def ship! @cart.update_attributes! state: 'Shipped' end end class Cancelled < Base end class Shipped < Base end end 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,114 @@ require './03_shopping_cart_state' # This is code I pulled in from another project to mock the # `update_attributes!` method. Sorry about the noise. module MockUpdateAttributes def mock_update_attributes_on(model) update_proc = proc do |attrs| attrs.each do |attr,value| setter = "#{attr}=".to_sym expect(model).to respond_to attr expect(model).to respond_to setter model.public_send setter, value end end allow(model).to receive(:update_attributes!, &update_proc) model end end describe ShoppingCartState do include MockUpdateAttributes let(:cart) { mock_update_attributes_on Struct.new(:state).new } subject { described_class.new cart } describe ShoppingCartState::Base do %w| add_item! pay! cancel! ship! fulfil! |.each do |action| describe "##{action}" do it 'raises a StateError' do expect { subject.public_send action }.to raise_error ShoppingCartState::StateError end end end end describe ShoppingCartState::Cancellable do it { should be_a ShoppingCartState::Base } describe '#cancel!' do it 'changes the cart state to Cancelled' do expect { subject.cancel! }.to change { cart.state }.to('Cancelled') end end end describe ShoppingCartState::New do it { should be_a ShoppingCartState::Cancellable } describe '#add_item!' do it 'adds an item to the cart' do allow(cart).to receive(:line_items).and_return([]) expect { subject.add_item! :item }.to change { cart.line_items.length }.by(1) end end describe '#pay!' do it 'changes the cart state to Paid' do expect { subject.pay! }.to change { cart.state }.to('Paid') end end end describe ShoppingCartState::Paid do it { should be_a ShoppingCartState::Cancellable } describe '#fulfil!' do context "When all line items are in stock" do it 'changes the cart state to ReadyToShip' do allow(cart).to receive(:all_in_stock?).and_return(true) expect { subject.fulfil! }.to change { cart.state }.to('ReadyToShip') end end context "When all line items are not in stock" do it 'changes the cart state to Backorder' do allow(cart).to receive(:all_in_stock?).and_return(false) expect { subject.fulfil! }.to change { cart.state }.to('Backorder') end end end end describe ShoppingCartState::Backorder do it { should be_a ShoppingCartState::Cancellable } describe '#fulfil!' do it 'changes the cart state to ReadyToShip' do expect { subject.fulfil! }.to change { cart.state }.to('ReadyToShip') end end end describe ShoppingCartState::ReadyToShip do it { should be_a ShoppingCartState::Cancellable } describe '#ship!' do it 'changes the cart state to Shipped' do expect { subject.ship! }.to change { cart.state }.to('Shipped') end end end describe ShoppingCartState::Cancelled do it { should be_a ShoppingCartState::Base } it { should_not be_a ShoppingCartState::Cancellable } end describe ShoppingCartState::Shipped do it { should be_a ShoppingCartState::Base } it { should_not be_a ShoppingCartState::Cancellable } end end -
James Harton revised this gist
Jul 23, 2014 . 2 changed files with 56 additions and 0 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,24 @@ # Our simple example of a Turnstile state machine. class Turnstile def initialize @state = "Locked" end def push! @state = "Locked" if unlocked? end def pay! @state = "Unlocked" if locked? end def locked? @state == "Locked" end def unlocked? @state = "Unlocked" end end 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,32 @@ require './01_turnstile' describe Turnstile do subject { described_class.new } it { should be_locked } context "When it is locked" do context "And we push it" do before { subject.push! } it { should be_locked } end context "And we pay it" do before { subject.pay! } it { should be_unlocked } end end context "When it is unlocked" do before { subject.pay! } context "And we push it" do before { subject.push! } it { should be_locked } end context "And we pay it" do before { subject.pay! } it { should be_unlocked } end end end -
James Harton revised this gist
Jul 23, 2014 . 1 changed file with 0 additions and 1 deletion.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1 +0,0 @@ -
James Harton created this gist
Jul 23, 2014 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1 @@ sdfsd