Fluency is "what you can say without having to think about how to say it." "Refactoring" is a language that describes ways to make your code better. I want to inspire you to learn more of that language, so you can make your code better without having to think about it.
I'll walk you through the process of reworking a 50-line controller action that's hard to comprehend, let alone refactor. We'll tease apart fiendishly intertwined structures, embrace duplication, use dirty tricks to our advantage, and uncover responsibilities—and bugs!—that weren't obvious at first glance.
1. Fluent Refactoring
Sam Livingston-Gray
THERE
WILL BE
CODE!
It may be
this small
(1..100).each do |i|
s = ''
fizz = (i % 3).zero?
buzz = (i % 5).zero?
s << 'Fizz' if fizz
s << 'Buzz' if buzz
s << '!' if fizz || buzz
s = i if s =~ /^$/
puts s
end
1
22. http://www.jamesshore.com/Blog/Proficiencies-of-Planning.html
Level 1
Tarzan at
a party
“Beer!”
“Good party.”
Level 2
Going to
the party
"Where is the party?"
"How do I get to the party?"
Level 3
Discussing
the party
"What happened at the party
last night?"
Level 4 Charlie Rose "Should parties be illegal?"
Levels of Proficiency
22
23. Re·fac·tor·ing (noun)
"...a disciplined technique for
restructuring an existing body of code,
altering its internal structure without
changing its external behavior."
-refactoring.com
23
25. "Yeah, we're going to have to take
a couple of weeks out of the schedule
for refactoring, and that's probably going
to break some stuff."
Doin It Rong
25
26. "Yeah, we're going to have to take
a couple of weeks out of the schedule
for refactoring, and that's probably going
to break some stuff."
Doin It Rong
26
27. "Yeah, we're going to have to take
a couple of weeks out of the schedule
for refactoring, and that's probably going
to break some stuff."
Doin It Rong
27
28. Re·fac·tor·ing (noun)
"...a disciplined technique for
restructuring an existing body of code,
altering its internal structure without
changing its external behavior."
-refactoring.com
28
31. Re·fac·tor·ing (noun)
"...a disciplined technique for
restructuring an existing body of
code, altering its internal structure
without changing its external
behavior."
-refactoring.com
31
32. Re·fac·tor·ing (noun)
"...a disciplined technique for
restructuring an existing body of
code, altering its internal structure
without changing its external
behavior."
32
47. class InstallationsController < ActionController::Base
# lots more stuff...
def schedule
desired_date = params[:desired_date]
if request.xhr?
begin
if @installation.pending_credit_check?
render :json => {:errors => ["Cannot schedule installation while credit check is pending"]}, :status => 400
return
end
audit_trail_for(current_user) do
if @installation.schedule!(desired_date, :installation_type => params[:installation_type], :city => @city)
if @installation.scheduled_date
date = @installation.scheduled_date.in_time_zone(@installation.city.timezone).to_date
render :json => {:errors => nil, :html => schedule_response(@installation, date)}
end
else
render :json => {:errors => [%Q{Could not update installation. #{@installation.errors.full_messages.join(' ')}}] }
end
end
rescue ActiveRecord::RecordInvalid => e
render :json => {:errors => [e.message] }
rescue ArgumentError => e
render :json => {:errors => ["Could not schedule installation. Start by making sure the desired date is on a business day."]}
end
else
if @installation.pending_credit_check?
flash[:error] = "Cannot schedule installation while credit check is pending"
redirect_to installations_path(:city_id => @installation.city_id, :view => "calendar") and return
end
begin
audit_trail_for(current_user) do
if @installation.schedule!(desired_date, :installation_type => params[:installation_type], :city => @city)
if @installation.scheduled_date
if @installation.customer_provided_equipment?
flash[:success] = %Q{Installation scheduled}
else
flash[:success] = %Q{Installation scheduled! Don't forget to order the equipment also.}
end
end
else
flash[:error] = %Q{Could not schedule installation, check the phase of the moon}
end
end
rescue => e
flash[:error] = e.message
end
redirect_to(@installation.customer_provided_equipment? ? customer_provided_installations_path
: installations_path(:city_id => @installation.city_id, :view => "calendar"))
end
end
# lots more stuff...
end
47
48. class InstallationsController < ActionController::Base
# lots more stuff...
def schedule
desired_date = params[:desired_date]
if request.xhr?
begin
if @installation.pending_credit_check?
render :json => {:errors => ["Cannot schedule installation while credit check is pending"]}, :status => 400
return
end
audit_trail_for(current_user) do
if @installation.schedule!(desired_date, :installation_type => params[:installation_type], :city => @city)
if @installation.scheduled_date
date = @installation.scheduled_date.in_time_zone(@installation.city.timezone).to_date
render :json => {:errors => nil, :html => schedule_response(@installation, date)}
end
else
render :json => {:errors => [%Q{Could not update installation. #{@installation.errors.full_messages.join(' ')}}] }
end
end
rescue ActiveRecord::RecordInvalid => e
render :json => {:errors => [e.message] }
rescue ArgumentError => e
render :json => {:errors => ["Could not schedule installation. Start by making sure the desired date is on a business day."]}
end
else
if @installation.pending_credit_check?
flash[:error] = "Cannot schedule installation while credit check is pending"
redirect_to installations_path(:city_id => @installation.city_id, :view => "calendar") and return
end
begin
audit_trail_for(current_user) do
if @installation.schedule!(desired_date, :installation_type => params[:installation_type], :city => @city)
if @installation.scheduled_date
if @installation.customer_provided_equipment?
flash[:success] = %Q{Installation scheduled}
else
flash[:success] = %Q{Installation scheduled! Don't forget to order the equipment also.}
end
end
else
flash[:error] = %Q{Could not schedule installation, check the phase of the moon}
end
end
rescue => e
flash[:error] = e.message
end
redirect_to(@installation.customer_provided_equipment? ? customer_provided_installations_path
: installations_path(:city_id => @installation.city_id, :view => "calendar"))
end
end
# lots more stuff...
end
Observations
~800 lines in file
~50 lines in method
Longest line: 177 chars
Indentation: 4-16 spaces
Nested control
structures:
audit_trail_for
begin/rescue/end
if/else/end
48
71. if request.xhr?
begin
if @installation.pending_credit_check?
render :json => #...
return
end
#...
end
else
if @installation.pending_credit_check?
flash[:error] = #...
redirect_to installations_path(:city_id =>
return
end
begin
#...
end
if request.xhr?
if @installation.pend
render :json => #..
return
end
else
if @installation.pend
flash[:error] = #..
redirect_to(...) an
return
end
end
if request.xhr?
#...
else
#...71
79. if ajax
if pending_credit_check
render :json => #...
return
end
else
if pending_credit_check
flash[:error] = #...
redirect_to #...
return
end
end
if ajax
if pending_credit_che
render :json => #..
return
end
end
if not ajax
if pending_credit_che
flash[:error] = #..
redirect_to #...
return
end
end
79
80. if ajax
if pending_credit_check
render :json => #...
return
end
end
if not ajax
if pending_credit_check
flash[:error] = #...
redirect_to #...
return
end
end
if ajax && pending_cred
render :json => #...
return
end
if (not ajax) && pendin
flash[:error] = #...
redirect_to #...
return
end
80
81. if ajax && pending_credit_check
render :json => #...
return
end
if (not ajax) && pending_credit_check
flash[:error] = #...
redirect_to #...
return
end
if pending_credit_check
if ajax
render :json => #..
return
end
if not ajax
flash[:error] = #..
redirect_to #...
return
end
end
81
82. if pending_credit_check
if ajax
render :json => #...
return
end
if not ajax
flash[:error] = #...
redirect_to #...
return
end
end
if pending_credit_check
if ajax
render :json => #..
return
else
flash[:error] = #..
redirect_to #...
return
end
end
82
83. if pending_credit_check
if ajax
render :json => #...
return
else
flash[:error] = #...
redirect_to #...
return
end
end
if pending_credit_check
if ajax
render :json => #..
else
flash[:error] = #..
redirect_to #...
end
return
end
83
84. if ajax
if pending_credit_check
render :json => #...
return
end
else
if pending_credit_check
flash[:error] = #...
redirect_to #...
return
end
end
if pending_credit_check
if ajax
render :json => #..
else
flash[:error] = #..
redirect_to #...
end
return
end
84
85. if pending_credit_check
if ajax
render :json => #...
else
flash[:error] = #...
redirect_to #...
end
return
end
if pending_credit_check
cant_schedule_while_c
return
end
85
91. begin
begin
raise “wtf” if coin.toss.heads?
rescue
if request.xhr?
raise “tfw” if tuesday?
else
raise “yak” if Moon.gibbous?
end
end
rescue => e
raise e
end
begin
begin
raise “wtf” if coin
rescue
if request.xhr?
raise “tfw” if tu
else
raise “yak” if Mo
end
end
rescue => e
if request.xhr?
raise e
else
raise e
end
end91
92. begin
begin
raise “wtf” if coin.toss.heads?
rescue
if request.xhr?
raise “tfw” if tuesday?
else
raise “yak” if Moon.gibbous?
end
end
rescue => e
if request.xhr?
raise e
else
raise e
end
end
begin
begin
raise “wtf” if coin
rescue
if request.xhr?
# DO NOTHING
else
raise “yak” if Mo
end
end
rescue => e
if request.xhr?
raise “tfw” if tues
else
raise e
end
end92
93. begin
begin
raise “wtf” if coin.toss.heads?
rescue
if request.xhr?
# DO NOTHING
else
raise “yak” if Moon.gibbous?
end
end
rescue => e
if request.xhr?
raise “tfw” if tuesday?
else
raise e
end
end
begin
begin
raise “wtf” if coin
rescue
if request.xhr?
# DO NOTHING
else
# DO NOTHING
end
end
rescue => e
if request.xhr?
raise “tfw” if tues
else
raise “yak” if Moon
end
end93
94. begin
begin
raise “wtf” if coin.toss.heads?
rescue
if request.xhr?
# DO NOTHING
else
# DO NOTHING
end
end
rescue => e
if request.xhr?
raise “tfw” if tuesday?
else
raise “yak” if Moon.gibbous?
end
end
begin
begin
raise “wtf” if coin
rescue
# DO NOTHING
end
rescue => e
if request.xhr?
raise “tfw” if tues
else
raise “yak” if Moon
end
end
94
95. begin
begin
raise “wtf” if coin.toss.heads?
rescue
# DO NOTHING
end
rescue => e
if request.xhr?
raise “tfw” if tuesday?
else
raise “yak” if Moon.gibbous?
end
end
begin
begin
raise “wtf” if coin
end
rescue => e
if request.xhr?
raise “tfw” if tues
else
raise “yak” if Moon
end
end
95
96. begin
begin
raise “wtf” if coin.toss.heads?
end
rescue => e
if request.xhr?
raise “tfw” if tuesday?
else
raise “yak” if Moon.gibbous?
end
end
begin
raise “wtf” if coin.t
rescue => e
if request.xhr?
raise “tfw” if tues
else
raise “yak” if Moon
end
end
96
97. begin
raise “wtf” if coin.toss.heads?
rescue => e
if request.xhr?
raise “tfw” if tuesday?
else
raise “yak” if Moon.gibbous?
end
end
begin
raise “wtf” if coin.t
rescue => e
handle_exception(e)
end
97
99. class ScheduleInstallation
def call
desired_date = params[:desired_date]
if @installation.pending_credit_check?
cant_schedule_while_credit_check_pending
return
end
begin
if request.xhr?
audit_trail_for(current_user) do
if @installation.schedule!(desired_date, :installation_type => params[:installation_type], :city => @city)
if @installation.scheduled_date
date = @installation.scheduled_date.in_time_zone(@installation.city.timezone).to_date
render :json => {:errors => nil, :html => schedule_response(@installation, date)}
end
else
render :json => {:errors => [%Q{Could not update installation. #{@installation.errors.full_messages.join(' ')}}] }
end
end
else
audit_trail_for(current_user) do
if @installation.schedule!(desired_date, :installation_type => params[:installation_type], :city => @city)
if @installation.scheduled_date
if @installation.customer_provided_equipment?
flash[:success] = %Q{Installation scheduled}
else
flash[:success] = %Q{Installation scheduled! Don't forget to order the equipment also.}
end
end
else
flash[:error] = %Q{Could not schedule installation, check the phase of the moon}
end
end
redirect_to(@installation.customer_provided_equipment? ? customer_provided_installations_path : installations_path(:city_id =>
@installation.city_id, :view => "calendar"))
end
rescue Exception => e
handle_exception e
end
end
end
99
100. class ScheduleInstallation
def call
desired_date = params[:desired_date]
if @installation.pending_credit_check?
cant_schedule_while_credit_check_pending
return
end
begin
audit_trail_for(current_user) do
if request.xhr?
if @installation.schedule!(desired_date, :installation_type => params[:installation_type], :city => @city)
if @installation.scheduled_date
date = @installation.scheduled_date.in_time_zone(@installation.city.timezone).to_date
render :json => {:errors => nil, :html => schedule_response(@installation, date)}
end
else
render :json => {:errors => [%Q{Could not update installation. #{@installation.errors.full_messages.join(' ')}}] }
end
else
if @installation.schedule!(desired_date, :installation_type => params[:installation_type], :city => @city)
if @installation.scheduled_date
if @installation.customer_provided_equipment?
flash[:success] = %Q{Installation scheduled}
else
flash[:success] = %Q{Installation scheduled! Don't forget to order the equipment also.}
end
end
else
flash[:error] = %Q{Could not schedule installation, check the phase of the moon}
end
redirect_to(@installation.customer_provided_equipment? ? customer_provided_installations_path : installations_path(:city_id =>
@installation.city_id, :view => "calendar"))
end
end
rescue Exception => e
handle_exception e
end
end
end
100
101. class ScheduleInstallation
def call
if @installation.pending_credit_check?
cant_schedule_while_credit_check_pending
return
end
begin
audit_trail_for(current_user) do
if request.xhr?
if @installation.schedule!(params[:desired_date], :installation_type => params[:installation_type], :city => @city)
if @installation.scheduled_date
date = @installation.scheduled_date.in_time_zone(@installation.city.timezone).to_date
render :json => {:errors => nil, :html => schedule_response(@installation, date)}
end
else
render :json => {:errors => [%Q{Could not update installation. #{@installation.errors.full_messages.join(' ')}}] }
end
else
if @installation.schedule!(params[:desired_date], :installation_type => params[:installation_type], :city => @city)
if @installation.scheduled_date
if @installation.customer_provided_equipment?
flash[:success] = %Q{Installation scheduled}
else
flash[:success] = %Q{Installation scheduled! Don't forget to order the equipment also.}
end
end
else
flash[:error] = %Q{Could not schedule installation, check the phase of the moon}
end
redirect_to(@installation.customer_provided_equipment? ? customer_provided_installations_path : installations_path(:city_id =>
@installation.city_id, :view => "calendar"))
end
end
rescue Exception => e
handle_exception e
end
end
end
101
102. class ScheduleInstallation
def call
if @installation.pending_credit_check?
cant_schedule_while_credit_check_pending
return
end
begin
audit_trail_for(current_user) do
success = schedule!
if request.xhr?
if success
if @installation.scheduled_date
date = @installation.scheduled_date.in_time_zone(@installation.city.timezone).to_date
render :json => {:errors => nil, :html => schedule_response(@installation, date)}
end
else
render :json => {:errors => [%Q{Could not update installation. #{@installation.errors.full_messages.join(' ')}}] }
end
else
if success
if @installation.scheduled_date
if @installation.customer_provided_equipment?
flash[:success] = %Q{Installation scheduled}
else
flash[:success] = %Q{Installation scheduled! Don't forget to order the equipment also.}
end
end
else
flash[:error] = %Q{Could not schedule installation, check the phase of the moon}
end
redirect_to(@installation.customer_provided_equipment? ? customer_provided_installations_path : installations_path(:city_id =>
@installation.city_id, :view => "calendar"))
end
end
rescue Exception => e
handle_exception e
end
end
end
102
103. class ScheduleInstallation
def call
if @installation.pending_credit_check?
cant_schedule_while_credit_check_pending
return
end
begin
audit_trail_for(current_user) do
success = schedule!
if success
if request.xhr?
if @installation.scheduled_date
date = @installation.scheduled_date.in_time_zone(@installation.city.timezone).to_date
render :json => {:errors => nil, :html => schedule_response(@installation, date)}
end
else
if @installation.scheduled_date
if @installation.customer_provided_equipment?
flash[:success] = %Q{Installation scheduled}
else
flash[:success] = %Q{Installation scheduled! Don't forget to order the equipment also.}
end
end
redirect_to(@installation.customer_provided_equipment? ? customer_provided_installations_path : installations_path(:city_id
=> @installation.city_id, :view => "calendar"))
end
else
if request.xhr?
render :json => {:errors => [%Q{Could not update installation. #{@installation.errors.full_messages.join(' ')}}] }
else
flash[:error] = %Q{Could not schedule installation, check the phase of the moon}
redirect_to(@installation.customer_provided_equipment? ? customer_provided_installations_path : installations_path(:city_id
=> @installation.city_id, :view => "calendar"))
end
end
end
rescue Exception => e
handle_exception e
end
end
end
103
104. class ScheduleInstallation
def call
if @installation.pending_credit_check?
cant_schedule_while_credit_check_pending
return
end
begin
audit_trail_for(current_user) do
success = schedule!
if success
if request.xhr?
if @installation.scheduled_date
date = @installation.scheduled_date.in_time_zone(@installation.city.timezone).to_date
render :json => {:errors => nil, :html => schedule_response(@installation, date)}
end
else
if @installation.scheduled_date
if @installation.customer_provided_equipment?
flash[:success] = %Q{Installation scheduled}
else
flash[:success] = %Q{Installation scheduled! Don't forget to order the equipment also.}
end
end
redirect_to(@installation.customer_provided_equipment? ? customer_provided_installations_path : installations_path(:city_id
=> @installation.city_id, :view => "calendar"))
end
else
scheduling_failed
end
end
rescue Exception => e
handle_exception e
end
end
end
104
105. class ScheduleInstallation
def call
if @installation.pending_credit_check?
cant_schedule_while_credit_check_pending
return
end
begin
audit_trail_for(current_user) do
success = schedule!
if success
if @installation.scheduled_date
if request.xhr?
date = @installation.scheduled_date.in_time_zone(@installation.city.timezone).to_date
render :json => {:errors => nil, :html => schedule_response(@installation, date)}
else
if @installation.customer_provided_equipment?
flash[:success] = %Q{Installation scheduled}
else
flash[:success] = %Q{Installation scheduled! Don't forget to order the equipment also.}
end
end
end
if request.xhr?
# do nothing
else
redirect_to(@installation.customer_provided_equipment? ? customer_provided_installations_path : installations_path(:city_id
=> @installation.city_id, :view => "calendar"))
end
else
scheduling_failed
end
end
rescue Exception => e
handle_exception e
end
end
end
105
106. class ScheduleInstallation
def call
if @installation.pending_credit_check?
cant_schedule_while_credit_check_pending
return
end
begin
audit_trail_for(current_user) do
success = schedule!
if success
if @installation.scheduled_date
scheduling_succeeded
end
if request.xhr?
# do nothing
else
redirect_to(@installation.customer_provided_equipment? ? customer_provided_installations_path : installations_path(:city_id
=> @installation.city_id, :view => "calendar"))
end
else
scheduling_failed
end
end
rescue Exception => e
handle_exception e
end
end
end
106
107. class ScheduleInstallation
def call
if @installation.pending_credit_check?
cant_schedule_while_credit_check_pending
return
end
begin
audit_trail_for(current_user) do
success = schedule!
if success
if @installation.scheduled_date
scheduling_succeeded
end
do_post_success_cleanup
else
scheduling_failed
end
end
rescue Exception => e
handle_exception e
end
end
end
107
108. class ScheduleInstallation
def call
if @installation.pending_credit_check?
cant_schedule_while_credit_check_pending
return
end
begin
audit_trail_for(current_user) do
if schedule!
if @installation.scheduled_date
scheduling_succeeded
end
do_post_success_cleanup
else
scheduling_failed
end
end
rescue Exception => e
handle_exception e
end
end
end
108
109. class ScheduleInstallation
def call
if @installation.pending_credit_check?
cant_schedule_while_credit_check_pending
return
end
begin
audit_trail_for(current_user) do
if schedule!
if @installation.scheduled_date
scheduling_succeeded
end
do_post_success_cleanup
else
scheduling_failed
end
end
rescue Exception => e
handle_exception e
end
end
end
request.xhr?
109
111. class ScheduleInstallation
def cannot_schedule_while_#...
if request.xhr?
# ...1 line...
else
# ...2 lines...
end
end
def handle_exception(e)
if request.xhr?
# ...7 lines...
else
# ...2 lines...
end
end
def scheduling_failed
if request.xhr?
# ...1 line...
else
# ...2 lines...
end
end
def scheduling_succeeded
if request.xhr?
# ...2 lines...
else
# ...5 lines...
end
end
def do_post_success_cleanup
if request.xhr?
# DO NOTHING
else
# ...1 line...
end
end
end
111
125. class ScheduleInstallation
def call
private
def cannot_schedule_while_credit_check_pendin
def handle_exception(e)
def scheduling_failed
def scheduling_succeeded
def do_post_success_cleanup
end
class Responder
end
class ScheduleInstallat
def call
end
class Responder
def cannot_schedule_w
def handle_exception(
def scheduling_failed
def scheduling_succee
def do_post_success_c
end
125
127. class Responder
def cannot_schedule_while_credit_check_pending
# ...6 lines...
end
def cannot_schedule_while_credit_check_pending
if request.xhr?
# ...1 line...
else
# ...2 lines...
end
end
def handle_exception(e)
if request.xhr?
# ...7 lines...
else
# ...2 lines...
end
end
def scheduling_failed
if request.xhr?
# ...1 line...
else
# ...2 lines...
end
end
def scheduling_succeeded
if request.xhr?
# ...2 lines...
else
# ...5 lines...
end
end
def do_post_success_cleanup
if request.xhr?
# NOP
else
# ...2 lines...
end
end
end
130. class Responder
def cannot_schedule_while_credit_check_pending
def handle_exception(e)
def scheduling_failed
def scheduling_succeeded
def do_post_success_cleanup
end
class AJAXResponder
def
cannot_schedule_while_cre
def handle_exception(e)
def scheduling_failed
def scheduling_succeede
def do_post_success_cle
end
class HTMLResponder
def
cannot_schedule_while_cre
def handle_exception(e)
def scheduling_failed
def scheduling_succeede
def do_post_success_cle
end
131. class AJAXResponder
def scheduling_failed
if request.xhr?
render :json => #...
else
flash[:error] = #...
redirect_to #...
end
end
end
class HTMLResponder
def scheduling_failed
if request.xhr?
render :json => #...
else
flash[:error] = #...
redirect_to #...
end
end
end
class AJAXResponder
def scheduling_failed
render :json => #...
end
end
class HTMLResponder
def scheduling_failed
flash[:error] = #...
redirect_to #...
end
end