making tastier code through refactoring
DESCRIPTION
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.TRANSCRIPT
Making tastier code through
Refactoring
Person.new( name: 'Gabriel Ortuño',
job: 'ASPgems',
web: 'arctarus.com',
pet_project: 'rezets.com',
github: 'arctarus',
twitter: 'arctarus'
)
1. Introduction
2. Sample
3. Conclusions
Refactoring?
"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
Code Smells
Refactoring Toolbox
Why?
Green Field
Legacy Code
When?
1. Introduction
2. Sample
3. Conclusions
New TaskPrint nutritional report in HTML
Recipe
nameingredients
nutritional_report
Ingredient
amountfood
Food
namenutritional_code
HIGHLOWREGULAR
1:N
1:1
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 end end
Whyyyy?
1º Build a solid set of tests
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
$ rspec spec
..............
Finished in 0.00742 seconds
14 examples, 0 failures
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 end end
Long Method
Comments
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 end end
...
# 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...
Extract Method
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
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
$ rspec spec
.FFFFFFFFFFFFF
Finished in 0.00742 seconds
14 examples, 13 failures
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
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
$ rspec spec
..............
Finished in 0.00742 seconds
14 examples, 0 failures
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
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
Feature Envy
Move Method
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
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
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
$ rspec spec
...................
Finished in 0.00588 seconds
19 examples, 0 failures
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
Remove
Feature Envy
with
Extract Method
class Ingredient ... def nutritional_points if food.nutritional_code == Food::HIGH && amount > 1 2 else 1 end endend
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 points" result endend
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
$ rspec spec
.......................
Finished in 0.00588 seconds
23 examples, 0 failures
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 points" result endend
Replace Temp with Query
class Recipe
...
def total_calories ingredients.sum(:calories) end
def total_nutritional_points ingredients.sum(:nutritional_points) endend
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 points" result end
...
end
$ rspec spec
.......................
Finished in 0.00621 seconds
23 examples, 0 failures
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 points" result end
...end
class 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
HTML Report
More Refactoring?
Replace Method with Method Objet
Template Method Pattern
class NutritionalReport
def initialize(recipe) @recipe = recipe end
def output head body foot end
def head ... def body ... def line(ingredient) ... def foot ... end
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
WIN!
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
I notice a weird smell...
Could it be envy?
Feature Envy
Move Method
Get rid of
with
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
class Ingredient def calories food.calories(amount) end
def nutritional_points food.nutritional_points(amount) endend
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
$ rspec spec/
................................
Finished in 0.00865 seconds
32 examples, 0 failures
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 end
end
Replace Type Code with State/Strategy
Switch StatementsFix
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
module DefaultNutritionalPoints
def points(amount) 1 end
end
class RegularNutritional
include DefaultNutritionalPoints
def calories(amount) acum = 1.5 acum += (amount - 3) * 1.5 if amount > 3 acum endend
class LowNutritional
include DefaultNutritionalPoints
def calories(amount) amount * 3 endend
class HighNutritional def calories(amount) acum = 5 acum += (amount - 2) * 1.5 if amount > 2 acum end
def points(amount) amount > 1 ? 2 : 1 endend
EPIC WIN!
1. Introduction
2. Sample
3. Conclusions
No Silver Bullet
Improve Design
Helps find bugs
Program faster
Good programmers writecode that humans can
understand
Refactor to Win
¡Thanks!
Questions?
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
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
Thanks!