Refactoring Conditional Dispatcher To Command

46
Refactoring Conditional Dispatcher with Command Sreenivas Ananthakrishna (based on Refactoring to Patterns)

description

 

Transcript of Refactoring Conditional Dispatcher To Command

Page 1: Refactoring Conditional Dispatcher To Command

Refactoring Conditional Dispatcher with Command

Sreenivas Ananthakrishna

(based on Refactoring to Patterns)

Page 2: Refactoring Conditional Dispatcher To Command

let’s start with an example...

Page 4: Refactoring Conditional Dispatcher To Command

so what can the ATM do?

Page 5: Refactoring Conditional Dispatcher To Command

so what can the ATM do?

➡ Display the login screen when card is inserted

Page 6: Refactoring Conditional Dispatcher To Command

so what can the ATM do?

➡ Display the login screen when card is inserted

➡ Authenticate the user

Page 7: Refactoring Conditional Dispatcher To Command

so what can the ATM do?

➡ Display the login screen when card is inserted

➡ Authenticate the user

➡ Allow authenticated user to withdraw $$$

Page 8: Refactoring Conditional Dispatcher To Command

ATM Overview

Page 9: Refactoring Conditional Dispatcher To Command

View ATMController

Services

Authentication

Account

Page 10: Refactoring Conditional Dispatcher To Command

View

➡ renders the screen

Services

➡ authenticates the user

➡ transfer/ debit money from account

Controller

➡ handles requests from view

Page 11: Refactoring Conditional Dispatcher To Command

let’s focus on building the ATMController...

Page 12: Refactoring Conditional Dispatcher To Command

and in TDD fashion, the test come first!

Page 13: Refactoring Conditional Dispatcher To Command

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

Page 14: Refactoring Conditional Dispatcher To Command

now, the implementation

Page 15: Refactoring Conditional Dispatcher To Command

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

Page 16: Refactoring Conditional Dispatcher To Command

second test

Page 17: Refactoring Conditional Dispatcher To Command

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

Page 18: Refactoring Conditional Dispatcher To Command

implementation

Page 19: Refactoring Conditional Dispatcher To Command

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

Page 20: Refactoring Conditional Dispatcher To Command

third test

Page 21: Refactoring Conditional Dispatcher To Command

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

Page 22: Refactoring Conditional Dispatcher To Command

implementation

Page 23: Refactoring Conditional Dispatcher To Command

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

Page 24: Refactoring Conditional Dispatcher To Command

addition of new dependencies has broken other tests!

Page 25: Refactoring Conditional Dispatcher To Command

so let’s fix it!

Page 26: Refactoring Conditional Dispatcher To Command

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

Page 27: Refactoring Conditional Dispatcher To Command

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

Page 28: Refactoring Conditional Dispatcher To Command

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

๏ handle method keeps getting bloated

Page 29: Refactoring Conditional Dispatcher To Command

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

๏ handle method keeps getting bloated

๏ which means higher complexity

Page 30: Refactoring Conditional Dispatcher To Command

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

Page 31: Refactoring Conditional Dispatcher To Command

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

Page 32: Refactoring Conditional Dispatcher To Command

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

Page 33: Refactoring Conditional Dispatcher To Command

refactoring mechanics

Page 34: Refactoring Conditional Dispatcher To Command

step 1: compose method

Page 35: Refactoring Conditional Dispatcher To Command

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

Page 36: Refactoring Conditional Dispatcher To Command

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

Page 37: Refactoring Conditional Dispatcher To Command

step 2: extract class

Page 38: Refactoring Conditional Dispatcher To Command

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

Before

Page 39: Refactoring Conditional Dispatcher To Command

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

After

Page 40: Refactoring Conditional Dispatcher To Command

extract superclass

Page 41: Refactoring Conditional Dispatcher To Command

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

Page 42: Refactoring Conditional Dispatcher To Command

configure the controller with map of actions

Page 43: Refactoring Conditional Dispatcher To Command

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

Page 44: Refactoring Conditional Dispatcher To Command

now, even the tests are simpler!

Page 45: Refactoring Conditional Dispatcher To Command

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

Page 46: Refactoring Conditional Dispatcher To Command

• 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