Slides from my session at QCon SP on "Refactoring at Large". Covering examples of refactoring in Java, Ruby/Rails and some ways to make architecture changes in a safer and step-by-step way
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]]}}}"));
}
}
@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"; }
}
@Controller public class CheckoutController {
“Extrair Clas
se”
private final Cart cart; OrderControll
er em
private final Order order; placeOrder()
gerando get/s
et
@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"; }
}
@Controller public class CheckoutController {
private final Cart cart;
private final Order order;
private final OrderController orderController = new OrderController(this);
@Autowired public CheckoutController(Cart cart, Order order) {
this.cart = cart;
this.order = order;
}
@RequestMapping("/go/to/checkout/") public String goCheckout(ModelMap model) {
// other stuff
cart.addTo(findUpsell(cart));
model.addAttribute("cart", cart);
return "checkout";
}
@RequestMapping("/place/order") public String placeOrder(ModelMap model) {
return orderController.placeOrder(model);
}
String findUpsell(Cart cart) { return cart.size() == 0 ? "iPad" : "iPod Nano"; }
public Order getOrder() { return order; }
public Cart getCart() { return cart; }
}
@Controller public class CheckoutController {
⌘⌥N
private final Cart cart;
“Inline” inic
private final Order order; ialização de
private final OrderController orderController = orderController
new OrderController(this);
@Autowired public CheckoutController(Cart cart, Order order) {
this.cart = cart;
this.order = order;
}
@RequestMapping("/go/to/checkout/") public String goCheckout(ModelMap model) {
// other stuff
cart.addTo(findUpsell(cart));
model.addAttribute("cart", cart);
return "checkout";
}
@RequestMapping("/place/order") public String placeOrder(ModelMap model) {
return orderController.placeOrder(model);
}
String findUpsell(Cart cart) { return cart.size() == 0 ? "iPad" : "iPod Nano"; }
public Order getOrder() { return order; }
public Cart getCart() { return cart; }
}
@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) {
// other stuff
cart.addTo(findUpsell(cart));
model.addAttribute("cart", cart);
return "checkout";
}
@RequestMapping("/place/order") public String placeOrder(ModelMap model) {
return new OrderController(this).placeOrder(model);
}
String findUpsell(Cart cart) { return cart.size() == 0 ? "iPad" : "iPod Nano"; }
public Order getOrder() { return order; }
public Cart getCart() { return cart; }
}
public class OrderController {
private final CheckoutController checkoutController;
public OrderController(CheckoutController checkoutController) {
this.checkoutController = checkoutController;
}
@RequestMapping("/place/order")
public String placeOrder(ModelMap model) {
checkoutController.getOrder().place(checkoutController.getCart());
model.addAttribute("order", checkoutController.getOrder());
return "orderPlaced";
}
}
⌘⌥F
“Introduzir a
tributo” orde
cart em Order r e
Controller
public class OrderController {
private final CheckoutController checkoutController;
public OrderController(CheckoutController checkoutController) {
this.checkoutController = checkoutController;
}
@RequestMapping("/place/order")
public String placeOrder(ModelMap model) {
checkoutController.getOrder().place(checkoutController.getCart());
model.addAttribute("order", checkoutController.getOrder());
return "orderPlaced";
}
}
public class OrderController {
private final CheckoutController checkoutController;
private final Order order;
private final Cart cart;
public OrderController(CheckoutController checkoutController) {
this.checkoutController = checkoutController;
order = this.checkoutController.getOrder();
cart = this.checkoutController.getCart();
}
@RequestMapping("/place/order")
public String placeOrder(ModelMap model) {
order.place(cart);
model.addAttribute("order", order);
return "orderPlaced";
}
}
⌘⌥P / ⌥ / ⌘Y
“Introduzir p
arâmetro” ord
e cart no con er
public class OrderController { strutor de
OrderControl
private final CheckoutController checkoutController; ler e inic
ializar
private final Order order;
private final Cart cart;
public OrderController(CheckoutController checkoutController) {
this.checkoutController = checkoutController;
order = this.checkoutController.getOrder();
cart = this.checkoutController.getCart();
}
@RequestMapping("/place/order")
public String placeOrder(ModelMap model) {
order.place(cart);
model.addAttribute("order", order);
return "orderPlaced";
}
}
public class OrderController {
private final CheckoutController checkoutController;
private final Order order;
private final Cart cart;
public OrderController(CheckoutController checkoutController,
Order order, Cart cart) {
this.checkoutController = checkoutController;
this.order = order;
this.cart = cart;
}
@RequestMapping("/place/order")
public String placeOrder(ModelMap model) {
order.place(cart);
model.addAttribute("order", order);
return "orderPlaced";
}
}
⌘⌫
“Remoção Segu
ra” do atribu
public class OrderController { e parâmetro to
private final CheckoutController checkoutController;
checkoutContr
oller
private final Order order;
private final Cart cart;
public OrderController(CheckoutController checkoutController,
Order order, Cart cart) {
this.checkoutController = checkoutController;
this.order = order;
this.cart = cart;
}
@RequestMapping("/place/order")
public String placeOrder(ModelMap model) {
order.place(cart);
model.addAttribute("order", order);
return "orderPlaced";
}
}
public class OrderController {
private final Order order;
private final Cart cart;
public OrderController(Order order, Cart cart) {
this.order = order;
this.cart = cart;
}
@RequestMapping("/place/order")
public String placeOrder(ModelMap model) {
order.place(cart);
model.addAttribute("order", order);
return "orderPlaced";
}
}
@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) {
// other stuff
cart.addTo(findUpsell(cart));
model.addAttribute("cart", cart);
return "checkout";
}
@RequestMapping("/place/order") public String placeOrder(ModelMap model) {
return new OrderController(order, cart).placeOrder(model);
}
String findUpsell(Cart cart) { return cart.size() == 0 ? "iPad" : "iPod Nano"; }
public Order getOrder() { return order; }
public Cart getCart() { return cart; }
}
@Controller public class CheckoutController {
⌘⌫
“Remoção Segu
private final Cart cart; ra” de getCar
e getOrder() t()
private final Order order; em
CheckoutContr
oller
@Autowired public CheckoutController(Cart cart, Order order) {
this.cart = cart;
this.order = order;
}
@RequestMapping("/go/to/checkout/") public String goCheckout(ModelMap model) {
// other stuff
cart.addTo(findUpsell(cart));
model.addAttribute("cart", cart);
return "checkout";
}
@RequestMapping("/place/order") public String placeOrder(ModelMap model) {
return new OrderController(order, cart).placeOrder(model);
}
String findUpsell(Cart cart) { return cart.size() == 0 ? "iPad" : "iPod Nano"; }
public Order getOrder() { return order; }
public Cart getCart() { return cart; }
}
@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) {
// other stuff
cart.addTo(findUpsell(cart));
model.addAttribute("cart", cart);
return "checkout";
}
@RequestMapping("/place/order") public String placeOrder(ModelMap model) {
return new OrderController(order, cart).placeOrder(model);
}
String findUpsell(Cart cart) { return cart.size() == 0 ? "iPad" : "iPod Nano"; }
}
⌘⌥N
@Controller public class CheckoutController { “Inline” méto
do placeOrder
mudar argumen e
private final Cart cart; tos para usar
cart e order
private final Order order; no teste
@Autowired public CheckoutController(Cart cart, Order order) {
this.cart = cart;
this.order = order;
}
@RequestMapping("/go/to/checkout/") public String goCheckout(ModelMap model) {
// other stuff
cart.addTo(findUpsell(cart));
model.addAttribute("cart", cart);
return "checkout";
}
@RequestMapping("/place/order") public String placeOrder(ModelMap model) {
return new OrderController(order, cart).placeOrder(model);
}
String findUpsell(Cart cart) { return cart.size() == 0 ? "iPad" : "iPod Nano"; }
}
@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) {
// other stuff
cart.addTo(findUpsell(cart));
model.addAttribute("cart", cart);
return "checkout";
}
String findUpsell(Cart cart) { return cart.size() == 0 ? "iPad" : "iPod Nano"; }
}
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 = new OrderController(order, cart).placeOrder(model);
assertThat(result, equalTo("orderPlaced"));
assertThat(model.toString(),
equalTo("{order=Order{cart=Cart{contents=[[dummy]]}}}"));
}
}
public class CheckoutControllerTest {
⌘⌥F
“ Introduzir at
private CheckoutController checkoutController; ributo”
private or be a ntro
Cart cart = new Cart(); // could as easily derComock ller no
teste,
private inicialia ando
Order order = new Order(); // could as easily be z mock no
setUp()
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 = new OrderController(order, cart).placeOrder(model);
assertThat(result, equalTo("orderPlaced"));
assertThat(model.toString(),
equalTo("{order=Order{cart=Cart{contents=[[dummy]]}}}"));
}
}
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();
private OrderController orderController;
@Before public void setup() {
checkoutController = new CheckoutController(cart, order);
orderController = new OrderController(order, cart);
}
@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 = orderController.placeOrder(model);
assertThat(result, equalTo("orderPlaced"));
assertThat(model.toString(),
equalTo("{order=Order{cart=Cart{contents=[[dummy]]}}}"));
}
}
@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) {
// other stuff
cart.addTo(findUpsell(cart));
model.addAttribute("cart", cart);
return "checkout";
}
String findUpsell(Cart cart) { return cart.size() == 0 ? "iPad" : "iPod Nano"; }
}
⌘⌫
“Remoção Segu
ra” do atribu
e parâmetro o to
@Controller public class CheckoutController { rder no
CheckoutContr
oller
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) {
// other stuff
cart.addTo(findUpsell(cart));
model.addAttribute("cart", cart);
return "checkout";
}
String findUpsell(Cart cart) { return cart.size() == 0 ? "iPad" : "iPod Nano"; }
}
@Controller public class CheckoutController {
private final Cart cart;
@Autowired public CheckoutController(Cart cart) {
this.cart = cart;
}
@RequestMapping("/go/to/checkout/") public String goCheckout(ModelMap model) {
// other stuff
cart.addTo(findUpsell(cart));
model.addAttribute("cart", cart);
return "checkout";
}
String findUpsell(Cart cart) { return cart.size() == 0 ? "iPad" : "iPod Nano"; }
}
r
dtsato/kata-ref actoring-calenda
http ://github.com/
CalendarSearchController
CalendarSearchController
Test
Event Event
def index(params)
scope = Event
case params[:timeframe]
when 'tomorrow'
scope = scope.between_day(DateTime.now + 1)
when 'this_week'
scope = scope.between_dates(DateTime.now, ( DateTime.now + 6 ).end_of_day)
when 'custom'
if params[:start_date].blank?
params[:start_date] = DateTime.now.beginning_of_week.strftime('%m/%d/%Y')
end
if params[:end_date].blank?
params[:end_date] = (DateTime.now.end_of_week - 2).strftime('%m/%d/%Y')
end
scope = scope.between_dates(DateTime.strptime(params[:start_date], '%m/%d/%Y'),
DateTime.strptime(params[:end_date], '%m/%d/%Y').end_of_day)
when 'hour'
scope = scope.between_hour_on_day(DateTime.strptime(params[:hour], '%m/%d/%Y %H:
%M'))
when 'today'
scope = scope.between_day(DateTime.now)
end
@events = scope.all
end
def index(params)
scope = Event switch/case
case params[:timeframe]
when 'tomorrow'
scope = scope.between_day(DateTime.now + 1)
when 'this_week'
scope = scope.between_dates(DateTime.now, ( DateTime.now + 6 ).end_of_day)
when 'custom'
if params[:start_date].blank?
params[:start_date] = DateTime.now.beginning_of_week.strftime('%m/%d/%Y')
end
if params[:end_date].blank?
params[:end_date] = (DateTime.now.end_of_week - 2).strftime('%m/%d/%Y')
end
scope = scope.between_dates(DateTime.strptime(params[:start_date], '%m/%d/%Y'),
DateTime.strptime(params[:end_date], '%m/%d/%Y').end_of_day)
when 'hour'
scope = scope.between_hour_on_day(DateTime.strptime(params[:hour], '%m/%d/%Y %H:
%M'))
when 'today'
scope = scope.between_day(DateTime.now)
end
@events = scope.all
end
def index(params)
scope = Event
case params[:timeframe]
when 'tomorrow'
scope = scope.between_day(DateTime.now + 1)
when 'this_week'
string parsing
scope = scope.between_dates(DateTime.now, ( DateTime.now + 6 ).end_of_day)
when 'custom'
if params[:start_date].blank?
params[:start_date] = DateTime.now.beginning_of_week.strftime('%m/%d/%Y')
end
if params[:end_date].blank?
params[:end_date] = (DateTime.now.end_of_week - 2).strftime('%m/%d/%Y')
end
scope = scope.between_dates(DateTime.strptime(params[:start_date], '%m/%d/%Y'),
DateTime.strptime(params[:end_date], '%m/%d/%Y').end_of_day)
when 'hour'
scope = scope.between_hour_on_day(DateTime.strptime(params[:hour], '%m/%d/%Y %H:
%M'))
when 'today'
scope = scope.between_day(DateTime.now)
end
@events = scope.all
end
def index(params)
scope = Event
case params[:timeframe]
when 'tomorrow'
scope = scope.between_day(DateTime.now + 1)
when 'this_week'
scope = scope.between_dates(DateTime.now, ( DateTime.now + 6 ).end_of_day)
when 'custom'
if params[:start_date].blank?
params[:start_date] = DateTime.now.beginning_of_week.strftime('%m/%d/%Y')
end
if params[:end_date].blank?
params[:end_date] = (DateTime.now.end_of_week - 2).strftime('%m/%d/%Y')
end
scope = scope.between_dates(DateTime.strptime(params[:start_date], '%m/%d/%Y'),
DateTime.strptime(params[:end_date], '%m/%d/%Y').end_of_day)
when 'hour'
scope = scope.between_hour_on_day(DateTime.strptime(params[:hour], '%m/%d/%Y %H:
%M'))
when 'today'
scope = scope.between_day(DateTime.now)
end
vários named scopes
@events = scope.all
end
Antes Depois
.......
.
............
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(-)
O Plano
1. Centralizar a lógica de manipulação de datas
2. Unificar named scopes
3. Extrair lógica do Controller
4. Quebrar timeframe em vários pedaços
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
mudança nos testes:
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
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
describe "Acceptance tests" do
around(:each) do |example| Depois
......
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
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
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
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(-)
Antes
.......
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(-)
Antes Depois
........
.......
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(-)
O Plano
1. Centralizar a lógica de manipulação de datas
2. Unificar named scopes
3. Extrair lógica do Controller
4. Quebrar timeframe em vários pedaços
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
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
class TimeFrame
... Antes
.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
class TimeFrame
... Antes Depois
............
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
class TimeFrame
... Antes Depois
............
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
Mikado Method
Criar projeto para Criar teste de aceitação
Stranger Eons Ltda. para nova aplicação
Criar pacote para Stranger
Eons Ltda.
Mikado Method
Código de UI em projeto
separado
Criar projeto para Criar teste de aceitação
Stranger Eons Ltda. para nova aplicação
Criar pacote para Stranger
Eons Ltda.
Mikado Method
Mover código UI para
novo projeto
Criar projeto UI
Código de UI em projeto
separado
Criar projeto para Criar teste de aceitação
Stranger Eons Ltda. para nova aplicação
Criar pacote para Stranger
Eons Ltda.
Mikado Method
Quebrar dependência
circular entre Aplicação e UI
Mover código UI para
novo projeto
Criar projeto UI
Código de UI em projeto
separado
Criar projeto para Criar teste de aceitação
Stranger Eons Ltda. para nova aplicação
Criar pacote para Stranger
Eons Ltda.
Mikado Method
Extrair ApplicationInterface
Quebrar dependência
circular entre Aplicação e UI
Mover código UI para
novo projeto
Criar projeto UI
Código de UI em projeto
separado
Criar projeto para Criar teste de aceitação
Stranger Eons Ltda. para nova aplicação
Criar pacote para Stranger
Eons Ltda.
Mikado Method
Extrair ApplicationInterface
Injetar instância de
ApplicationInterface
Quebrar dependência
circular entre Aplicação e UI
Mover código UI para
novo projeto
Criar projeto UI
Código de UI em projeto
separado
Criar projeto para Criar teste de aceitação
Stranger Eons Ltda. para nova aplicação
Criar pacote para Stranger
Eons Ltda.
Mikado Method
✔
Extrair ApplicationInterface
Injetar instância de
✔
ApplicationInterface
Quebrar dependência
✔
circular entre Aplicação e UI
Mover código UI para
novo projeto
Criar projeto UI
Código de UI em projeto
separado
Criar projeto para Criar teste de aceitação
Stranger Eons Ltda. para nova aplicação
Criar pacote para Stranger
Eons Ltda.
Mikado Method
✔
Extrair ApplicationInterface
Injetar instância de
✔
ApplicationInterface
Quebrar dependência
✔
circular entre Aplicação e UI
Mover código UI para ✔
novo projeto
✔
✔
Criar projeto UI
Código de UI em projeto
separado
Criar projeto para Criar teste de aceitação
Stranger Eons Ltda. para nova aplicação
Criar pacote para Stranger
Eons Ltda.
Mikado Method
✔
Extrair ApplicationInterface
Injetar instância de
✔
ApplicationInterface
Quebrar dependência
✔
circular entre Aplicação e UI
Mover código UI para ✔
novo projeto
✔
✔
Criar projeto UI
Código de UI em projeto
separado
Criar projeto para ✔ Criar teste de aceitação ✔
Stranger Eons Ltda. para nova aplicação
✔
Criar pacote para Stranger
Eons Ltda.
A verdade
APP
Model
View
SVC
Controller
vai quebrar
tudo !!!
Helper
Arquitetura de
transição
APP
Model
View
Controller
SVC
Helper
Arquitetura de
transição
APP
Model
View
Controller
SVC
Helper
Arquitetura de
transição
APP
Model
View
Controller
SVC
Helper
Arquitetura de
transição
APP
Model
View
Controller
SVC
Helper
Arquitetura de
transição
APP
Model
View
Controller
SVC
Helper
Arquitetura de
transição
APP
Model
View
Controller
SVC
Helper
Arquitetura de
Transição
• Go:
• “Branch by Abstraction”
• Migrar de iBatis para Hibernate
• Migrar de Velocity/JsTemplate para JRuby
on Rails
Arquitetura de
Transição
• Go:
• “Branch by Abstraction”
• Migrar de iBatis para Hibernate
• Migrar de Velocity/JsTemplate para JRuby
on Rails
-
0 11/05/make-large
http://continuo usdelivery.com/2
-
l y-with-branch-by
scale-cha nges-incremental
abstraction/
Referências
Livros:
"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
Créditos (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/
Código:
https://github.com/dtsato/refactoring_experiment
https://github.com/dtsato/kata-refactoring-calendar
S etembro (18h):
Segun da-Feira, 12 de
/ join-revolution
http://thought works.com/events
Objetivo: extrair placeOrder para outro controller\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
Foco n&#xED;vel um pouco mais alto\n
\n
N&#xE3;o tem segredo para identificar objetivo\n
N&#xE3;o tem segredo para identificar objetivo\n
N&#xE3;o tem segredo para identificar objetivo\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
\n
L&#xED;der T&#xE9;cnico: compartilha a vis&#xE3;o\nWorkshops de arquitetura presente vs. futura\n
L&#xED;der T&#xE9;cnico: compartilha a vis&#xE3;o\nWorkshops de arquitetura presente vs. futura\n
L&#xED;der T&#xE9;cnico: compartilha a vis&#xE3;o\nWorkshops de arquitetura presente vs. futura\n
L&#xED;der T&#xE9;cnico: compartilha a vis&#xE3;o\nWorkshops de arquitetura presente vs. futura\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
1. Rollback ao decidir separar o projeto UI\n2. Depend&#xEA;ncia circular entre Aplica&#xE7;&#xE3;o e UI\n
1. Rollback ao decidir separar o projeto UI\n2. Depend&#xEA;ncia circular entre Aplica&#xE7;&#xE3;o e UI\n
1. Rollback ao decidir separar o projeto UI\n2. Depend&#xEA;ncia circular entre Aplica&#xE7;&#xE3;o e UI\n
1. Rollback ao decidir separar o projeto UI\n2. Depend&#xEA;ncia circular entre Aplica&#xE7;&#xE3;o e UI\n
1. Rollback ao decidir separar o projeto UI\n2. Depend&#xEA;ncia circular entre Aplica&#xE7;&#xE3;o e UI\n
1. Rollback ao decidir separar o projeto UI\n2. Depend&#xEA;ncia circular entre Aplica&#xE7;&#xE3;o e UI\n
1. Rollback ao decidir separar o projeto UI\n2. Depend&#xEA;ncia circular entre Aplica&#xE7;&#xE3;o e UI\n
1. Rollback ao decidir separar o projeto UI\n2. Depend&#xEA;ncia circular entre Aplica&#xE7;&#xE3;o e UI\n
1. Rollback ao decidir separar o projeto UI\n2. Depend&#xEA;ncia circular entre Aplica&#xE7;&#xE3;o e UI\n
1. Rollback ao decidir separar o projeto UI\n2. Depend&#xEA;ncia circular entre Aplica&#xE7;&#xE3;o e UI\n