Advertisement

Refatoração em Larga Escala

Software Consultant at ThoughtWorks
Sep. 10, 2011
Advertisement

More Related Content

Advertisement
Advertisement

Refatoração em Larga Escala

  1. Refatoração em Larga Escala Danilo Sato - @dtsato ThoughtWorks www.dtsato.com
  2. 8 países 0+ esc ritórios 2 On & off shore 1600+ pessoas $200m receita US
  3. 8 países 0+ esc ritórios 2 On & off shore 1600+ pessoas $200m receita US Estam os contratando! s r ent-opportunitie http://thou ghtworks.com/cur
  4. Por que mudamos código?
  5. Por que mudamos código? • Adicionar funcionalidade
  6. Por que mudamos código? • Adicionar funcionalidade • Arrumar bugs
  7. Por que mudamos código? • Adicionar funcionalidade • Arrumar bugs • Melhorar o design
  8. Por que mudamos código? • Adicionar funcionalidade • Arrumar bugs • Melhorar o design • Otimização
  9. Por que mudamos código? • Adicionar funcionalidade • Arrumar bugs • Melhorar o design • Otimização
  10. "Refatoração é uma técnica disciplinada para reestruturar código, alterando sua estrutura interna sem alterar seu comportamento externo"
  11. Refatoração Código
  12. Refatoração Código
  13. Refatoração Código Extr air Método
  14. Refatoração Código
  15. Refatoração Código
  16. Refatoração Código Eliminar Duplicação
  17. Extrair Método • Mover Método • Extrair Classe • Remover Mediador • Auto-Encapsular Atributo • Decompor Condicional • ...
  18. Testes! Código Teste
  19. Testes! Código Teste
  20. Testes! Código Teste Eliminar Duplicação
  21. Outros tipos... Código Teste
  22. Outros tipos... Código Código Teste
  23. Outros tipos... Código Código Teste Teste
  24. Outros tipos... Código Código Teste Teste Se paração de Respons abilidades
  25. Outros tipos... Código Teste
  26. Outros tipos... Teste Código
  27. Outros tipos... Teste Código
  28. Outros tipos... Teste Código Mudança no Design
  29. Ainda mais...
  30. “Big Picture”
  31. “Big Picture”
  32. “Big Picture”
  33. “Big Picture”
  34. “Big Picture”
  35. Estratégia Mecânica
  36. Coding Kata!
  37. Coding Kata!
  38. Estratégia Mecânica
  39. CheckoutControllerTest CheckoutController Order Cart
  40. m/dtsato/refact oring-experiment ht tp://github.co - mant.com/2011/0 9/01/refactoring http://paulham experiment CheckoutControllerTest CheckoutController Order Cart
  41. 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]]}}}")); } }
  42. @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"; } }
  43. @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"; } }
  44. @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; } }
  45. @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; } }
  46. @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; } }
  47. 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"; } }
  48. ⌘⌥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"; } }
  49. 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"; } }
  50. ⌘⌥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"; } }
  51. 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"; } }
  52. ⌘⌫ “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"; } }
  53. 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"; } }
  54. @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; } }
  55. @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; } }
  56. @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"; } }
  57. ⌘⌥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"; } }
  58. @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"; } }
  59. 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]]}}}")); } }
  60. 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]]}}}")); } }
  61. 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]]}}}")); } }
  62. @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"; } }
  63. ⌘⌫ “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"; } }
  64. @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"; } }
  65. Estratégia Mecânica
  66. Identificar Objetivo
  67. Identificar Objetivo
  68. Identificar Objetivo Design “ativo”
  69. Identificar Objetivo Design “ativo” • Design OO
  70. Identificar Objetivo Design “ativo” • Design OO • Direção do Projeto
  71. Identificar Objetivo Design “ativo” • Design OO • Direção do Projeto • Decisões Arquiteturais
  72. Identificar Objetivo Design “passivo”
  73. Identificar Objetivo Design “passivo” • Maus Cheiros
  74. Identificar Objetivo Design “passivo” • Maus Cheiros • Métricas
  75. Identificar Objetivo Design “passivo” • Maus Cheiros • Métricas • Visualizações
  76. Traçar Estratégia
  77. Traçar Estratégia ? ? ?
  78. Traçar Estratégia
  79. Traçar Estratégia • Isole o Impacto das Mudanças
  80. Traçar Estratégia • Isole o Impacto das Mudanças • Baby Steps
  81. Traçar Estratégia • Isole o Impacto das Mudanças • Baby Steps • Mantenha os testes passando
  82. Traçar Estratégia • Isole o Impacto das Mudanças • Baby Steps • Mantenha os testes passando • Ir para trás antes de ir para frente
  83. CalendarSearchController CalendarSearchController Test Event Event
  84. r dtsato/kata-ref actoring-calenda http ://github.com/ CalendarSearchController CalendarSearchController Test Event Event
  85. 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
  86. 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
  87. 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
  88. 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
  89. class Event < ActiveRecord::Base named_scope :between_dates, lambda { |start_date, end_date| {:conditions => ["at >= ? AND at <= ?", start_date, end_date ] } } named_scope :between_hour_on_day, lambda { |start_hour| end_date = start_hour + 1.hour-1.second { :conditions => {:at => start_hour..end_date} } } named_scope :between_day, lambda { |date| start_date = date.at_beginning_of_day end_date = start_date.end_of_day { :conditions => {:at => start_date..end_date} } } end
  90. jeitos diferentes de fazer a mesma class Event < ActiveRecord::Base coisa named_scope :between_dates, lambda { |start_date, end_date| {:conditions => ["at >= ? AND at <= ?", start_date, end_date ] } } named_scope :between_hour_on_day, lambda { |start_hour| end_date = start_hour + 1.hour-1.second { :conditions => {:at => start_hour..end_date} } } named_scope :between_day, lambda { |date| start_date = date.at_beginning_of_day end_date = start_date.end_of_day { :conditions => {:at => start_date..end_date} } } end
  91. class Event < ActiveRecord::Base named_scope :between_dates, lambda { |start_date, end_date| {:conditions => ["at >= ? AND at <= ?", start_date, end_date ] } } named_scope :between_hour_on_day, lambda { |start_hour| end_date = start_hour + 1.hour-1.second { :conditions => {:at => start_hour..end_date} } } mais lógica named_scope :between_day, lambda { |date| start_date = date.at_beginning_of_day end_date = start_date.end_of_day { :conditions => {:at => start_date..end_date} } } end
  92. Objetivos Ativo: • Isolar testes unitários do banco de dados Passivo: • Tratar dos maus cheiros
  93. 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
  94. def index(params) case params[:timeframe] when 'tomorrow' date = DateTime.now + 1 start_date = date.at_beginning_of_day end_date = start_date.end_of_day when 'this_week' start_date = DateTime.now end_date = (DateTime.now + 6 ).end_of_day when 'custom' start_date = params[:start_date].blank? ? DateTime.now.beginning_of_week : DateTime.strptime(params[:start_date], '%m/%d/%Y') end_date = params[:end_date].blank? ? (DateTime.now.end_of_week - 2) : DateTime.strptime(params[:end_date], '%m/%d/%Y').end_of_day when 'hour' start_date = DateTime.strptime(params[:hour], '%m/%d/%Y %H:%M') end_date = start_date + 1.hour-1.second when 'today' date = DateTime.now start_date = date.at_beginning_of_day end_date = start_date.end_of_day end @events = Event.between_dates(start_date, end_date) end
  95. class Event < ActiveRecord::Base named_scope :between_dates, lambda { |start_date, end_date| {:conditions => {:at => start_date..end_date}} } end
  96. 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(-)
  97. Antes . ............ ds 0 .49367 secon Finished in res 13 examp les, 0 failu 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(-)
  98. 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(-)
  99. 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
  100. 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
  101. 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
  102. 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
  103. 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
  104. 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
  105. 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(-)
  106. 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(-)
  107. 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(-)
  108. 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
  109. 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
  110. 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
  111. 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
  112. 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
  113. 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
  114. Estratégia Mecânica
  115. “Big Picture” • Mudanças Arquiteturais • Unificação de abordagens diferentes • Substituição de componentes/frameworks
  116. “Big Picture”
  117. “Big Picture” Escopo Risco Esforço Comunicação
  118. Mikado Method Criar pacote para Stranger Eons Ltda.
  119. Mikado Method Criar projeto para Stranger Eons Ltda. Criar pacote para Stranger Eons Ltda.
  120. Mikado Method Criar projeto para Criar teste de aceitação Stranger Eons Ltda. para nova aplicação Criar pacote para Stranger Eons Ltda.
  121. 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.
  122. 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.
  123. 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.
  124. 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.
  125. 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.
  126. 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.
  127. 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.
  128. 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.
  129. Uma história APP
  130. Uma história APP SVC
  131. Uma história APP SVC
  132. Uma história APP SVC
  133. A verdade APP Model View Controller
  134. A verdade APP Model View Controller Helper
  135. A verdade APP Model View SVC Controller Helper
  136. A verdade APP Model View SVC Controller vai quebrar tudo !!! Helper
  137. Arquitetura de transição APP Model View Controller SVC Helper
  138. Arquitetura de transição APP Model View Controller SVC Helper
  139. Arquitetura de transição APP Model View Controller SVC Helper
  140. Arquitetura de transição APP Model View Controller SVC Helper
  141. Arquitetura de transição APP Model View Controller SVC Helper
  142. Arquitetura de transição APP Model View Controller SVC Helper
  143. Arquitetura de Transição • Go: • “Branch by Abstraction” • Migrar de iBatis para Hibernate • Migrar de Velocity/JsTemplate para JRuby on Rails
  144. 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/
  145. 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
  146. S etembro (18h): Segun da-Feira, 12 de / join-revolution http://thought works.com/events
  147. Obrigado! Danilo Sato - @dtsato ThoughtWorks www.dtsato.com

