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

Refactoring Conditional Dispatcher To Command

Apr 22, 2015

Download

Business

 
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
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