Refactoring Conditional Dispatcher To Command

Post on 22-Apr-2015

1.401 views 4 download

description

 

Transcript of Refactoring Conditional Dispatcher To Command

Refactoring Conditional Dispatcher with Command

Sreenivas Ananthakrishna

(based on Refactoring to Patterns)

let’s start with an example...

so what can the ATM do?

so what can the ATM do?

➡ Display the login screen when card is inserted

so what can the ATM do?

➡ Display the login screen when card is inserted

➡ Authenticate the user

so what can the ATM do?

➡ Display the login screen when card is inserted

➡ Authenticate the user

➡ Allow authenticated user to withdraw $$$

ATM Overview

View ATMController

Services

Authentication

Account

View

➡ renders the screen

Services

➡ authenticates the user

➡ transfer/ debit money from account

Controller

➡ handles requests from view

let’s focus on building the ATMController...

and in TDD fashion, the test come first!

describe "an instance of ", ATMController do it "should render login when card is inserted" do view = mock(View) view.should_receive(:render).

with(:login, {:account_id => 1234}) controller = ATMController.new view controller.handle :card_inserted, {:account_id => 1234} endend

now, the implementation

class ATMController def initialize view @view = view end def handle event, params @view.render :login, {:account_id => params[:account_id]} endend

second test

it "should raise error for unknown event" do view = mock(View) view.should_not_receive(:render)

controller = ATMController.new view lambda {controller.handle(:foo, {})}.

should raise_error(RuntimeError, "cannot handle event foo")

end

implementation

class ATMController def initialize view @view = view end def handle event, params if event == :card_inserted @view.render :login, {:account_id => params[:account_id]} else raise "cannot handle event #{event}" end endend

third test

it "should display withdraw menu when user has authenticated" do view = mock(View) view.should_receive(:render).with(:withdraw_menu) authentication_service = mock(AuthenticationService) authentication_service.should_receive(:authenticate). with(1234, 5678). and_return(true) controller = ATMController.new view, authentication_service controller.handle :authenticate, {:account_id => 1234, :pin => 5678} end

implementation

class ATMController def initialize view, authentication_service @authentication_service = authentication_service @view = view end def handle event, params case event when :card_inserted @view.render :login, {:account_id => params[:account_id]} when :authenticate if @authentication_service. authenticate(params[:account_id], params[:pin]) @view.render :withdraw_menu end else raise "cannot handle event #{event}" end en

addition of new dependencies has broken other tests!

so let’s fix it!

it "should render login when card is inserted" do view = mock(View) view.should_receive(:render).with(:login, {:account_id => 1234}) controller = ATMController.new view controller.handle :card_inserted, {:account_id => 1234} end

authentication_service = mock(AuthenticationService) authentication_service.should_not_receive(:authenticate)

, authentication_service

so, as the controller keeps handling new events...

so, as the controller keeps handling new events...

๏ handle method keeps getting bloated

so, as the controller keeps handling new events...

๏ handle method keeps getting bloated

๏ which means higher complexity

so, as the controller keeps handling new events...

๏ handle method keeps getting bloated

๏ which means higher complexity

๏ adding new events requires changing the controller implementation

so, as the controller keeps handling new events...

๏ handle method keeps getting bloated

๏ which means higher complexity

๏ adding new events requires changing the controller implementation

๏ addition of new receivers also affects existing test cases

let’s see how we can simplify by refactoring to the Command

refactoring mechanics

step 1: compose method

def handle event, params case event when :card_inserted @view.render :login, {:account_id => params[:account_id]} when :authenticate if @authentication_service. authenticate(params[:account_id], params[:pin]) @view.render :withdraw_menu end else raise "cannot handle event #{event}" end end

Before

def handle event, params case event when :card_inserted handle_card_inserted params when :authenticate handle_authenticate params else raise "cannot handle event #{event}" end end

After

step 2: extract class

def handle_authenticate params if @authentication_service. authenticate(params[:account_id], params[:pin]) @view.render :withdraw_menu endend

Before

def handle_authenticate params action = AuthenticateAction.new @view, @authentication_service action.execute paramsend

After

extract superclass

class AuthenticateAction < Action def initialize view, authentication_service @view = view @authentication_service = authentication_service end def execute params if @authentication_service. authenticate(params[:account_id], params[:pin]) @view.render :withdraw_menu end endend

class Action def execute params raise "not implemented!" endend

configure the controller with map of actions

class ATMController def initialize map @actions = map end def handle event, params if @actions.include? event @actions[event].execute params else raise "cannot handle event #{event}" end endend

now, even the tests are simpler!

describe "an instance of ", ATMController do it "should execute the action for the event" do params = {'foo' => 'bar'} action = mock(Action) action.should_receive(:execute).with(params) controller = ATMController.new({:foo_event => action}) controller.handle(:foo_event, params) end it "should raise error for unknown event" do controller = ATMController.new({}) lambda {controller.handle(:foo, {})}. should raise_error "cannot handle event foo" end

end

• Do we need a command pattern in dynamic languages ?

• can we get away with using a block/closure

• What are the different ways in which these commands could be configured?

some points for discussion