The document discusses refactoring code through an "inside out" approach. It begins by showing sample code, then discusses strategies and objectives for refactoring like improving design, adding features, and fixing bugs. Specific refactoring techniques are explained like extracting methods, eliminating duplication, and changing design. The presentation emphasizes using tests during refactoring and considering the "big picture" of strategies and mechanics when refactoring large bodies of code. Coding examples are provided to demonstrate refactoring steps.
10. Why do we change
code?
• Add features
• Fix bugs
11. Why do we change
code?
• Add features
• Fix bugs
• Improve design
12. Why do we change
code?
• Add features
• Fix bugs
• Improve design
• Optimization
13. Why do we change
code?
• Add features
• Fix bugs
• Improve design
• Optimization
14.
15.
16.
17. "Refactoring is a disciplined
technique for restructuring an
existing body of code, altering its
internal structure without
changing its external behavior."
46. -
1 2/10/refactoring
o.com/blog/2011/
http://www.dtsat /
h rough-experiment
st rategies-a-walkt
-
0 9/01/refactoring
http://paulh ammant.com/2011/
experiment
CheckoutControllerTest CheckoutController
Order Cart
47. public class CheckoutControllerTest {
private CheckoutController checkoutController;
private Cart cart = new Cart(); // could as easily be a mock
private Order order = new Order(); // could as easily be a mock
private ModelMap model = new ModelMap();
@Before public void setup() {
checkoutController = new CheckoutController(cart, order);
}
@Test public void checkout_should_add_an_upsell() {
String result = checkoutController.goCheckout(model);
assertThat(result, equalTo("checkout"));
assertThat(model.toString(), equalTo("{cart=Cart{contents=[[iPad]]}}"));
}
@Test public void place_order_in_checkout_should_make_an_order() {
cart.addTo("dummy");
String result = checkoutController.placeOrder(model);
assertThat(result, equalTo("orderPlaced"));
assertThat(model.toString(),
equalTo("{order=Order{cart=Cart{contents=[[dummy]]}}}"));
}
}
48. @Controller public class CheckoutController {
private final Cart cart;
private final Order order;
@Autowired public CheckoutController(Cart cart, Order order) {
this.cart = cart;
this.order = order;
}
@RequestMapping("/go/to/checkout/") public String goCheckout(ModelMap model) {
cart.addTo(findUpsell(cart));
model.addAttribute("cart", cart);
return "checkout";
}
@RequestMapping("/place/order") public String placeOrder(ModelMap model) {
order.place(cart);
model.addAttribute("order", order);
return "orderPlaced";
}
String findUpsell(Cart cart) { return cart.size() == 0 ? "iPad" : "iPod Nano"; }
}
49. Mechanics
t his video on: riment
Watch factoring-expe
com/dt sato/re
p://vimeo.
htt
50. First Attempt...
t his video on: riment
Watch factoring-expe
com/dt sato/re
p://vimeo.
htt
51. Strategy
t his video on: riment
Watch factoring-expe
com/dt sato/re
p://vimeo.
htt
84. Before .......
After
.
............
ds Finished in
0 .49367 secon 0.25355 seco
Finished in res 7 examples, nds
13 examp les, 0 failu 0 failures
class Event < ActiveRecord::Base
named_scope :between_dates, lambda { |start_date, end_date|
{:conditions => {:at => start_date..end_date}}
}
end
lib/calendar_search_controller.rb | 27 +++++++++++++--------------
lib/event.rb | 13 +------------
spec/event_spec.rb | 37 -------------------------------------
3 files changed, 14 insertions(+), 63 deletions(-)
85. Strategizing
1. Centralize date manipulation logic
2. Unify named scopes
3. Extract logic from Controller
4. Break timeframe into separate pieces
86. class CalendarSearchController
attr_reader :events
def index(params)
timeframe = TimeFrame.for(params[:timeframe], params[:start_date],
params[:end_date], params[:hour])
@events = Event.between_dates(timeframe.start_date, timeframe.end_date)
end
end
change to tests:
it "should create time frame from params and retrieve events" do
timeframe = TimeFrame.new(DateTime.now, DateTime.now)
TimeFrame.should_receive(:for).with('today', 'start', 'end',
'hour').and_return(timeframe)
events = [Event.new, Event.new]
Event.should_receive(:between_dates).with(timeframe.start_date,
timeframe.end_date).and_return(events)
@controller.index(:timeframe => 'today', :start_date => 'start', :end_date =>
'end', :hour => 'hour')
@controller.events.should == events
end
87. describe "Acceptance tests" do
around(:each) do |example|
Event.transaction { example.run; raise ActiveRecord::Rollback }
end
before do
@controller = CalendarSearchController.new
@today = DateTime.strptime('06/01/2011', '%m/%d/%Y')
DateTime.stub!(:now).and_return(@today)
end
it "should return today's events" do
today_events = [Event.create(:at => @today), Event.create(:at => @today)]
tomorrow = @today + 1
tomorrow_events = [Event.create(:at => tomorrow), Event.create(:at => tomorrow)]
@controller.index(:timeframe => 'today')
@controller.events.should == today_events
end
it "should return tomorrow's events" ...
it "should return this week's events" ...
it "should return events for specified date range" ...
it "should default date range to this business week" ...
it "returns events for a specified hour" ...
end
88. describe "Acceptance tests" do
around(:each) do |example| After
......
Event.transaction { example.run; raise ActiveRecord::Rollback }
end
Finished in
0.24538 seco
before do 6 examples, nds
0 failures
@controller = CalendarSearchController.new
@today = DateTime.strptime('06/01/2011', '%m/%d/%Y')
DateTime.stub!(:now).and_return(@today)
end
it "should return today's events" do
today_events = [Event.create(:at => @today), Event.create(:at => @today)]
tomorrow = @today + 1
tomorrow_events = [Event.create(:at => tomorrow), Event.create(:at => tomorrow)]
@controller.index(:timeframe => 'today')
@controller.events.should == today_events
end
it "should return tomorrow's events" ...
it "should return this week's events" ...
it "should return events for specified date range" ...
it "should default date range to this business week" ...
it "returns events for a specified hour" ...
end
89. class TimeFrame < Struct.new(:start_date, :end_date)
def self.for(type, start_date, end_date, hour)
case type
when 'tomorrow'
date = DateTime.now + 1
TimeFrame.new(date.at_beginning_of_day, date.end_of_day)
when 'this_week'
TimeFrame.new(DateTime.now, (DateTime.now + 6 ).end_of_day)
when 'custom'
start_date = start_date.blank? ? DateTime.now.beginning_of_week :
DateTime.strptime(start_date, '%m/%d/%Y')
end_date = end_date.blank? ? (DateTime.now.end_of_week - 2) :
DateTime.strptime(end_date, '%m/%d/%Y').end_of_day
TimeFrame.new(start_date, end_date)
when 'hour'
start_date = DateTime.strptime(hour, '%m/%d/%Y %H:%M')
end_date = start_date + 1.hour-1.second
TimeFrame.new(start_date, end_date)
when 'today'
date = DateTime.now
TimeFrame.new(date.at_beginning_of_day, date.end_of_day)
end
end
end
90. describe TimeFrame do
before do
@today = DateTime.strptime('06/01/2011', '%m/%d/%Y')
DateTime.stub!(:now).and_return(@today)
end
describe "today's event" do
it "should use beginning and end of day" do
timeframe = TimeFrame.for('today', nil, nil, nil)
timeframe.start_date.should == @today.at_beginning_of_day
timeframe.end_date.should == @today.end_of_day
end
end
...
end
91. describe TimeFrame do
before do
@today = DateTime.strptime('06/01/2011', '%m/%d/%Y')
DateTime.stub!(:now).and_return(@today)
end
describe "today's event" do
it "should use beginning and end of day" do
timeframe = TimeFrame.for('today', nil, nil, nil)
timeframe.start_date.should == @today.at_beginning_of_day
timeframe.end_date.should == @today.end_of_day
end
Rakefile | 4 ++
end
lib/calendar_search_controller.rb | 24 +---------
...
lib/time_frame.rb | 24 ++++++++++
end
spec/acceptance_tests.rb | 75 +++++++++++++++++++++++++++++++
spec/calendar_search_controller_spec.rb | 75 ++++---------------------------
spec/time_frame_spec.rb | 63 ++++++++++++++++++++++++++
6 files changed, 178 insertions(+), 87 deletions(-)
92. Before
.......
ds
describe d in 0 .25355 secon
Finishe TimeFrame do es
before les, 0 failur
7 examp do
@today = DateTime.strptime('06/01/2011', '%m/%d/%Y')
DateTime.stub!(:now).and_return(@today)
end
describe "today's event" do
it "should use beginning and end of day" do
timeframe = TimeFrame.for('today', nil, nil, nil)
timeframe.start_date.should == @today.at_beginning_of_day
timeframe.end_date.should == @today.end_of_day
end
Rakefile | 4 ++
end
lib/calendar_search_controller.rb | 24 +---------
...
lib/time_frame.rb | 24 ++++++++++
end
spec/acceptance_tests.rb | 75 +++++++++++++++++++++++++++++++
spec/calendar_search_controller_spec.rb | 75 ++++---------------------------
spec/time_frame_spec.rb | 63 ++++++++++++++++++++++++++
6 files changed, 178 insertions(+), 87 deletions(-)
93. Before ........
After
.......
ds Finished in
describe d in 0 .25355 secon
she TimeFrame do
0.17938 seco
nds
Fini 8 examples,
before les, 0 failures
7 ex amp do
0 failures
@today = DateTime.strptime('06/01/2011', '%m/%d/%Y')
DateTime.stub!(:now).and_return(@today)
end
describe "today's event" do
it "should use beginning and end of day" do
timeframe = TimeFrame.for('today', nil, nil, nil)
timeframe.start_date.should == @today.at_beginning_of_day
timeframe.end_date.should == @today.end_of_day
end
Rakefile | 4 ++
end
lib/calendar_search_controller.rb | 24 +---------
...
lib/time_frame.rb | 24 ++++++++++
end
spec/acceptance_tests.rb | 75 +++++++++++++++++++++++++++++++
spec/calendar_search_controller_spec.rb | 75 ++++---------------------------
spec/time_frame_spec.rb | 63 ++++++++++++++++++++++++++
6 files changed, 178 insertions(+), 87 deletions(-)
94. Strategizing
1. Centralize date manipulation logic
2. Unify named scopes
3. Extract logic from Controller
4. Break timeframe into separate pieces
95. class TimeFrame
attr_reader :start_date, :end_date
class Today < TimeFrame
def initialize
date = DateTime.now
@start_date, @end_date = date.at_beginning_of_day, date.end_of_day
end
end
...
end
describe TimeFrame::Today do
it "should use beginning and end of day" do
timeframe = TimeFrame::Today.new
timeframe.start_date.should == @today.at_beginning_of_day
timeframe.end_date.should == @today.end_of_day
end
...
end
96. class TimeFrame
...
def self.for(type, start_date, end_date, hour)
case type
when 'tomorrow' then Tomorrow.new
when 'this_week' then ThisWeek.new
when 'custom' then Custom.new(start_date, end_date)
when 'hour' then Hour.new(hour)
when 'today' then Today.new
end
end
end
describe TimeFrame::Today do
...
describe "parsing" do
it "should parse today's timeframe" do
TimeFrame.for('today', nil, nil, nil).should
be_an_instance_of(TimeFrame::Today)
end
...
end
end
97. class TimeFrame
... Before
.def .self.for(type, start_date, end_date, hour)
... ...
case type
ds
0 .17938 secon
when 'tomorrow' then Tomorrow.new
Finished in
when les, 0 failures ThisWeek.new
8 examp 'this_week' then
when 'custom' then Custom.new(start_date, end_date)
when 'hour' then Hour.new(hour)
when 'today' then Today.new
end
end
end
describe TimeFrame::Today do
...
describe "parsing" do
it "should parse today's timeframe" do
TimeFrame.for('today', nil, nil, nil).should
be_an_instance_of(TimeFrame::Today)
end
...
end
end
98. class TimeFrame
... Before ............
After
def .self.for(type, start_date, end_date, hour) .
.... ...
case type
ds Finished in
when d in 0 .17938 secon
she 'tomorrow' then Tomorrow.new
0.19023 seco
nds
Fini 13 examples,
when les, 0 failures ThisWeek.new
8 examp 'this_week' then
0 failures
when 'custom' then Custom.new(start_date, end_date)
when 'hour' then Hour.new(hour)
when 'today' then Today.new
end
end
end
describe TimeFrame::Today do
...
describe "parsing" do
it "should parse today's timeframe" do
TimeFrame.for('today', nil, nil, nil).should
be_an_instance_of(TimeFrame::Today)
end
...
end
end
99. class TimeFrame
... Before ............
After
def .self.for(type, start_date, end_date, hour) .
.... ...
case type
ds Finished in
when d in 0 .17938 secon
she 'tomorrow' then Tomorrow.new
0.19023 seco
nds
Fini 13 examples,
when les, 0 failures ThisWeek.new
8 examp 'this_week' then
0 failures
when 'custom' then Custom.new(start_date, end_date)
when 'hour' then Hour.new(hour)
when 'today' then Today.new
end
end
end
describe TimeFrame::Today do
...
describe "parsing" do
lib/time_frame.rb | 59 +++++++++++++++++++++++++++++++++-------------
it "should parse today's timeframe" do
spec/time_frame_spec.rb | 44 ++++++++++++++++++++++++++--------
TimeFrame.for('today', nil, nil, nil).should
2 files changed, 75 insertions(+), 28 deletions(-)
be_an_instance_of(TimeFrame::Today)
end
...
end
end
145. Transitional
Architecture
• Go:
• “Branch by Abstraction”
• Migrating from iBatis to Hibernate
• Migrating from Velocity/JsTemplate to
JRuby on Rails
146. Transitional
Architecture
• Go:
• “Branch by Abstraction”
• Migrating from iBatis to Hibernate
• Migrating from Velocity/JsTemplate to
JRuby on Rails
-
0 11/05/make-large
http://continuo usdelivery.com/2
-
l y-with-branch-by
scale-cha nges-incremental
abstraction/
147. References
Books:
"Refactoring: Improving the Design of Existing Code" - Martin Fowler, Kent Beck, John
Brant, William Opdyke, Don Roberts
"Refactoring to Patterns" - Joshua Kerievsky
"Working Effectively with Legacy Code" - Michael Feathers
"Beheading the Software Beast: Relentless Restructurings with The Mikado Method" -
Daniel Brolund, Ola Ellnestam
Credits (fotos):
http://www.flickr.com/photos/dhammza/2227347832/
http://www.flickr.com/photos/pragdave/173640462/
Links:
http://paulhammant.com/2011/09/01/refactoring-experiment
http://continuousdelivery.com/2011/05/make-large-scale-changes-incrementally-with-
branch-by-abstraction/
Code:
https://github.com/dtsato/refactoring_experiment
https://github.com/dtsato/kata-refactoring-calendar
Objetivo: extrair placeOrder para outro controller\n
Extracting placeOrder() into a new OrderController class, and a new OrderControllerTest.\nNote: Controller Annotation is copy/pasted\n
Note: Controller name first typo (Products -> SuggestedProducts)\n\n
Extracting Upseller collaborator\nNote: Controller annotation needs to be copy/pasted\n
\n
N&#xE3;o tem segredo para identificar objetivo (NFRs)\n
N&#xE3;o tem segredo para identificar objetivo (NFRs)\n
N&#xE3;o tem segredo para identificar objetivo (NFRs)\n
\n
\n
\n
\n
\n
\n
\n
\n
Michael Feathers: Effect Sketches\nCluster de m&#xE9;todos/atributos\nMova c&#xF3;digo repetido para um mesmo lugar\n
Michael Feathers: Effect Sketches\nCluster de m&#xE9;todos/atributos\nMova c&#xF3;digo repetido para um mesmo lugar\n
Michael Feathers: Effect Sketches\nCluster de m&#xE9;todos/atributos\nMova c&#xF3;digo repetido para um mesmo lugar\n
Michael Feathers: Effect Sketches\nCluster de m&#xE9;todos/atributos\nMova c&#xF3;digo repetido para um mesmo lugar\n
Aplica&#xE7;&#xE3;o Rails. Cobertura 100%\nForm de busca de eventos\nEm Ruby -> n&#xE3;o tem mesmas ferramentas avan&#xE7;adas de refatora&#xE7;&#xE3;o\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
* Escopo &#xFA;nico: between_dates\n* Mais feio (indo para tr&#xE1;s antes de ir para frente)\n* Isolando mudan&#xE7;a (toda a l&#xF3;gica de datas num &#xFA;nico lugar)\n
* Remo&#xE7;&#xE3;o de c&#xF3;digo > Adi&#xE7;&#xE3;o de c&#xF3;digo\n* Testes mais r&#xE1;pidos pois os testes dos 3 named scopes acessavam o BD\n
* Remo&#xE7;&#xE3;o de c&#xF3;digo > Adi&#xE7;&#xE3;o de c&#xF3;digo\n* Testes mais r&#xE1;pidos pois os testes dos 3 named scopes acessavam o BD\n
* Remo&#xE7;&#xE3;o de c&#xF3;digo > Adi&#xE7;&#xE3;o de c&#xF3;digo\n* Testes mais r&#xE1;pidos pois os testes dos 3 named scopes acessavam o BD\n
\n
Skinny Controller\nTestes do Controller usam mocks para testar colabora&#xE7;&#xE3;o\n
Testes do controller moveram para teste de aceita&#xE7;&#xE3;o (cucumber?)\n\n
* Timeframe s&#xF3; cont&#xEA;m start/end\n* L&#xF3;gica ainda &#xE9; feia\n
* Teste n&#xE3;o acessa mais o BD!\n* Adi&#xE7;&#xE3;o de muitos testes\n* Pouca mudan&#xE7;a no c&#xF3;digo em si\n
* Teste n&#xE3;o acessa mais o BD!\n* Adi&#xE7;&#xE3;o de muitos testes\n* Pouca mudan&#xE7;a no c&#xF3;digo em si\n
* Teste n&#xE3;o acessa mais o BD!\n* Adi&#xE7;&#xE3;o de muitos testes\n* Pouca mudan&#xE7;a no c&#xF3;digo em si\n
\n
* Separa&#xE7;&#xE3;o de subclasses em TimeFrame para remover switch/case\n* Testes mudam junto\n
* Factory method ainda tem switch/case (discutir poss&#xED;vel uso de metaprograma&#xE7;&#xE3;o)\n* Redu&#xE7;&#xE3;o total dos testes unit&#xE1;rios: de 0.4936 para 0.19023 (61%)\n
* Factory method ainda tem switch/case (discutir poss&#xED;vel uso de metaprograma&#xE7;&#xE3;o)\n* Redu&#xE7;&#xE3;o total dos testes unit&#xE1;rios: de 0.4936 para 0.19023 (61%)\n
* Factory method ainda tem switch/case (discutir poss&#xED;vel uso de metaprograma&#xE7;&#xE3;o)\n* Redu&#xE7;&#xE3;o total dos testes unit&#xE1;rios: de 0.4936 para 0.19023 (61%)\n
\n
L&#xED;der T&#xE9;cnico: compartilha a vis&#xE3;o\nWorkshops de arquitetura presente vs. futura\nRequires a disciplined approach to tackling large changes\n
L&#xED;der T&#xE9;cnico: compartilha a vis&#xE3;o\nWorkshops de arquitetura presente vs. futura\nRequires a disciplined approach to tackling large changes\n
L&#xED;der T&#xE9;cnico: compartilha a vis&#xE3;o\nWorkshops de arquitetura presente vs. futura\nRequires a disciplined approach to tackling large changes\n
L&#xED;der T&#xE9;cnico: compartilha a vis&#xE3;o\nWorkshops de arquitetura presente vs. futura\nRequires a disciplined approach to tackling large changes\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
Escolha objetivo, comece de forma inocente, aprenda e desenho um grafo, rollback quando est&#xE1; quebrado, trabalhe das folhas para a ra&#xED;z\n