Refactoring is a systematic process of improving the quality of code without changing its behavior. Refactoring transforms a mess into clean code and simple design.
Refactoring | Optimization | Bugfixing | |
---|---|---|---|
Making code clean | true |
false | false |
Optimizing performance | false | true |
false |
Changing code functionality | false | false | true |
When the development team has:
Definition of Done
)Code smells are usually
not bugs
and don’t currently prevent the program from functioning. Instead, they indicate weaknesses in design that may be slowing the development down or increasing the risk of bugs or failures in the future.“A code smell is a surface indication that usually corresponds to a deeper problem in the system.”
— Martin Fowler
Refactoring techniques - they are just approaches, not the final implementation (like patterns).
Refactoring should be measurable, finite, costly-oriented, regular, corpuscular
Refactoring task is not “the task of the second chance”
Refactoring tasks (pull requests) should be quick and fast to merge (too expensive to keep them too long)
Bloater smells represent something that has grown so large that it cannot be effectively handled. These smells usually accumulate over time as the program evolves:
These smells mean that if you need to change something in one place in your code, you have to make many changes in other places too. It makes software development much more complicated and error-prone as a result:
The common thing for the Dispensable smells is that they all represent something unnecessary that should be removed from the source code to make it cleaner, more efficient and easier to understand:
All the smells in this group contribute to excessive coupling between classes or show what happens if coupling is replaced by excessive delegation:
A method has a large number of lines. (We’re immediately suspicious of any method with more than five lines.)
A class or module has a large number of instance variables, methods, or just lines of code.
An instance variable is set only at certain times, and it is nil (or unused) at other times.
It may not be worth the trouble of creating a new class if it doesn’t represent a useful abstraction.
Child class uses only some of the methods and properties inherited from the base class.
Making a simple change requires you to change several classes or modules.
You find yourself changing the same module for different reasons.
Whenever you create a subclass for a class, you find yourself needing to create a subclass for another class.
The code contains a comment.
Don’t delete comments that are pulling their own weight such as rdoc API documentation.
A variable, parameter, code fragment, method, module, or class is not used anywhere (perhaps other than in tests)
A class doesn’t affect much it’s parents, children, or clients. They seem to be doing all the associated work and there isn’t enough behavior left in the class to justify its continued existence.
Sometimes, a Lazy Class is present to communicate intent. You may have to balance communication versus simplicity in your design; and when communication wins, leave the Lazy Class in place.
A name doesn’t communicate its intent well enough. Examples of this can include:
You see calls of the form a.b.c.d.
One class uses the internal fields and methods of another class.
A class is doing too much simple delegation.
You need to refactor the code of online banking system. It’s really weird and needs some cleaning!
class Post attr_reader :title, :body, :date def initialize(title, body, date) @title = title @body = body @date = date end def condensed_format result = '' result << "Title: #{title}" result << "Date: #{date.strftime "%Y/%m/%d"}" result end def full_format result = '' result << "Title: #{title}" result << "Date: #{date.strftime "%Y/%m/%d"}" result << "--\n#{body}" result end end
Ruby
class Post attr_reader :title, :body, :date def initialize(title, body, date) @title = title @body = body @date = date end def condensed_format metadata end def full_format result = metadata result << "--\n#{body}" result end private def metadata result = '' result << "Title: #{title}" result << "Date: #{date.strftime "%Y/%m/%d"}" result end end
Ruby
class Rating def get_rating more_than_five_late_deliveries ? 2 : 1 end def more_than_five_late_deliveries @number_of_late_deliveries > 5 end end
Ruby
class Rating def get_rating @number_of_late_deliveries > 5 ? 2 : 1 end end
Ruby
class Student attr_accessor :first_term_assiduity, :first_term_test, :first_term_behavior attr_accessor :second_term_assiduity, :second_term_test, :second_term_behavior attr_accessor :third_term_assiduity, :third_term_test, :third_term_behavior def set_all_grades_to(grade) %w(first second third).each do |which_term| %w(assiduity test behavior).each do |criteria| send "#{which_term}_term_#{criteria}=".to_sym, grade end end end def first_term_grade (first_term_assiduity + first_term_test + first_term_behavior) / 3 end def second_term_grade (second_term_assiduity + second_term_test + second_term_behavior) / 3 end def third_term_grade (third_term_assiduity + third_term_test + third_term_behavior) / 3 end end
Ruby
class Student attr_reader :terms def initialize @terms = [ Term.new(:first), Term.new(:second), Term.new(:third) ] end def set_all_grades_to(grade) terms.each { |term| term.set_all_grades_to(grade) } end def first_term_grade term(:first).grade end def second_term_grade term(:second).grade end def third_term_grade term(:third).grade end def term(reference) terms.find { |term| term.name == reference } end end class Term attr_reader :name, :assiduity, :test, :behavior def initialize(name) @name = name @assiduity = 0 @test = 0 @behavior = 0 end def set_all_grades_to(grade) @assiduity = grade @test = grade @behavior = grade end def grade (assiduity + test + behavior) / 3 end end
Ruby
class Person def initialize @office_telephone = TelephoneNumber.new end def telephone_number @office_telephone.telephone_number end def office_telephone @office_telephone end end class TelephoneNumber attr_accessor :area_code, :number def telephone_number '(' + area_code + ') ' + number end end martin = Person.new martin.office_telephone.area_code = "781"
Ruby
class Person def initialize @office_telephone = TelephoneNumber.new end def area_code @office_telephone.area_code end def area_code=(arg) @office_telephone.area_code = arg end def number @office_telephone.number end def number=(arg) @office_telephone.number = arg end end class TelephoneNumber attr_accessor :area_code, :number def telephone_number '(' + area_code + ') ' + number end end martin = Person.new martin.area_code = "781"
Ruby
class Bid before_save :capture_account_number def capture_account_number buyer.preferred_account_number end end class Sale before_save :capture_account_number def capture_account_number buyer.preferred_account_number end end
Ruby
module AccountNumberCapture def self.included(klass) klass.class_eval do before_save :capture_account_number end end def capture_account_number buyer.preferred_account_number end end class Bid include AccountNumberCapture end class Sale include AccountNumberCapture end
Ruby
class Person attr_reader :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end end class MalePerson < Person def full_name first_name + " " + last_name end def gender "M" end end class FemalePerson < Person def full_name first_name + " " + last_name end def gender "F" end end
Ruby
class Person attr_reader :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end def full_name first_name + " " + last_name end end class MalePerson < Person def gender "M" end end class FemalePerson < Person def gender "F" end end
Ruby
class UserService USERNAME = "josemota" PASSWORD = "secret" class << self def lgn(username, password) username == USERNAME && password == PASSWORD end end end
Ruby
class UserService USERNAME = "josemota" PASSWORD = "secret" class << self def sign_in(username, password) username == USERNAME && password == PASSWORD end end end
Ruby
class AccountType def premium? # Do some logic end end class Account def overdraft_charge return @days_overdrawn * 1.75 unless @account_type.premium? result = 10 result += (@days_overdrawn - 7) * 0.85 if @days_overdrawn > 7 result end def bank_charge result = 4.5 result += overdraft_charge if @days_overdrawn > 0 result end end
Ruby
class AccountType def premium? # Do some logic end def overdraft_charge(days_overdrawn) return days_overdrawn * 1.75 unless premium? result = 10 result += (days_overdrawn - 7) * 0.85 if days_overdrawn > 7 result end end class Account def bank_charge result = 4.5 result += @account_type.overdraft_charge(@days_overdrawn) if @days_overdrawn > 0 result end end
Ruby
PHONE_CODES = { en_gb: "44", en_us: "541" } class Phone attr_reader :number def initialize(number) @number = number end def to_s number.to_s end end class Person attr_reader :locale, :phone def initialize(locale: :en_gb, phone: nil) @locale = locale @phone = Phone.new phone end def full_phone ["+", PHONE_CODES[locale], " ", phone].join end end
Ruby
PHONE_CODES = { en_gb: "44", en_us: "541" } class Phone attr_reader :number, :locale def initialize(number, locale) @number = number @locale = locale end def to_s PHONE_CODES[locale] + " " + number end end class Person attr_reader :phone def initialize(locale: :en_gb, phone: nil) @phone = Phone.new phone, locale end def full_phone ["+", phone].join end end
Ruby
class Ticket attr_reader :price def initialize @price = 2.0 end end class SeniorTicket < Ticket def price @price * 0.75 end end class JuniorTicket < Ticket def price @price * 0.5 end end
Ruby
class Ticket def initialize @price = 2.0 end def price @price * discount end def discount 1 end end class SeniorTicket < Ticket def discount 0.75 end end class JuniorTicket < Ticket def discount 0.5 end end
Ruby
class Student def first_term_grade 10 end def second_term_grade 11 end def third_term_grade 12 end end
Ruby
class Student GRADES = { first: 10, second: 11, third: 12 } def term_grade(index) GRADES[index] end end
Ruby
class Post attr_reader :id, :title, :body, :created_at def initialize(id, title, body, created_at) @id = id @title = title @body = body @created_at = created_at @published = false end def self.find(id) # database operation to retrieve data. # We will simulate it for now. post = POSTS.find { |post| post.id == id } end def publish @published = true POSTS.count { |post| !post.published? } end def unpublish @published = false end def published? @published end end # Sample data POSTS = [ Post.new( 1, "Introduce Null Object Pattern", "Post body should be here", Time.new(2013,01,25) ), Post.new( 2, "Introduce Assertion", "Post body should be here", Time.new(2012,02,26) ), Post.new( 3, "Extract Method", "Post body should be here", Time.new(2014,01,27) ), Post.new( 4, "Replace Type Code with Polymorphism", "Post body should be here", Time.new(2015,10,12) ) ]
Ruby
class Post attr_reader :id, :title, :body, :created_at def initialize(id, title, body, created_at) @id = id @title = title @body = body @created_at = created_at @published = false end def self.find(id) # database operation to retrieve data. # We will simulate it for now. post = POSTS.find { |post| post.id == id } end def self.unpublished POSTS.count { |post| !post.published? } end def publish @published = true end def unpublish @published = false end def published? @published end end # Sample data POSTS = [ Post.new( 1, "Introduce Null Object Pattern", "Post body should be here", Time.new(2013,01,25) ), Post.new( 2, "Introduce Assertion", "Post body should be here", Time.new(2012,02,26) ), Post.new( 3, "Extract Method", "Post body should be here", Time.new(2014,01,27) ), Post.new( 4, "Replace Type Code with Polymorphism", "Post body should be here", Time.new(2015,10,12) ) ]
Ruby
class Client attr_reader :department, :clerk def initialize(department, clerk) @department = department @clerk = clerk end end class Manager attr_accessor :department end class Clerk attr_reader :department def initialize(department) @department = department end def manager department.manager end end class Department attr_reader :manager def initialize(manager) @manager = manager manager.department = self end end
Ruby
class Client attr_reader :clerk def initialize(clerk) @clerk = clerk end end class Manager attr_accessor :department end class Clerk attr_reader :department def initialize(department) @department = department end def manager department.manager end end class Department attr_reader :manager def initialize(manager) @manager = manager manager.department = self end end
Ruby
class SquareRootCalculator class << self def calculate(number) if number > 0 Math.sqrt(number) end end end end
Ruby
module Assertions def assert(&block) raise ArgumentError unless block.call end end class SquareRootCalculator extend Assertions def self.calculate(number) assert { number > 0 } Math.sqrt(number) end end
Ruby
class Post attr_reader :id, :title, :body, :created_at def initialize(id, title, body, created_at) @id = id @title = title @body = body @created_at = created_at @published = false end def self.find_and_publish(id) # database operation to retrieve data. # We will simulate it for now. post = POSTS.find { |post| post.id == id } post.publish unless post.nil? end def publish @published = true end end POSTS = [ Post.new( 1, "Introduce Null Object Pattern", "Post body should be here", Time.new(2013,01,25) ) ]
Ruby
class Post attr_reader :id, :title, :body, :created_at def initialize(id, title, body, created_at) @id = id @title = title @body = body @created_at = created_at @published = false end def self.find_and_publish(id) # database operation to retrieve data. # We will simulate it for now. post = POSTS.find { |post| post.id == id } || NullPost.new post.publish end def publish @published = true end end class NullPost def publish # noop end end POSTS = [ Post.new( 1, "Introduce Null Object Pattern", "Post body should be here", Time.new(2013,01,25) ) ]
Ruby
class CartItem def initialize(product, quantity) @product = product @quantity = quantity end def price base_price = @quantity * @product.price level_of_discount = 1 level_of_discount = 2 if @quantity > 100 discounted_price(base_price, level_of_discount) end private def discounted_price(base_price, level_of_discount) return base_price * 0.9 if level_of_discount == 2 base_price * 0.95 end end
Ruby
class CartItem attr_reader :product, :quantity def initialize(product, quantity) @product = product @quantity = quantity end def price base_price * discount_coefficient end private def base_price quantity * product.price end def discount_coefficient discount_level == 2 ? 0.9 : 0.95 end def discount_level quantity > 100 ? 2 : 1 end end
Ruby
class Room def within_plan?(heating_plan) low = days_temperature_range.low high = days_temperature_range.high heating_plan.within_range?(low, high) end end class HeatingPlan def within_range?(low, high) (low >= @range.low) && (high <= @range.high) end end
Ruby
class Room def within_plan?(heating_plan) heating_plan.within_range?(days_temperature_range) end end class HeatingPlan def within_range?(room_range) (room_range.low >= @range.low) && (room_range.high <= @range.high) end end
Ruby
class Account def add_charge(base_price, tax_rate, imported) total = base_price + base_price * tax_rate total += base_price * 0.1 if imported @charges << total end def total_charge @charges.inject(0) { |total, charge| total + charge } end end account = Account.new account.add_charge(5, 0.1, true) account.add_charge(12, 0.125, false) total = account.total_charge
Ruby
class Account def add_charge(charge) @charges << charge end def total_charge @charges.inject(0) do |total_for_account, charge| total_for_account + charge.total end end end class Charge def initialize(base_price, tax_rate, imported) @base_price = base_price @tax_rate = tax_rate @imported = imported end def total result = @base_price + @base_price * @tax_rate result += @base_price * 0.1 if @imported result end end account = Account.new account.add_charge(Charge.new(9.0, 0.1, true)) account.add_charge(Charge.new(12.0, 0.125, true)) total = account.total_charge
Ruby
class Cart attr_reader :products def initialize(products) @products = products end def total products.inject(0) { |sum, product| sum + product[2] } end end Cart.new([ [ "Sweater" , "Pink" , 5.0 ], [ "Trousers" , "Blue" , 8.0 ], [ "Golf Club" , "Gray" , 12.0 ] ]).total
Ruby
class Cart attr_reader :products def initialize(products) @products = products.map { |product| Product.new *product } end def total products.inject(0) { |sum, product| sum + product.price } end end class Product attr_reader :name, :color, :price def initialize(name, color, price) @name = name @color = color @price = price end end Cart.new([ [ "Sweater" , "Pink" , 5.0 ], [ "Trousers" , "Blue" , 8.0 ], [ "Golf Club" , "Gray" , 12.0 ] ]).total
Ruby
class User attr_reader :email def initialize(email) raise ArgumentError, 'Email must contain "@"' unless email =~ /@/ @email = email.strip.downcase end end
Ruby
class User def initialize(email) @email_address = EmailAddress.new(email) end def email @email_address.email end end class EmailAddress attr_reader :email def initialize(email) raise ArgumentError, 'Email must contain "@"' unless email =~ /@/ @email = email.strip.downcase end end
Ruby
class Person def tax(income: nil, expenses: 0, type: :dependent_worker) return_value = 0 number_of_people_under_roof = 1 if type == :dependent_worker return_value += income * 0.02 else return_value += income * 0.04 end if number_of_people_under_roof > 2 return_value *= 1.10 end if income - expenses > income * 0.05 return_value += expenses * 0.05 end return_value -= expenses * 0.30 end end
Ruby
class Person def tax(income: nil, expenses: 0, type: :dependent_worker) TaxAlgorithm.new(income: income, expenses: expenses, type: type).compute end end class TaxAlgorithm def initialize(income: nil, expenses: 0, type: :dependent_worker) @income = income @expenses = expenses @type = type @return_value = 0 @number_of_people_under_roof = 1 end def compute process_type process_number_of_people process_income_expense_difference deduct_expenses end private def process_type if @type == :dependent_worker @return_value += @income * 0.02 else @return_value += @income * 0.04 end end def process_number_of_people @return_value *= 1.10 if @number_of_people_under_roof > 2 end def process_income_expense_difference @return_value += @expenses * 0.05 if @income - @expenses > @income * 0.05 end def deduct_expenses @return_value -= @expenses * 0.30 end end
Ruby
class MyQueue < Array attr_reader :queue def initialize @queue = self end def mypush(element) push(element) end end queue = MyQueue.new queue.mypush 42 queue.queue #=> [42] queue.push 23 #=> [42, 23]
Ruby
class MyQueue extend Forwardable attr_reader :queue def initialize @queue = [] end def_delegator :@queue, :push, :mypush end queue = MyQueue.new queue.mypush 42 queue.queue #=> [42] queue.push 23 #=> NoMethodError
Ruby
class PhonePlan def initialize(number_of_phones:, price:, type:) @number_of_phones = number_of_phones @price = price @type = type end def cost if type == 'individual' number_of_phones * price elsif type == 'business' subtotal = number_of_phones * price if number_of_phones < 50 subtotal * 0.75 else subtotal * 0.50 end end end private attr_reader :number_of_phones, :price, :type end
Ruby
class PhonePlan def initialize(number_of_phones:, price:) @number_of_phones = number_of_phones @price = price end private attr_reader :number_of_phones, :price end class IndividualPlan < PhonePlan def cost number_of_phones * price end end class BusinessPlan < PhonePlan def cost if number_of_phones < 50 subtotal * 0.75 else subtotal * 0.50 end end private def subtotal number_of_phones * price end end
Ruby
class Cuboid attr_reader :length, :width, :height def initialize(length, width, height) @length = length @width = width @height = height end def volume area = length * width area * height end end
Ruby
class Cuboid attr_reader :length, :width, :height def initialize(length, width, height) @length = length @width = width @height = height end def volume area * height end def area length * width end end
Ruby
class Employee def initialize(type: :regular) @type = type end def base_salary 500.0 end def salary base_salary + bonus end def self.build(type: :regular) new type: type end private def bonus case @type when :regular then 0 when :boss then 1500.0 when :manager then 800.0 end end end
Ruby
class Employee def initialize(type: :regular) @type = type end def base_salary 500.0 end def salary base_salary + bonus end def self.build(type: :employee) const_get(type.capitalize).new end def bonus 0 end end class Manager < Employee def bonus 800 end end class Boss < Employee def bonus 1500 end end
Ruby
class Employee def initialize(type: :regular) @type = type end def base_salary 500.0 end def salary base_salary + bonus end private def self.build(type: :regular) new type: type end def bonus case @type when :regular then 0 when :boss then 1500.0 when :manager then 800.0 end end end
Ruby
class Employee def initialize(type: :regular) @type = type end def base_salary 500.0 end def salary base_salary + bonus end def self.build(type: :regular) instance = new instance.extend const_get(type.capitalize) end end module Regular def bonus 0 end end module Manager def bonus 800 end end module Boss def bonus 1500 end end
Ruby
class User attr_reader :name, :type, :options def initialize(name, type, options = {}) @name = name @type = type @options = options end def public_key_matches? # Do some logic true end def oauth_authenticates? # Do some logic true end class << self def login(name, options = {}) current_user = USERS.find { |user| user.name == name } case current_user.type when :password current_user.options[:password] == options[:password] when :public_key current_user.public_key_matches? when :oauth current_user.oauth_authenticates? end end end end
Ruby
class User attr_reader :name, :type, :options def initialize(name, type, options = {}) @name = name @type = type @options = options @strategy = case @type when :password then Auth::Password.new(self) when :public_key then Auth::PublicKey.new(self) when :oauth then Auth::OAuth.new(self) end end def auth!(options) @strategy.auth?(options) end class << self def login(name, options = {}) user = USERS.find { |u| u.name == name } user.auth! options end end end module Auth class Password def initialize(user) @user = user end def auth?(options) @user.options[:password] == options[:password] end end class PublicKey def initialize(user) @user = user end def auth?(options) # Do some logic true end end class OAuth def initialize(user) @user = user end def auth?(options) # Do some logic true end end end
Ruby
def found_friends(people) friends = [] people.each do |person| if(person == "Don") friends << "Don" end if(person == "John") friends << "John" end if(person == "Kent") friends << "Kent" end end friends end
Ruby
def found_friends(people) people.select do |person| %w(Don John Kent).include?(person) end end
Ruby
if date < SUMMER_START && date > SUMMER_END charge = quantity * @winter_rate + @winter_service_charge else charge = quantity * @summer_rate end
Ruby
def final_charge return winter_charge(quantity) unless summer?(date) summer_charge(quantity) end def summer?(date) date >= SUMMER_START && date <= SUMMER_END end def winter_charge(quantity) quantity * @winter_rate + @winter_service_charge end def summer_charge(quantity) quantity * @summer_rate end
Ruby
managers = [] employees.each do |e| managers << e if e.manager? end offices = [] employees.each { |e| offices << e.office } manager_offices = [] employees.each do |e| manager_offices << e.office if e.manager? end
Ruby
managers = employees.select { |e| e.manager? } offices = employees.collect { |e| e.office } manager_offices = employees.select { |e| e.manager? }.collect { |e| e.office }
Ruby
class SearchCriteria def initialize(title = nil, author_id = nil, publisher_id = nil) @title = title @author_id = author_id @publisher_id = publisher_id end end criteria = SearchCriteria.new("Metaprogramming Ruby", 5, 8)
Ruby
class SearchCriteria def initialize(title: nil, author_id: nil, publisher_id: nil) @title = title @author_id = author_id @publisher_id = publisher_id end end criteria = SearchCriteria.new(title: "Metaprogramming Ruby", author_id: 5)
Ruby
class Order attr_reader :customer def customer=(value) customer.friend_orders.subtract(self) unless customer.nil? @customer = value customer.friend_orders.add(self) unless customer.nil? end end class Customer def initialize @orders = Set.new end def add_order(order) order.customer = self end # should only be used by Order when modifying the association def friend_orders @orders end end
Ruby
class Order attr_accessor :customer end class Customer def add_order(order) order.customer = self end end
Ruby
class Person def initialize(department) @department = department end def manager @department.manager end end class Department attr_reader :manager def initialize(manager) @manager = manager end end financial_dep = Department.new('CFO') john = Person.new(financial_dep) manager = john.manager
Ruby
class Person attr_reader :department def initialize(department) @department = department end end class Department attr_reader :manager def initialize(manager) @manager = manager end end financial_dep = Department.new('CFO') john = Person.new(financial_dep) manager = john.department.manager
Ruby