Making tastier code through refactoring

Preview:

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.

Citation preview

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!