Making tastier code through refactoring

462 views

Published on

All we have ever worked with an application with legacy code. Refactoring is a technique that allows us to restructure and redesign our application code without changing its behavior so that it is more readable and easier to maintain. This presentation discusses the advantages and disadvantages of refactoring as well as some of the main techniques that apply during a refactoring.

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Making tastier code through refactoring

  1. 1. Making tastier code throughRefactoring
  2. 2. Person.new( name: Gabriel Ortuño, job: ASPgems, web: arctarus.com, pet_project: rezets.com, github: arctarus, twitter: arctarus)
  3. 3. 1. Introduction 2. Sample3. Conclusions
  4. 4. Refactoring?
  5. 5. "Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure" Martin Fowler
  6. 6. Code Smells
  7. 7. Refactoring Toolbox
  8. 8. Why?
  9. 9. Green Field
  10. 10. Legacy Code
  11. 11. When?
  12. 12. 1. Introduction 2. Sample3. Conclusions
  13. 13. New TaskPrint nutritional report in HTML
  14. 14. Recipe Ingredientname 1:Ningredients amount foodnutritional_report 1:1 Food HIGH LOW REGULAR name nutritional_code
  15. 15. class Recipe ... def nutritional_report total_calories, nutritional_points = 0, 0 result = "Nutritional Report for #{name}n" self.ingredients.each do |ingredient| this_calories = 0 # add calories by ingredient case ingredient.food.nutritional_code when Food::HIGH this_calories += 5 this_calories += (ingredient.amount - 2) * 1.5 if ingredient.amount > 2 when Food::LOW this_calories += ingredient.amount * 3 when Food::REGULAR this_calories += 1.5 this_calories += (ingredient.amount - 3) * 1.5 if ingredient.amount > 3 end # add nutritional points nutritional_points += 1 # add extra nutritional points for high food if ingredient.food.nutritional_code == Food::HIGH && ingredient.amount > 1 nutritional_points += 1 end # show figures for this rental result += "t" + ingredient.food.name + "t" + this_calories.to_s + "n" total_calories += this_calories end # add footer lines result += "Total calories are #{total_calories}n" result += "You earned #{nutritional_points} nutritional points" result endend
  16. 16. Whyyyy?
  17. 17. 1º Build a solid set of tests
  18. 18. describe Recipe do let(:recipe) { Recipe.new("Lentils with chorizo") } let(:chorizo) { Food.new(chorizo, Food::HIGH) } let(:lentil) { Food.new(lentil, Food::LOW) } let(:potatoe) { Food.new(potatoe, Food::REGULAR) } it "has a name" do recipe.name.should == "Lentils with chorizo" end describe "calories" do it "without ingredients are 0" it "with one regular ingredient are 1.5" it "with one regular ingredient and amount > 3 are 3" it "with one high ingredient are 5" end ...end
  19. 19. $ rspec spec..............Finished in 0.00742 seconds14 examples, 0 failures
  20. 20. class Recipe ... def nutritional_report total_calories, nutritional_points = 0, 0 result = "Nutritional Report for #{name}n" self.ingredients.each do |ingredient| this_calories = 0 # add calories by ingredient case ingredient.food.nutritional_code when Food::HIGH this_calories += 5 this_calories += (ingredient.amount - 2) * 1.5 if ingredient.amount > 2 when Food::LOW this_calories += ingredient.amount * 3 when Food::REGULAR this_calories += 1.5 this_calories += (ingredient.amount - 3) * 1.5 if ingredient.amount > 3 end # add nutritional points nutritional_points += 1 # add extra nutritional points for high food if ingredient.food.nutritional_code == Food::HIGH && ingredient.amount > 1 nutritional_points += 1 end # show figures for this rental result += "t" + ingredient.food.name + "t" + this_calories.to_s + "n" total_calories += this_calories end # add footer lines result += "Total calories are #{total_calories}n" result += "You earned #{nutritional_points} nutritional points" result endend
  21. 21. Long Method
  22. 22. Comments
  23. 23. class Recipe ... def nutritional_report total_calories, nutritional_points = 0, 0 result = "Nutritional Report for #{name}n" self.ingredients.each do |ingredient| this_calories = 0 # add calories by ingredient case ingredient.food.nutritional_code when Food::HIGH this_calories += 5 this_calories += (ingredient.amount - 2) * 1.5 if ingredient.amount > 2 when Food::LOW this_calories += ingredient.amount * 3 when Food::REGULAR this_calories += 1.5 this_calories += (ingredient.amount - 3) * 1.5 if ingredient.amount > 3 end # add nutritional points nutritional_points += 1 # add extra nutritional points for high food if ingredient.food.nutritional_code == Food::HIGH && ingredient.amount > 1 nutritional_points += 1 end # show figures for this rental result += "t" + ingredient.food.name + "t" + this_calories.to_s + "n" total_calories += this_calories end # add footer lines result += "Total calories are #{total_calories}n" result += "You earned #{nutritional_points} nutritional points" result endend
  24. 24. ...# add calories by ingredientcase ingredient.food.nutritional_codewhen Food::HIGH this_calories += 5 this_calories += (ingredient.amount - 2) * 1.5 if ...when Food::LOW this_calories += ingredient.amount * 3when Food::REGULAR this_calories += 1.5 this_calories += (ingredient.amount - 3) * 1.5 if ...end...
  25. 25. Extract Method
  26. 26. class Recipe ... def calories_for(ingredient) case ingredient.food.nutritional_code when Food::HIGH this_calories += (ingredient.amount - 2) * 1.5 if ... this_calories += 5 when Food::LOW this_calories += ingredient.amount * 3 when Food::REGULAR this_calories += (ingredient.amount - 3) * 1.5 if ... this_calories += 1.5 end endend
  27. 27. def nutritional_report total_calories, nutritional_points = 0, 0 result = "Nutritional Report for #{name}n" self.ingredients.each do |ingredient| this_calories = calories_for(ingredient) # add nutritional points nutritional_points += 1 # add extra nutritional points for high food if ingredient.food.nutritional_code == Food::HIGH && ingredient.amount > 1 nutritional_points += 1 end # show figures for this rental result += "t" + ingredient.food.name + "t" + this_calories.to_s +"n" total_calories += this_calories end # add footer lines result += "Total calories are #{total_calories}n" result += "You earned #{nutritional_points} nutritional points" resultend
  28. 28. $ rspec spec.FFFFFFFFFFFFFFinished in 0.00742 seconds14 examples, 13 failures
  29. 29. class Recipe ... def calories_for(ingredient) case ingredient.food.nutritional_code when Food::HIGH this_calories += (ingredient.amount - 2) * 1.5 if ... this_calories += 5 when Food::LOW this_calories += ingredient.amount * 3 when Food::REGULAR this_calories += (ingredient.amount - 3) * 1.5 if ... this_calories += 1.5 end endend
  30. 30. class Recipe ... def calories_for(ingredient) this_calories = 0 case ingredient.food.nutritional_code when Food::HIGH this_calories += (ingredient.amount - 2) * 1.5 if ... this_calories += 5 when Food::LOW this_calories += ingredient.amount * 3 when Food::REGULAR this_calories += (ingredient.amount - 3) * 1.5 if ... this_calories += 1.5 end endend
  31. 31. $ rspec spec..............Finished in 0.00742 seconds14 examples, 0 failures
  32. 32. class Recipe ... def calories_for(ingredient) this_calories = 0 case ingredient.food.nutritional_code when Food::HIGH this_calories += (ingredient.amount - 2) * 1.5 if ... this_calories += 5 when Food::LOW this_calories += ingredient.amount * 3 when Food::REGULAR this_calories += (ingredient.amount - 3) * 1.5 if ... this_calories += 1.5 end endend
  33. 33. class Recipe ... def calories_for(ingredient) this_calories = 0 case ingredient.food.nutritional_code when Food::HIGH this_calories += (ingredient.amount - 2) * 1.5 if ... this_calories += 5 when Food::LOW this_calories += ingredient.amount * 3 when Food::REGULAR this_calories += (ingredient.amount - 3) * 1.5 if ... this_calories += 1.5 end endend
  34. 34. Feature Envy
  35. 35. Move Method
  36. 36. class Ingredient ... def calories this_calories = 0 case food.nutritional_code when Food::HIGH this_calories += 5 this_calories += (amount - 2) * 1.5 if amount > 2 when Food::LOW this_calories += amount * 3 when Food::REGULAR this_calories += 1.5 this_calories += (amount - 3) * 1.5 if amount > 3 end endend
  37. 37. class Recipe def nutritional_report total_calories, nutritional_points = 0, 0 result = "Nutritional Report for #{name}n" self.ingredients.each do |ingredient| # add nutritional points nutritional_points += 1 # add extra nutritional points for high food if ingredient.food.nutritional_code == Food::HIGH && ingredient.amount > 1 nutritional_points += 1 end # show figures for this rental result += "t" + ingredient.food.name + "t" result += ingredient.calories.to_s + "n" total_calories += ingredient.calories end # add footer lines result += "Total calories are #{total_calories}n" result += "You earned #{nutritional_points} nutritional points" result endend
  38. 38. describe Ingredient do let(:chorizo) { Food.new(chorizo,Food::HIGH) } let(:lentil) { Food.new(lentil, Food::LOW) } let(:potatoe) { Food.new(potatoe, Food::REGULAR) } describe calories do it "with one regular food are 1.5" it "with one regular food and amount > 3 are 3" it "with one high food are 5" it "with one high food and amount > 2 are 6.5" it "with one low food are 3" endend
  39. 39. $ rspec spec...................Finished in 0.00588 seconds19 examples, 0 failures
  40. 40. class Recipe def nutritional_report total_calories, nutritional_points = 0, 0 result = "Nutritional Report for #{name}n" self.ingredients.each do |ingredient| # add nutritional points nutritional_points += 1 # add extra nutritional points for high food if ingredient.food.nutritional_code == Food::HIGH && ingredient.amount > 1 nutritional_points += 1 end # show figures for this rental result += "t" + ingredient.food.name + "t" result += ingredient.calories.to_s + "n" total_calories += ingredient.calories end # add footer lines result += "Total calories are #{total_calories}n" result += "You earned #{nutritional_points} nutritional points" result endend
  41. 41. Remove Feature Envy withExtract Method
  42. 42. class Ingredient ... def nutritional_points if food.nutritional_code == Food::HIGH && amount > 1 2 else 1 end endend
  43. 43. class Recipe ... def nutritional_report total_calories, nutritional_points = 0, 0 result = "Nutritional Report for #{name}n" self.ingredients.each do |ingredient| nutritional_points += ingredient.nutritional_points # show figures for this rental result += "t" + ingredient.food.name + "t" result += ingredient.calories.to_s + "n" total_calories += ingredient.calories end # add footer lines result += "Total calories are #{total_calories}n" result += "You earned #{nutritional_points} nutritional point result endend
  44. 44. describe Ingredient do describe "nutritional points" do it "is 2 if food is high and amount > 1" it "is 1 if food is high and amount = 1" it "is 1 if food is not high and amount = 1" it "is 1 if food is not high and amount > 1" endend
  45. 45. $ rspec spec.......................Finished in 0.00588 seconds23 examples, 0 failures
  46. 46. class Recipe def nutritional_report total_calories, nutritional_points = 0, 0 result = "Nutritional Report for #{name}n" self.ingredients.each do |ingredient| nutritional_points += ingredient.nutritional_points # show figures for this rental result += "t" + ingredient.food.name + "t" result += ingredient.calories.to_s + "n" total_calories += ingredient.calories end # add footer lines result += "Total calories are #{total_calories}n" result += "You earned #{nutritional_points} nutritional point result endend
  47. 47. Replace Temp with Query
  48. 48. class Recipe ... def total_calories ingredients.sum(:calories) end def total_nutritional_points ingredients.sum(:nutritional_points) endend
  49. 49. class Recipe def nutritional_report result = "Nutritional Report for #{name}n" ingredients.each do |ingredient| # show figures for this rental result += "t" + ingredient.food.name + "t" result += ingredient.calories.to_s + "n" end # add footer lines result += "Total calories are #{total_calories}n" result += "You earned #{total_nutritional_points} nutritional result end ...end
  50. 50. $ rspec spec.......................Finished in 0.00621 seconds23 examples, 0 failures
  51. 51. class Recipe def nutritional_report result = "Nutritional Report for #{name}n" ingredients.each do |ingredient| # show figures for this rental result += "t" + ingredient.food.name + "t" result += ingredient.calories.to_s + "n" end # add footer lines result += "Total calories are #{total_calories}n" result += "You earned #{total_nutritional_points} nutritional result end ...end
  52. 52. HTML Reportclass Recipe def html_nutritional_report result = "<h1>Nutritional Report for #{name}</h1>" ingredients.each do |ingredient| # show figures for this rental result += "<p>#{ingredient.food.name} " result += "{ingredient.calories}</p>" end # add footer lines result += "<p>Total calories are #{total_calories}</p>" result += "<p>You earned #{total_nutritional_points} " result += "nutritional points</p>" result end ...end
  53. 53. More Refactoring? Replace Method with Method ObjetTemplate Method Pattern
  54. 54. class NutritionalReport def initialize(recipe) @recipe = recipe end def output head body foot end def head ... def body ... def line(ingredient) ... def foot ...end
  55. 55. class HTMLNutritionalReport < NutritionalReport def head "<h1>Nutritional Report for #{name}</h1>" end def line(ingredient) "<p>#{ingredient.food.name} #{ingredient.calories}</p>" end def foot result = "<p>Total calories are #{@recipe.total_calories}</p result += "<p>You earned #{@recipe.total_nutritional_points}nutritional points</p>" result endend
  56. 56. WIN!
  57. 57. class Ingredient ... def calories this_calories = 0 case food.nutritional_code when Food::HIGH this_calories += (amount - 2) * 1.5 if amount > 2 this_calories += 5 when Food::LOW this_calories += amount * 3 when Food::REGULAR this_calories += (amount - 3) * 1.5 if amount > 3 this_calories += 1.5 end end def nutritional_points (food.nutritional_code == Food::HIGH && amount > 1) ? 2 : 1 end ...end
  58. 58. I notice a weird smell... Could it be envy?
  59. 59. Get rid ofFeature Envy withMove Method
  60. 60. class Food def calories(amount) this_calories = 0 case nutritional_code when HIGH this_calories += (amount - 2) * 1.5 if amount > 2 this_calories += 5 when LOW this_calories += amount * 3 when REGULAR this_calories += (amount - 3) * 1.5 if amount > 3 this_calories += 1.5 end end def nutritional_points(amount) (nutritional_code == HIGH && amount > 1) ? 2 : 1 endend
  61. 61. class Ingredient def calories food.calories(amount) end def nutritional_points food.nutritional_points(amount) endend
  62. 62. describe Ingredient do let(:chorizo) { Food.new(chorizo, Food::HIGH) } let(:lentil) { Food.new(lentil, Food::LOW) } let(:potatoe) { Food.new(potatoe, Food::REGULAR) } describe calories do it "with one regular food are 1.5" it "with one regular food and amount > 3 are 3" it "with one high food are 5" it "with one high food and amount > 2 are 6.5" it "with one low food are 3" end describe "nutritional points" do it "is 2 if food is high and amount > 1" it "is 1 if food is high and amount = 1" it "is 1 if food is not high and amount = 1" it "is 1 if food is not high and amount > 1" endend
  63. 63. $ rspec spec/................................Finished in 0.00865 seconds32 examples, 0 failures
  64. 64. class Food def calories(amount) this_calories = 0 case nutritional_code when HIGH this_calories += (amount - 2) * 1.5 if amount > 2 this_calories += 5 when LOW this_calories += amount * 3 when REGULAR this_calories += (amount - 3) * 1.5 if amount > 3 this_calories += 1.5 end end def nutritional_points(amount) (nutritional_code == HIGH && amount > 1) ? 2 : 1 endend
  65. 65. Fix Switch StatementsReplace Type Code with State/Strategy
  66. 66. class Food ... def nutritional_code=(value) @nutritional_code = value @nutritional_type = case @nutritional_code when HIGH then HighNutritional.new when LOW then LowNutritional.new when REGULAR then RegularNutritional.new end end def calories(amount) @nutritional_type.calories(amount) end def nutritional_points(amount) @nutritional_type.points(amount) endend
  67. 67. module DefaultNutritionalPoints def points(amount) 1 endendclass RegularNutritional include DefaultNutritionalPoints def calories(amount) acum = 1.5 acum += (amount - 3) * 1.5 if amount > 3 acum endend
  68. 68. class LowNutritional include DefaultNutritionalPoints def calories(amount) amount * 3 endendclass HighNutritional def calories(amount) acum = 5 acum += (amount - 2) * 1.5 if amount > 2 acum end def points(amount) amount > 1 ? 2 : 1 endend
  69. 69. EPICWIN!
  70. 70. 1. Introduction 2. Sample3. Conclusions
  71. 71. No Silver Bullet
  72. 72. Improve Design
  73. 73. Helps find bugs
  74. 74. Program faster
  75. 75. Good programmers write code that humans can understand
  76. 76. Refactor to Win
  77. 77. ¡Thanks!
  78. 78. Questions?
  79. 79. References● Refactoring: Improving design of existing code - Martin Fowler● Refactoring to Patterns - Joshua Kerievsky● Clean Code - Robert C. Martin● Design Patterns in Ruby - Russ Olsen● Source Making http://sourcemaking.com/refactoring
  80. 80. Tools● Reek - Code Smell Detector for ruby https://github.com/troessner/reek● Rails Best Practices http://rails-bestpractices.com● Code Climate http://codeclimate.com● Ruby Refactoring Tool for Vim https://github.com/ecomba/vim-ruby-refactoring
  81. 81. Thanks!

×