Refactoring Conditional Dispatcher To Command
-
Upload
melbournepatterns -
Category
Business
-
view
1.398 -
download
4
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...
The ATM!
http://www.flickr.com/photos/martineian/485029758/http://www.flickr.com/photos/fastlanedaily/509830016/
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