4 simple rules to refactoring
TRANSCRIPT
4 SIMPLE RULES TO REFACTORINGSTEVEN YAP, FUTUREWORKZ
Steven [email protected]://github.com/stevenyap
COUNTRY = SINGAPORE STATE = SINGAPORE CITY = SINGAPORE
CAPITAL = SINGAPORE
• Host Saigon.rb Ruby Meetup• Co-Founder of Futureworkz• Ruby on Rails coder• Agile startup consultant
Awesome Ruby on Rails Developmenthttp://www.futureworkz.com
http://playbook.futureworkz.com/
WHAT IS REFACTORING?
IS REFACTORING IMPORTANT?
WHO LOVES REFACTORING?
WHO HATES REFACTORING?
4 SIMPLE RULES TO REFACTORING BY STEVEN YAP
I HATE REFACTORING BECAUSE...
▸ extra work to do
▸ what if i break something else?
▸ waste of time
▸ takes up too much time
▸ no time
▸ don't know what to refactor
▸ how to refactor?
▸ i can't see the code smell
▸ makes me feel stupid
▸ any other reasons?
HATE REFACTORING 😡😡😡😡
4 SIMPLE RULES TO REFACTORINGHTTP://PLAYBOOK.FUTUREWORKZ.COM/PROTOCOLS/CODE-REVIEW/INDEX.HTML
LOVE REFACTORING 😍😍😍😍
4 SIMPLE RULES TO REFACTORING BY STEVEN YAP
THE FOUR RULES
▸ Write Test Cases
▸ Don't Repeat Yourself (DRY)
▸ Naming Reveals Intention
▸ Single Responsibility Principle (SRP)
RULE #1: WRITE TEST CASES
4 SIMPLE RULES TO REFACTORING BY STEVEN YAP
RULE #1: WRITE TEST CASES
▸ No test cases = no refactoring
▸ Never try to refactor without a test case to cover you
▸ Other coders' test cases protect you too
▸ Stress-free in coding = more happiness
▸ Easy way to write test cases (self-promotion):http://blog.futureworkz.com/ruby-on-rails/easy-way-write-test-case/
▸ The more test cases you write, the faster you write the test cases
require'rails_helper'
describeProgramdocontext'validations'doit{is_expected.tovalidate_presence_of:name}it{is_expected.tovalidate_presence_of:category_id}it{is_expected.tovalidate_presence_of:team_id}it{is_expected.tovalidate_presence_of:estimated_start_date}it{is_expected.toenumerize(:status).in(:wip,:unsuccessful,:pending_approval,:approved,:rejected,:completed)}end
context'associations'doit{is_expected.tohave_one:job}it{is_expected.tohave_many:quotations}it{is_expected.tohave_many:pos_to_suppliers}it{is_expected.tohave_many:pos_to_suppliers_in_group}it{is_expected.tohave_many:approved_quotations}it{is_expected.tohave_many:cost_items}end
describe'.started'dolet!(:pending_program){create(:pending_program)}let!(:started_program){create(:approved_program)}
it'returnsanarrayofstartedprogram'doexpect(Program.started).toincludestarted_programexpect(Program.started).to_notincludepending_programendendend
HATE REFACTORING 😡😡😡😡
HATE REFACTORING 😍😡😡😡
RULE #2: DON'T REPEAT YOURSELF
classShoppingCarthas_many:products
deffinal_priceproducts.map(&:price).inject(&:+)+country_taxend
defcountry_taxproducts.map(&:price).inject(&:+)*0.1endend
classShoppingCarthas_many:products
deffinal_pricetotal_price+country_taxend
defcountry_taxtotal_price*0.1end
deftotal_priceproducts.map(&:price).inject(&:+)endend
classShoppingCarthas_many:products
deffinal_priceproducts.map(&:price).inject(&:+)+country_taxend
defcountry_taxproducts.map(&:price).inject(&:+)*0.1endend
classOrder<ActiveRecord::Basedefpending?status==:pendingend
defpaid?status==:paidend
defdelivered?status==:deliveredend
defcancelled?status==:cancelledend
defrefunded?status==:refundedendend
classOrder<ActiveRecord::BaseSTATUSES=[:pending,:paid,:delivered,:cancelled,:refunded]
STATUSES.eachdo|status_name|define_method"#{status}?"dostatus==status_nameendendend
classOrder<ActiveRecord::Basedefpending?status==:pendingend
defpaid?status==:paidend
defdelivered?status==:deliveredend
defcancelled?status==:cancelledend
defrefunded?status==:refundedendend
4 SIMPLE RULES TO REFACTORING BY STEVEN YAP
RULE #2: DON'T REPEAT YOURSELF (DRY)
▸ Easiest way to refactor
▸ Find duplicates in codes and extract them into a method/class
▸ JUST OPEN YOUR EYES AND READ YOUR CODE AGAIN
▸ Find duplicated patterns in codes and extract them
▸ Find duplicated codes across files is harder
HATE REFACTORING 😍😡😡😡
HATE REFACTORING 😍😍😡😡
RULE #3: NAMING REVEALS INTENTION
4 SIMPLE RULES TO REFACTORING BY STEVEN YAP
GOOD NAMING IN RUBY/RAILS
▸ Array.first, Array.second, Array.third, ..., Array.forty_two
▸ Date.tomorrow, Date.today
▸ 1.hour.ago, 3.weeks.from_now
▸ Array.each
4 SIMPLE RULES TO REFACTORING BY STEVEN YAP
GOOD NAMING?
▸ shopping_cart.total_price
▸ Product.price_less_than(20)
▸ Numerology.calculate_lucky_number_from(dob: user.dob)
▸ person.male?
▸ get_youtube_id(youtube_url)
4 SIMPLE RULES TO REFACTORING BY STEVEN YAP
RULE #3: NAMING REVEALS INTENTION
▸ Naming for variable, method, class, file, even values
▸ Reveal what you are doing or why you are doing, not how you are doingeg.(how) UserMailer.use_gmail_smtp_send_email(what) UserMailer.send_email(why) UserMailer.send_activation_email
▸ The next coder can understand/guess intuitively what your variable/method/class is doing
require'rails_helper'
describeProductsController,type::controllerdodescribe'#index'dolet!(:p1){create(:product,published:true)}let!(:p2){create(:product,published:false)}
it'returnproducts'doget:indexexpect(assigns(:products)).to_notincludep2endendend
require'rails_helper'
describeProductsController,type::controllerdodescribe'#index'dolet!(:published_product){create(:product,published:true)}let!(:unpublished_product){create(:product,published:false)}
it'doesnotreturnunpublishedproducts'doget:indexexpect(assigns(:products)).to_notincludeunpublished_productendendend
require'rails_helper'
describeProductsController,type::controllerdodescribe'#index'dolet!(:p1){create(:product,published:true)}let!(:p2){create(:product,published:false)}
it'returnproducts'doget:indexexpect(assigns(:products)).to_notincludep2endendend
classUser<ActiveRecord::Baseafter_create:verify
defverifyUserMailer.verify(self).deliverendend
classUser<ActiveRecord::Baseafter_create:verify
defverify#sendemailverificationUserMailer.verify(self).deliverendend
classUser<ActiveRecord::Baseafter_create:send_email_verification
defsend_email_verificationUserMailer.verify_email(self).deliverendend
classUser<ActiveRecord::Baseafter_create:verify
defverify#sendemailverificationUserMailer.verify(self).deliverendend
4 SIMPLE RULES TO REFACTORING BY STEVEN YAP
RULE #3: NAMING REVEALS INTENTION - THE DON'TS
▸ DON'T DO THIS:
▸ n = 100
▸ p1 = Product.new
▸ product1 = Product.new
▸ category = Product.new
▸ Write comments to explain your code*
4 SIMPLE RULES TO REFACTORING BY STEVEN YAP
RULE #3: NAMING REVEALS INTENTION - THE DOS
▸ Use conventional naming:created_at (datetime) vs created_on (date)products (array of product)published?
▸ Name what the variable/method/class is doing or why it needs to do this
▸ Search in thesaurus.com
▸ Ask a non-coder how to name something or describe what you want to do
▸ Convert comment into a method instead
▸ Ask yourself: How can I let myself understand this code 1 year later?
HATE REFACTORING 😍😍😡😡
HATE REFACTORING 😍😍😍😡
RULE #4: SINGLE RESPONSIBILITY PRINCIPLE
4 SIMPLE RULES TO REFACTORING BY STEVEN YAP
RULE #4: SINGLE RESPONSIBILITY PRINCIPLE (SRP)
▸ Robert C. Martin: "A class should have only one reason to change"
▸ A class or method should only do one thing
classMessagesController<ApplicationControllerdefcreaterecipient=User.find(message_params[:recipient])subject=block_contact_information(message_params[:subject])body=block_contact_information(message_params[:body])
message=Message.new(recipient,subject,body)ifmessage.saverenderjson:{success:"Yourmessagehasbeensentsuccessfully"}elserenderjson:{error:message.errors}endend
private
defblock_contact_information(content)phone_regex=/(?:\+?|\b)[0-9]{4,}/content.gsub!(phone_regex,'[BlockedContent]')
email_regex=/[a-z]+[a-z0-9_\-]*@[a-z0-9_\-]+\.[a-z]{2,}/icontent.gsub!(email_regex,'[BlockedContent]')
website_regex=/https?:\/\/[\S]+/content.gsub!(website_regex,'[BlockedContent]')end
defmessage_paramsparams.require(:message).permit(:receipient,:subject,:body)endend
defblock_contact_information(content)phone_regex=/(?:\+?|\b)[0-9]{4,}/content.gsub!(phone_regex,'[BlockedContent]')
email_regex=/[a-z]+[a-z0-9_\-]*@[a-z0-9_\-]+\.[a-z]{2,}/icontent.gsub!(email_regex,'[BlockedContent]')
website_regex=/https?:\/\/[\S]+/content.gsub!(website_regex,'[BlockedContent]')end
defblock_contact_information(content)block_phone_contact(content)block_email_contact(content)block_website_contact(content)end
defblock_phone_contact(content)phone_regex=/(?:\+?|\b)[0-9]{4,}/content.gsub!(phone_regex,'[BlockedContent]')end
defblock_email_contact(content)email_regex=/[a-z]+[a-z0-9_\-]*@[a-z0-9_\-]+\.[a-z]{2,}/icontent.gsub!(email_regex,'[BlockedContent]')end
defblock_website_contact(content)website_regex=/https?:\/\/[\S]+/content.gsub!(website_regex,'[BlockedContent]')end
defblock_contact_information(content)phone_regex=/(?:\+?|\b)[0-9]{4,}/content.gsub!(phone_regex,'[BlockedContent]')
email_regex=/[a-z]+[a-z0-9_\-]*@[a-z0-9_\-]+\.[a-z]{2,}/icontent.gsub!(email_regex,'[BlockedContent]')
website_regex=/https?:\/\/[\S]+/content.gsub!(website_regex,'[BlockedContent]')end
classBlockContactdefself.santize(content)[:phone,:email,:website].inject(content)do|content,contact_type|self.send("block_#{contact_type}",content)endend
defself.block_phone(content)phone_regex=/(?:\+?|\b)[0-9]{4,}/content.gsub(phone_regex,'[BlockedContent]')end
defself.block_email(content)email_regex=/[a-z]+[a-z0-9_\-]*@[a-z0-9_\-]+\.[a-z]{2,}/icontent.gsub(email_regex,'[BlockedContent]')end
defself.block_website(content)website_regex=/https?:\/\/[\S]+/content.gsub(website_regex,'[BlockedContent]')endend
#BlockContact.santize('[email protected]')#=>"Myphoneis[BlockedContent]andmyemailis[BlockedContent]"
4 SIMPLE RULES TO REFACTORING BY STEVEN YAP
RULE #4: SINGLE RESPONSIBILITY PRINCIPLE (SRP)
▸ Long methods are the easiest to find (>10 lines?)
▸ Know the responsibility of Model, View, Controller well
▸ Ask yourself if this object is doing the right thing
▸ Ask yourself if this method is doing only one thing
▸ SRP normally give rise to more advanced refactoring/patternsEg. observer, delegate, services, etc
HATE REFACTORING 😍😍😍😡
HATE REFACTORING 😍😍😍😍
LOVE REFACTORING 😍😍😍😍
4 SIMPLE RULES TO REFACTORING BY STEVEN YAP
USING THE FOUR RULES
▸ Rule #1 of rule #1:Always think about test cases first!
▸ Refactor using DRY + Naming + SRP iteratively
▸ Think through your code using the 4 rules to achieve a minimum good quality
▸ Don't stop at the 4 rules & learn/refactor more as you gain more experience(eg. SOLID principles, LoD, Anti-patterns)
4 SIMPLE RULES TO REFACTORING BY STEVEN YAP
FINAL TIPS ON REFACTORING & FEELING BETTER AS A CODER
▸ Don't stress yourself to refactor the code to be the bestThere are no BEST code in the world
▸ Don't be hard on yourself if you cannot detect a code smellLearn from it! You will become better over time!
▸ Take a moment to relish your refactored code! Be proud to show it off in the pull request for code review.
▸ Protect your reputation as a world-class coderHave pride as a coder!
CLEAN & CLEAR CODE EASY TO UNDERSTAND
CODE IS BEAUTIFUL CODE IS ART
LOVE REFACTORING 😍😍😍😍
4 SIMPLE RULES TO REFACTORING BY STEVEN YAP