Editor's Notes

  1. \n
  2. \n
  3. \n
  4. \n
  5. \n
  6. \n
  7. \n
  8. \n
  9. \n
  10. \n
  11. \n
  12. \n
  13. \n
  14. zoom out\n
  15. zoom out\n
  16. \n
  17. \n
  18. \n
  19. \n
  20. \n
  21. \n
  22. \n
  23. \n
  24. \n
  25. \n
  26. \n
  27. \n
  28. \n
  29. \n
  30. \n
  31. \n
  32. \n
  33. \n
  34. \n
  35. \n
  36. \n
  37. \n
  38. \n
  39. \n
  40. \n
  41. \n
  42. \n
  43. \n
  44. \n
  45. \n
  46. \n
  47. \n
  48. \n
  49. \n
  50. \n
  51. \n
  52. \n
  53. \n
  54. \n
  55. \n
  56. \n
  57. \n
  58. \n
  59. \n
  60. \n
  61. \n
  62. \n
  63. Mec&amp;#xE2;nica: IDE resolve, ferramentas, cat&amp;#xE1;logos...\n
  64. Exerc&amp;#xED;cios para praticar\nMelhoram uma habilidade espec&amp;#xED;fica: TDD, Algoritmos, Design, C&amp;#xF3;digo ruim? etc...\n
  65. \n
  66. Cobertura 100%\nApplica&amp;#xE7;&amp;#xE3;o Java/Spring\n
  67. \n
  68. Objetivo: extrair placeOrder para outro controller\n
  69. \n
  70. \n
  71. \n
  72. \n
  73. \n
  74. \n
  75. \n
  76. \n
  77. \n
  78. \n
  79. \n
  80. \n
  81. \n
  82. Foco n&amp;#xED;vel um pouco mais alto\n
  83. \n
  84. N&amp;#xE3;o tem segredo para identificar objetivo\n
  85. N&amp;#xE3;o tem segredo para identificar objetivo\n
  86. N&amp;#xE3;o tem segredo para identificar objetivo\n
  87. \n
  88. \n
  89. \n
  90. \n
  91. \n
  92. \n
  93. \n
  94. \n
  95. Michael Feathers: Effect Sketches\nCluster de m&amp;#xE9;todos/atributos\nMova c&amp;#xF3;digo repetido para um mesmo lugar\n
  96. Michael Feathers: Effect Sketches\nCluster de m&amp;#xE9;todos/atributos\nMova c&amp;#xF3;digo repetido para um mesmo lugar\n
  97. Michael Feathers: Effect Sketches\nCluster de m&amp;#xE9;todos/atributos\nMova c&amp;#xF3;digo repetido para um mesmo lugar\n
  98. Michael Feathers: Effect Sketches\nCluster de m&amp;#xE9;todos/atributos\nMova c&amp;#xF3;digo repetido para um mesmo lugar\n
  99. Aplica&amp;#xE7;&amp;#xE3;o Rails. Cobertura 100%\nForm de busca de eventos\nEm Ruby -&gt; n&amp;#xE3;o tem mesmas ferramentas avan&amp;#xE7;adas de refatora&amp;#xE7;&amp;#xE3;o\n
  100. \n
  101. \n
  102. \n
  103. \n
  104. \n
  105. \n
  106. \n
  107. \n
  108. \n
  109. \n
  110. * Escopo &amp;#xFA;nico: between_dates\n* Mais feio (indo para tr&amp;#xE1;s antes de ir para frente)\n* Isolando mudan&amp;#xE7;a (toda a l&amp;#xF3;gica de datas num &amp;#xFA;nico lugar)\n
  111. * Remo&amp;#xE7;&amp;#xE3;o de c&amp;#xF3;digo &gt; Adi&amp;#xE7;&amp;#xE3;o de c&amp;#xF3;digo\n* Testes mais r&amp;#xE1;pidos pois os testes dos 3 named scopes acessavam o BD\n
  112. * Remo&amp;#xE7;&amp;#xE3;o de c&amp;#xF3;digo &gt; Adi&amp;#xE7;&amp;#xE3;o de c&amp;#xF3;digo\n* Testes mais r&amp;#xE1;pidos pois os testes dos 3 named scopes acessavam o BD\n
  113. * Remo&amp;#xE7;&amp;#xE3;o de c&amp;#xF3;digo &gt; Adi&amp;#xE7;&amp;#xE3;o de c&amp;#xF3;digo\n* Testes mais r&amp;#xE1;pidos pois os testes dos 3 named scopes acessavam o BD\n
  114. \n
  115. Skinny Controller\nTestes do Controller usam mocks para testar colabora&amp;#xE7;&amp;#xE3;o\n
  116. Testes do controller moveram para teste de aceita&amp;#xE7;&amp;#xE3;o (cucumber?)\n\n
  117. * Timeframe s&amp;#xF3; cont&amp;#xEA;m start/end\n* L&amp;#xF3;gica ainda &amp;#xE9; feia\n
  118. * Teste n&amp;#xE3;o acessa mais o BD!\n* Adi&amp;#xE7;&amp;#xE3;o de muitos testes\n* Pouca mudan&amp;#xE7;a no c&amp;#xF3;digo em si\n
  119. * Teste n&amp;#xE3;o acessa mais o BD!\n* Adi&amp;#xE7;&amp;#xE3;o de muitos testes\n* Pouca mudan&amp;#xE7;a no c&amp;#xF3;digo em si\n
  120. * Teste n&amp;#xE3;o acessa mais o BD!\n* Adi&amp;#xE7;&amp;#xE3;o de muitos testes\n* Pouca mudan&amp;#xE7;a no c&amp;#xF3;digo em si\n
  121. \n
  122. * Separa&amp;#xE7;&amp;#xE3;o de subclasses em TimeFrame para remover switch/case\n* Testes mudam junto\n
  123. * Factory method ainda tem switch/case (discutir poss&amp;#xED;vel uso de metaprograma&amp;#xE7;&amp;#xE3;o)\n* Redu&amp;#xE7;&amp;#xE3;o total dos testes unit&amp;#xE1;rios: de 0.4936 para 0.19023 (61%)\n
  124. * Factory method ainda tem switch/case (discutir poss&amp;#xED;vel uso de metaprograma&amp;#xE7;&amp;#xE3;o)\n* Redu&amp;#xE7;&amp;#xE3;o total dos testes unit&amp;#xE1;rios: de 0.4936 para 0.19023 (61%)\n
  125. * Factory method ainda tem switch/case (discutir poss&amp;#xED;vel uso de metaprograma&amp;#xE7;&amp;#xE3;o)\n* Redu&amp;#xE7;&amp;#xE3;o total dos testes unit&amp;#xE1;rios: de 0.4936 para 0.19023 (61%)\n
  126. \n
  127. \n
  128. L&amp;#xED;der T&amp;#xE9;cnico: compartilha a vis&amp;#xE3;o\nWorkshops de arquitetura presente vs. futura\n
  129. L&amp;#xED;der T&amp;#xE9;cnico: compartilha a vis&amp;#xE3;o\nWorkshops de arquitetura presente vs. futura\n
  130. L&amp;#xED;der T&amp;#xE9;cnico: compartilha a vis&amp;#xE3;o\nWorkshops de arquitetura presente vs. futura\n
  131. L&amp;#xED;der T&amp;#xE9;cnico: compartilha a vis&amp;#xE3;o\nWorkshops de arquitetura presente vs. futura\n
  132. Escolha objetivo, comece de forma inocente, aprenda e desenho um grafo, rollback quando est&amp;#xE1; quebrado, trabalhe das folhas para a ra&amp;#xED;z\n
  133. 1. Rollback ao decidir separar o projeto UI\n2. Depend&amp;#xEA;ncia circular entre Aplica&amp;#xE7;&amp;#xE3;o e UI\n
  134. 1. Rollback ao decidir separar o projeto UI\n2. Depend&amp;#xEA;ncia circular entre Aplica&amp;#xE7;&amp;#xE3;o e UI\n
  135. 1. Rollback ao decidir separar o projeto UI\n2. Depend&amp;#xEA;ncia circular entre Aplica&amp;#xE7;&amp;#xE3;o e UI\n
  136. 1. Rollback ao decidir separar o projeto UI\n2. Depend&amp;#xEA;ncia circular entre Aplica&amp;#xE7;&amp;#xE3;o e UI\n
  137. 1. Rollback ao decidir separar o projeto UI\n2. Depend&amp;#xEA;ncia circular entre Aplica&amp;#xE7;&amp;#xE3;o e UI\n
  138. 1. Rollback ao decidir separar o projeto UI\n2. Depend&amp;#xEA;ncia circular entre Aplica&amp;#xE7;&amp;#xE3;o e UI\n
  139. 1. Rollback ao decidir separar o projeto UI\n2. Depend&amp;#xEA;ncia circular entre Aplica&amp;#xE7;&amp;#xE3;o e UI\n
  140. 1. Rollback ao decidir separar o projeto UI\n2. Depend&amp;#xEA;ncia circular entre Aplica&amp;#xE7;&amp;#xE3;o e UI\n
  141. 1. Rollback ao decidir separar o projeto UI\n2. Depend&amp;#xEA;ncia circular entre Aplica&amp;#xE7;&amp;#xE3;o e UI\n
  142. 1. Rollback ao decidir separar o projeto UI\n2. Depend&amp;#xEA;ncia circular entre Aplica&amp;#xE7;&amp;#xE3;o e UI\n
  143. \n
  144. \n
  145. \n
  146. Exemplo\n
  147. Exemplo\n
  148. Exemplo\n
  149. Exemplo\n
  150. Exemplo\n
  151. Exemplo\n
  152. Exemplo\n
  153. Exemplo\n
  154. Exemplo\n
  155. Exemplo\n
  156. Exemplo\n
  157. Exemplo\n
  158. Exemplo\n
  159. Exemplo\n
  160. \n
  161. \n
  162. \n
  163. \n
Advertisement