Download - Making tastier code through refactoring

Transcript
Page 1: Making tastier code through refactoring

Making tastier code through

Refactoring

Page 2: Making tastier code through refactoring

Person.new( name: 'Gabriel Ortuño',

job: 'ASPgems',

web: 'arctarus.com',

pet_project: 'rezets.com',

github: 'arctarus',

twitter: 'arctarus'

)

Page 3: Making tastier code through refactoring

1. Introduction

2. Sample

3. Conclusions

Page 4: Making tastier code through refactoring

Refactoring?

Page 5: Making tastier code through 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

Page 6: Making tastier code through refactoring
Page 7: Making tastier code through refactoring

Code Smells

Page 8: Making tastier code through refactoring

Refactoring Toolbox

Page 9: Making tastier code through refactoring

Why?

Page 10: Making tastier code through refactoring

Green Field

Page 11: Making tastier code through refactoring

Legacy Code

Page 12: Making tastier code through refactoring

When?

Page 13: Making tastier code through refactoring
Page 14: Making tastier code through refactoring

1. Introduction

2. Sample

3. Conclusions

Page 15: Making tastier code through refactoring

New TaskPrint nutritional report in HTML

Page 16: Making tastier code through refactoring

Recipe

nameingredients

nutritional_report

Ingredient

amountfood

Food

namenutritional_code

HIGHLOWREGULAR

1:N

1:1

Page 17: Making tastier code through refactoring
Page 18: Making tastier code through refactoring

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

Page 19: Making tastier code through refactoring

Whyyyy?

Page 20: Making tastier code through refactoring

1º Build a solid set of tests

Page 21: Making tastier code through refactoring

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

Page 22: Making tastier code through refactoring

$ rspec spec

..............

Finished in 0.00742 seconds

14 examples, 0 failures

Page 23: Making tastier code through refactoring

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

Page 24: Making tastier code through refactoring

Long Method

Page 25: Making tastier code through refactoring

Comments

Page 26: Making tastier code through refactoring

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

Page 27: Making tastier code through refactoring

...

# 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...

Page 28: Making tastier code through refactoring

Extract Method

Page 29: Making tastier code through refactoring

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

Page 30: Making tastier code through refactoring

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

Page 31: Making tastier code through refactoring

$ rspec spec

.FFFFFFFFFFFFF

Finished in 0.00742 seconds

14 examples, 13 failures

Page 32: Making tastier code through refactoring
Page 33: Making tastier code through refactoring

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

Page 34: Making tastier code through refactoring

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

Page 35: Making tastier code through refactoring

$ rspec spec

..............

Finished in 0.00742 seconds

14 examples, 0 failures

Page 36: Making tastier code through refactoring

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

Page 37: Making tastier code through refactoring

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

Page 38: Making tastier code through refactoring

Feature Envy

Page 39: Making tastier code through refactoring

Move Method

Page 40: Making tastier code through refactoring

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

Page 41: Making tastier code through refactoring

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

Page 42: Making tastier code through refactoring

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

Page 43: Making tastier code through refactoring

$ rspec spec

...................

Finished in 0.00588 seconds

19 examples, 0 failures

Page 44: Making tastier code through refactoring

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

Page 45: Making tastier code through refactoring

Remove

Feature Envy

with

Extract Method

Page 46: Making tastier code through refactoring

class Ingredient ... def nutritional_points if food.nutritional_code == Food::HIGH && amount > 1 2 else 1 end endend

Page 47: Making tastier code through refactoring

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

Page 48: Making tastier code through refactoring

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

Page 49: Making tastier code through refactoring

$ rspec spec

.......................

Finished in 0.00588 seconds

23 examples, 0 failures

Page 50: Making tastier code through refactoring

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

Page 51: Making tastier code through refactoring

Replace Temp with Query

Page 52: Making tastier code through refactoring

class Recipe

...

def total_calories ingredients.sum(:calories) end

def total_nutritional_points ingredients.sum(:nutritional_points) endend

Page 53: Making tastier code through refactoring

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

Page 54: Making tastier code through refactoring

$ rspec spec

.......................

Finished in 0.00621 seconds

23 examples, 0 failures

Page 55: Making tastier code through refactoring

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

Page 56: Making tastier code through refactoring

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

Page 57: Making tastier code through refactoring

More Refactoring?

Replace Method with Method Objet

Template Method Pattern

Page 58: Making tastier code through refactoring

class NutritionalReport

def initialize(recipe) @recipe = recipe end

def output head body foot end

def head ... def body ... def line(ingredient) ... def foot ... end

Page 59: Making tastier code through refactoring

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

Page 60: Making tastier code through refactoring

WIN!

Page 61: Making tastier code through refactoring

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

Page 62: Making tastier code through refactoring

I notice a weird smell...

Could it be envy?

Page 63: Making tastier code through refactoring

Feature Envy

Move Method

Get rid of

with

Page 64: Making tastier code through refactoring

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

Page 65: Making tastier code through refactoring

class Ingredient def calories food.calories(amount) end

def nutritional_points food.nutritional_points(amount) endend

Page 66: Making tastier code through refactoring

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

Page 67: Making tastier code through refactoring

$ rspec spec/

................................

Finished in 0.00865 seconds

32 examples, 0 failures

Page 68: Making tastier code through refactoring

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

Page 69: Making tastier code through refactoring

Replace Type Code with State/Strategy

Switch StatementsFix

Page 70: Making tastier code through refactoring

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

Page 71: Making tastier code through refactoring

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

Page 72: Making tastier code through refactoring

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

Page 73: Making tastier code through refactoring

EPIC WIN!

Page 74: Making tastier code through refactoring

1. Introduction

2. Sample

3. Conclusions

Page 75: Making tastier code through refactoring

No Silver Bullet

Page 76: Making tastier code through refactoring

Improve Design

Page 77: Making tastier code through refactoring

Helps find bugs

Page 78: Making tastier code through refactoring

Program faster

Page 79: Making tastier code through refactoring

Good programmers writecode that humans can

understand

Page 80: Making tastier code through refactoring

Refactor to Win

Page 81: Making tastier code through refactoring

¡Thanks!

Page 82: Making tastier code through refactoring

Questions?

Page 83: Making tastier code through refactoring

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

Page 84: Making tastier code through 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

Page 85: Making tastier code through refactoring

Thanks!