refactoring in practice - ruby hoedown 2010

135
REFACTORING IN PRACTICE Thursday, September 9, 2010

Upload: alex-sharp

Post on 06-Dec-2014

5.687 views

Category:

Technology


0 download

DESCRIPTION

 

TRANSCRIPT

Page 1: Refactoring in Practice - Ruby Hoedown 2010

REFACTORING IN PRACTICE

Thursday, September 9, 2010

Page 2: Refactoring in Practice - Ruby Hoedown 2010

WHO IS TEH ME?

Alex SharpLead Developer at OptimisDev

{ :tweets_as => '@ajsharp' :blogs_at => 'alexjsharp.com' :ships_at => 'github.com/ajsharp' }

Thursday, September 9, 2010

Page 3: Refactoring in Practice - Ruby Hoedown 2010

_WHY THIS TALK?

Thursday, September 9, 2010

Page 4: Refactoring in Practice - Ruby Hoedown 2010

RAILS IS GROWING UP

Thursday, September 9, 2010

Page 5: Refactoring in Practice - Ruby Hoedown 2010

RAILS IS GROWING UPMATURING

Thursday, September 9, 2010

Page 6: Refactoring in Practice - Ruby Hoedown 2010

IT’S NOT ALL GREEN FIELDS AND “SOFT LAUNCHES”

Thursday, September 9, 2010

Page 7: Refactoring in Practice - Ruby Hoedown 2010

APPS MATURE TOO

Thursday, September 9, 2010

Page 8: Refactoring in Practice - Ruby Hoedown 2010

so what is this talk about?

Thursday, September 9, 2010

Page 9: Refactoring in Practice - Ruby Hoedown 2010

large apps are a completely different beast than small apps

Thursday, September 9, 2010

Page 10: Refactoring in Practice - Ruby Hoedown 2010

domain complexity

Thursday, September 9, 2010

Page 11: Refactoring in Practice - Ruby Hoedown 2010

tight code coupling across domain concepts

Thursday, September 9, 2010

Page 12: Refactoring in Practice - Ruby Hoedown 2010

these apps become harder to maintain as size increases

Thursday, September 9, 2010

Page 13: Refactoring in Practice - Ruby Hoedown 2010

tight domain coupling exposes itself as a symptom of application size

Thursday, September 9, 2010

Page 14: Refactoring in Practice - Ruby Hoedown 2010

this talk is about working with large apps

Thursday, September 9, 2010

Page 15: Refactoring in Practice - Ruby Hoedown 2010

it’s about staying on the straight and narrow

Thursday, September 9, 2010

Page 16: Refactoring in Practice - Ruby Hoedown 2010

it’s about fixing bad code.

Thursday, September 9, 2010

Page 17: Refactoring in Practice - Ruby Hoedown 2010

it’s about responsibly fixing bad code.

Thursday, September 9, 2010

Page 18: Refactoring in Practice - Ruby Hoedown 2010

it’s about responsibly fixing bad and improving code.

Thursday, September 9, 2010

Page 19: Refactoring in Practice - Ruby Hoedown 2010

RULES

OF

REFACTORING

Thursday, September 9, 2010

Page 20: Refactoring in Practice - Ruby Hoedown 2010

RULE #1TESTS ARE CRUCIAL

Thursday, September 9, 2010

Page 21: Refactoring in Practice - Ruby Hoedown 2010

we’re out of our element without tests

Thursday, September 9, 2010

Page 22: Refactoring in Practice - Ruby Hoedown 2010

red, green, refactor doesn’t work without the red

Thursday, September 9, 2010

Page 23: Refactoring in Practice - Ruby Hoedown 2010

Thursday, September 9, 2010

Page 24: Refactoring in Practice - Ruby Hoedown 2010

RULE #2AVOID RABBIT HOLES

Thursday, September 9, 2010

Page 25: Refactoring in Practice - Ruby Hoedown 2010

RULE #3: TAKE SMALL WINS

Thursday, September 9, 2010

Page 26: Refactoring in Practice - Ruby Hoedown 2010

I.E. AVOID THE EPIC REFACTOR(IF POSSIBLE)

Thursday, September 9, 2010

Page 27: Refactoring in Practice - Ruby Hoedown 2010

what do we mean when we use the term “refactor”

Thursday, September 9, 2010

Page 28: Refactoring in Practice - Ruby Hoedown 2010

TRADITIONAL DEFINITION

To refactor code is to change it’s implementation while maintaining it’s behavior.

- via Martin Fowler, Kent Beck et. al

Thursday, September 9, 2010

Page 29: Refactoring in Practice - Ruby Hoedown 2010

in web apps this means that the behavior for the end user remains unchanged

Thursday, September 9, 2010

Page 30: Refactoring in Practice - Ruby Hoedown 2010

let’s look at some code

Thursday, September 9, 2010

Page 31: Refactoring in Practice - Ruby Hoedown 2010

DEPRECATE ACTION

Thursday, September 9, 2010

Page 32: Refactoring in Practice - Ruby Hoedown 2010

MAJOR VIOLATIONS

• Zero tests

• Performance bottlenecks

• main impetus for touching this code

• Phantom calling code

• this is why we need to deprecate

Thursday, September 9, 2010

Page 33: Refactoring in Practice - Ruby Hoedown 2010

Thursday, September 9, 2010

Page 34: Refactoring in Practice - Ruby Hoedown 2010

FIRST STEP: READ CODE

Thursday, September 9, 2010

Page 35: Refactoring in Practice - Ruby Hoedown 2010

def index @patients = Patient.search(params[:search], params[:page])

respond_to do |format| format.html { render :layout => 'application' } format.json { render :json => @patients.to_json } endend

Before: /patients

Thursday, September 9, 2010

Page 36: Refactoring in Practice - Ruby Hoedown 2010

def index @patients = Patient.search(params[:search], params[:page])

respond_to do |format| format.html { render :layout => 'application' } format.json { render :json => @patients.to_json } endend

Before: /patients

This method is crazy. Crazy slow, that is.

Thursday, September 9, 2010

Page 37: Refactoring in Practice - Ruby Hoedown 2010

def self.search(search, page) includes = {} unless search.nil? if search.index('@') conditions = ['(contacts.email like ?) and contacts.ispatient = ?', "%#{search}%", true] includes = [:contact] elsif search.match(/^[0-9]+\//) d = Date.parse( search, true ) if d.year > Date.today.year d = Date.civil( d.year - 100, d.month, d.day ) end conditions = ['(patients.birth_date = ? )', d] elsif search.match(/^[0-9]+/) conditions = ['(patients.ssn like ? or phones.number like ?) and contacts.ispatient = ?', "%#{search}%", "%#{search}%", true] includes = {:contact => [:phones]} else conditions = ['(patients.last_name like ? or patients.first_name like ?)', "%#{search}%", "%#{search}%"] end end paginate :per_page => 15, :page => page, :include => includes, :conditions => conditions, :order => 'patients.last_name, patients.first_name' end

Thursday, September 9, 2010

Page 38: Refactoring in Practice - Ruby Hoedown 2010

def self.search(search, page) includes = {} unless search.nil? if search.index('@') conditions = ['(contacts.email like ?) and contacts.ispatient = ?', "%#{search}%", true] includes = [:contact] elsif search.match(/^[0-9]+\//) d = Date.parse( search, true ) if d.year > Date.today.year d = Date.civil( d.year - 100, d.month, d.day ) end conditions = ['(patients.birth_date = ? )', d] elsif search.match(/^[0-9]+/) conditions = ['(patients.ssn like ? or phones.number like ?) and contacts.ispatient = ?', "%#{search}%", "%#{search}%", true] includes = {:contact => [:phones]} else conditions = ['(patients.last_name like ? or patients.first_name like ?)', "%#{search}%", "%#{search}%"] end end paginate :per_page => 15, :page => page, :include => includes, :conditions => conditions, :order => 'patients.last_name, patients.first_name' end

very hard to test

Thursday, September 9, 2010

Page 39: Refactoring in Practice - Ruby Hoedown 2010

def self.search(search, page) includes = {} unless search.nil? if search.index('@') conditions = ['(contacts.email like ?) and contacts.ispatient = ?', "%#{search}%", true] includes = [:contact] elsif search.match(/^[0-9]+\//) d = Date.parse( search, true ) if d.year > Date.today.year d = Date.civil( d.year - 100, d.month, d.day ) end conditions = ['(patients.birth_date = ? )', d] elsif search.match(/^[0-9]+/) conditions = ['(patients.ssn like ? or phones.number like ?) and contacts.ispatient = ?', "%#{search}%", "%#{search}%", true] includes = {:contact => [:phones]} else conditions = ['(patients.last_name like ? or patients.first_name like ?)', "%#{search}%", "%#{search}%"] end end paginate :per_page => 15, :page => page, :include => includes, :conditions => conditions, :order => 'patients.last_name, patients.first_name' end

could not figure out the bottleneck

Thursday, September 9, 2010

Page 40: Refactoring in Practice - Ruby Hoedown 2010

def self.search(search, page) includes = {} unless search.nil? if search.index('@') conditions = ['(contacts.email like ?) and contacts.ispatient = ?', "%#{search}%", true] includes = [:contact] elsif search.match(/^[0-9]+\//) d = Date.parse( search, true ) if d.year > Date.today.year d = Date.civil( d.year - 100, d.month, d.day ) end conditions = ['(patients.birth_date = ? )', d] elsif search.match(/^[0-9]+/) conditions = ['(patients.ssn like ? or phones.number like ?) and contacts.ispatient = ?', "%#{search}%", "%#{search}%", true] includes = {:contact => [:phones]} else conditions = ['(patients.last_name like ? or patients.first_name like ?)', "%#{search}%", "%#{search}%"] end end paginate :per_page => 15, :page => page, :include => includes, :conditions => conditions, :order => 'patients.last_name, patients.first_name' end

good candidate for extraction

Thursday, September 9, 2010

Page 41: Refactoring in Practice - Ruby Hoedown 2010

def self.search(search, page) includes = {} unless search.nil? # i.e. are we searching by email if search.index('@') conditions = ['(contacts.email like ?) and contacts.ispatient = ?', "%#{search}%", true] includes = [:contact] elsif search.match(/^[0-9]+\//) d = Date.parse( search, true ) if d.year > Date.today.year d = Date.civil( d.year - 100, d.month, d.day ) end conditions = ['(patients.birth_date = ? )', d] elsif search.match(/^[0-9]+/) conditions = ['(patients.ssn like ? or phones.number like ?) and contacts.ispatient = ?', "%#{search}%", "%#{search}%", true] includes = {:contact => [:phones]} else conditions = ['(patients.last_name like ? or patients.first_name like ?)', "%#{search}%", "%#{search}%"] end end

all_paginated(includes, conditions, page) end

def self.all_paginated(includes, conditions, page) includes = { :contact => [:primary_phone] } if includes.empty? paginate(:per_page => 15, :page => page, :include => includes, :conditions => conditions, :order => 'patients.last_name, patients.first_name') end

a slight improvement

Thursday, September 9, 2010

Page 42: Refactoring in Practice - Ruby Hoedown 2010

so why not just fix the whole thing?

Thursday, September 9, 2010

Page 43: Refactoring in Practice - Ruby Hoedown 2010

crazy phantom javascript code

Thursday, September 9, 2010

Page 44: Refactoring in Practice - Ruby Hoedown 2010

PatientsControllerV2#indexPatientsController#index

We want to alter some behavior here

index.erb

show.erb

phantom ajax call

Calling Code

Known calls

Unknown calls

Thursday, September 9, 2010

Page 45: Refactoring in Practice - Ruby Hoedown 2010

PatientsControllerV2#index

PatientsController#index

We want to alter some behavior here

index.erb

show.erb

phantom ajax call

Calling Code

Known calls

Unknown calls

Thursday, September 9, 2010

Page 46: Refactoring in Practice - Ruby Hoedown 2010

class PatientsController < ApplicationController before_filter :deprecate_action, :only => [:index]

def index @patients = Patient.search(params[:search], params[:page])

respond_to do |format| # ... end end private def deprecate_action notify_hoptoad(:error_message => "DEPRECATION WARNING!: /patients/index invoked") endend

After : /patients

Thursday, September 9, 2010

Page 47: Refactoring in Practice - Ruby Hoedown 2010

def deprecate_action notify_hoptoad(:error_message => "DEPRECATION WARNING!: /patients/index invoked")end

After : /patients

use hoptoad in production to notify us of hits to deprecated code

Thursday, September 9, 2010

Page 48: Refactoring in Practice - Ruby Hoedown 2010

def deprecate_action notify_hoptoad(:error_message => "DEPRECATION WARNING!: /patients/index invoked")end

After : /patients

use hoptoad in production to notify us of hits to deprecated code

CAREFUL! This can crush your app w/ lots of requests.Consider pushing into a BG job.

Thursday, September 9, 2010

Page 49: Refactoring in Practice - Ruby Hoedown 2010

After : /v2/patients

class V2::PatientsController < ApplicationController def index @patients = Patient.all_paginated([], [], params[:page]) endend

Thursday, September 9, 2010

Page 50: Refactoring in Practice - Ruby Hoedown 2010

WINS

• Massive performance improvement

• Separation of responsibilities

Thursday, September 9, 2010

Page 51: Refactoring in Practice - Ruby Hoedown 2010

COMMON PROBLEMS(I.E. ANTI-PATTERNS)

Thursday, September 9, 2010

Page 52: Refactoring in Practice - Ruby Hoedown 2010

LACK OF TECHNICAL LEADERSHIP

Thursday, September 9, 2010

Page 53: Refactoring in Practice - Ruby Hoedown 2010

LACK OF TECHNICAL LEADERSHIP

• Many common anti-pattens violated consistently

Thursday, September 9, 2010

Page 54: Refactoring in Practice - Ruby Hoedown 2010

LACK OF TECHNICAL LEADERSHIP

• Many common anti-pattens violated consistently

• DRY

Thursday, September 9, 2010

Page 55: Refactoring in Practice - Ruby Hoedown 2010

LACK OF TECHNICAL LEADERSHIP

• Many common anti-pattens violated consistently

• DRY

• single responsibility

Thursday, September 9, 2010

Page 56: Refactoring in Practice - Ruby Hoedown 2010

LACK OF TECHNICAL LEADERSHIP

• Many common anti-pattens violated consistently

• DRY

• single responsibility

• large procedural code blocks

Thursday, September 9, 2010

Page 57: Refactoring in Practice - Ruby Hoedown 2010

LACK OF PLATFORM KNOWLEDGE

• Often developers replicate things that Rails gives you for free

• This a frequent offense, but very low-hanging fruit to fix

Thursday, September 9, 2010

Page 58: Refactoring in Practice - Ruby Hoedown 2010

LACK OF TESTS

A few typical scenarios:

Thursday, September 9, 2010

Page 59: Refactoring in Practice - Ruby Hoedown 2010

LACK OF TESTS

• Aggressive deadlines: “there’s no time for tests”

A few typical scenarios:

Thursday, September 9, 2010

Page 60: Refactoring in Practice - Ruby Hoedown 2010

LACK OF TESTS

• Aggressive deadlines: “there’s no time for tests”

• Lack of testing experience leads to abandonment of tests

A few typical scenarios:

Thursday, September 9, 2010

Page 61: Refactoring in Practice - Ruby Hoedown 2010

LACK OF TESTS

• Aggressive deadlines: “there’s no time for tests”

• Lack of testing experience leads to abandonment of tests

• two files with tests in a 150+ model app

A few typical scenarios:

Thursday, September 9, 2010

Page 62: Refactoring in Practice - Ruby Hoedown 2010

LACK OF TESTS

• Aggressive deadlines: “there’s no time for tests”

• Lack of testing experience leads to abandonment of tests

• two files with tests in a 150+ model app

• “We’ll add tests later”

A few typical scenarios:

Thursday, September 9, 2010

Page 63: Refactoring in Practice - Ruby Hoedown 2010

POOR DOMAIN MODELING

Thursday, September 9, 2010

Page 64: Refactoring in Practice - Ruby Hoedown 2010

POOR DOMAIN MODELING

• UDD: UI-driven development

Thursday, September 9, 2010

Page 65: Refactoring in Practice - Ruby Hoedown 2010

POOR DOMAIN MODELING

• UDD: UI-driven development

• Unfamiliarity with domain modeling

Thursday, September 9, 2010

Page 66: Refactoring in Practice - Ruby Hoedown 2010

POOR DOMAIN MODELING

• UDD: UI-driven development

• Unfamiliarity with domain modeling

• probably b/c many apps don’t need it

Thursday, September 9, 2010

Page 67: Refactoring in Practice - Ruby Hoedown 2010

WRANGLING VIEW LOGIC WITH A PRESENTER

Thursday, September 9, 2010

Page 68: Refactoring in Practice - Ruby Hoedown 2010

COMMON PROBLEMS

• i-var bloat

• results in difficult to test controllers

• high barriers like this typically lead to developers just not testing

• results in brittle views

• instance variables are an implementation, not an interface

Thursday, September 9, 2010

Page 69: Refactoring in Practice - Ruby Hoedown 2010

THE BILLINGS SCREEN

Thursday, September 9, 2010

Page 70: Refactoring in Practice - Ruby Hoedown 2010

THE BILLINGS SCREEN

• Core functionality was simply broken, not working

Thursday, September 9, 2010

Page 71: Refactoring in Practice - Ruby Hoedown 2010

THE BILLINGS SCREEN

• Core functionality was simply broken, not working

• This area of the app is somewhat of a “shanty town”

Thursday, September 9, 2010

Page 72: Refactoring in Practice - Ruby Hoedown 2010

1. READ CODE

Thursday, September 9, 2010

Page 73: Refactoring in Practice - Ruby Hoedown 2010

class BillingsController < ApplicationController def index @visit_statuses = Visit::Statuses @visit_status = 'Complete' @visit_status = params[:visit_status] unless params[:visit_status].blank?

@billing_status = if @visit_status.upcase != 'COMPLETE' then '' elsif ( params[:billing_status] && params[:billing_status] != '–' ) then params[:billing_status] else Visit::Billing_pending end

@noted = 'true' @noted = 'false' if params[:noted] == 'false' @clinics = current_user.allclinics( @practice.id ) @visits = current_clinic.visits.billable_visits(@billing_status, @visit_status).order_by("visit_date") session[:billing_visits] = @visits.collect{ |v| v.id } endend

Thursday, September 9, 2010

Page 74: Refactoring in Practice - Ruby Hoedown 2010

class BillingsController < ApplicationController def index @visit_statuses = Visit::Statuses @visit_status = 'Complete' @visit_status = params[:visit_status] unless params[:visit_status].blank?

@billing_status = if @visit_status.upcase != 'COMPLETE' then '' elsif ( params[:billing_status] && params[:billing_status] != '–' ) then params[:billing_status] else Visit::Billing_pending end

@noted = 'true' @noted = 'false' if params[:noted] == 'false' @clinics = current_user.allclinics( @practice.id ) @visits = current_clinic.visits.billable_visits(@billing_status, @visit_status).order_by("visit_date") session[:billing_visits] = @visits.collect{ |v| v.id } endend

Holy @

ivar!Holy @ivar!

Hol

y @

ivar!

Holy @ivar!

Holy @ivar!

Holy @ivar!

Holy @

ivar!

Thursday, September 9, 2010

Page 75: Refactoring in Practice - Ruby Hoedown 2010

%th=collection_select(:search, :clinic_id, @clinics, :id, :name , {:prompt => "All"}, \ :class => 'filterable')%th=select_tag('search[visit_status_eq]', \ options_for_select( Visit::Statuses.collect(&:capitalize), @visit_status ), {:class => 'filterable'})%th=select_tag('search[billing_status_eq]', \ options_for_select([ "Pending", "Rejected", "Processed" ], @billing_status), {:class => 'filterable'})%th=collection_select(:search, :resource_id, @providers, :id, :full_name, { :prompt => 'All' }, \ :class => 'filterable')%tbody= render :partial => 'visit', :collection => @visits

Thursday, September 9, 2010

Page 76: Refactoring in Practice - Ruby Hoedown 2010

%th=collection_select(:search, :clinic_id, @clinics, :id, :name , {:prompt => "All"}, \ :class => 'filterable')%th=select_tag('search[visit_status_eq]', \ options_for_select( Visit::Statuses.collect(&:capitalize), @visit_status ), {:class => 'filterable'})%th=select_tag('search[billing_status_eq]', \ options_for_select([ "Pending", "Rejected", "Processed" ], @billing_status), {:class => 'filterable'})%th=collection_select(:search, :resource_id, @providers, :id, :full_name, { :prompt => 'All' }, \ :class => 'filterable')%tbody= render :partial => 'visit', :collection => @visits

Thursday, September 9, 2010

Page 77: Refactoring in Practice - Ruby Hoedown 2010

SECOND STEP:DOCUMENT SMELLS

Thursday, September 9, 2010

Page 78: Refactoring in Practice - Ruby Hoedown 2010

SMELLS

• Tricky presentation logic quickly becomes brittle

• Lot’s of hard-to-test, important code

• Needlessly storing constants in ivars

• Craziness going on with @billing_status. Seems important.

Thursday, September 9, 2010

Page 79: Refactoring in Practice - Ruby Hoedown 2010

class BillingsController < ApplicationController def index @visit_statuses = Visit::Statuses @visit_status = 'Complete' @visit_status = params[:visit_status] unless params[:visit_status].blank?

@billing_status = if @visit_status.upcase != 'COMPLETE' then '' elsif ( params[:billing_status] && params[:billing_status] != '-' ) then params[:billing_status] else Visit::Billing_pending end

@noted = 'true' @noted = 'false' if params[:noted] == 'false' @clinics = current_user.allclinics( @practice.id ) @visits = current_clinic.visits.billable_visits(@billing_status, @visit_status).order_by("visit_date") session[:billing_visits] = @visits.collect{ |v| v.id } endend

seems important...maybe for searching?

Thursday, September 9, 2010

Page 80: Refactoring in Practice - Ruby Hoedown 2010

class BillingsController < ApplicationController def index @visit_statuses = Visit::Statuses @visit_status = 'Complete' @visit_status = params[:visit_status] unless params[:visit_status].blank?

@billing_status = if @visit_status.upcase != 'COMPLETE' then '' elsif ( params[:billing_status] && params[:billing_status] != '-' ) then params[:billing_status] else Visit::Billing_pending end

@noted = 'true' @noted = 'false' if params[:noted] == 'false' @clinics = current_user.allclinics( @practice.id ) @visits = current_clinic.visits.billable_visits(@billing_status, @visit_status).order_by("visit_date") session[:billing_visits] = @visits.collect{ |v| v.id } endend

WTF!?!?

Thursday, September 9, 2010

Page 81: Refactoring in Practice - Ruby Hoedown 2010

A QUICK NOTE:“PERPETUAL WTF SYNDROME”

Thursday, September 9, 2010

Page 82: Refactoring in Practice - Ruby Hoedown 2010

TOO MANY WTF’S WILL DRIVE YOU NUTS

Thursday, September 9, 2010

Page 83: Refactoring in Practice - Ruby Hoedown 2010

AWFUL CODE IS DEMORALIZING

Thursday, September 9, 2010

Page 84: Refactoring in Practice - Ruby Hoedown 2010

YOU FORGET THAT GOOD CODE EXISTS

Thursday, September 9, 2010

Page 85: Refactoring in Practice - Ruby Hoedown 2010

THAT’S ONE REASON WHY THE NEXT STEP IS SO

IMPORTANT

Thursday, September 9, 2010

Page 86: Refactoring in Practice - Ruby Hoedown 2010

IT’S KEEPS THE DIALOGUE POSITIVE

Thursday, September 9, 2010

Page 87: Refactoring in Practice - Ruby Hoedown 2010

3. IDENTIFY ENGINEERING GOALS

Thursday, September 9, 2010

Page 88: Refactoring in Practice - Ruby Hoedown 2010

ENGINEERING GOALS

• Program to an interface, not an implementation

•Only one instance variable

• Use extract class to wrap up presentation logic

Thursday, September 9, 2010

Page 89: Refactoring in Practice - Ruby Hoedown 2010

describe BillingsPresenter do before do @practice = mock_model(Practice) @user = mock_model(User, :practice => @practice) @presenter = BillingsPresenter.new({}, @user, @practice) end

describe '#visits' do it 'should receive billable_visits' do @practice.should_receive(:billable_visits) @presenter.visits end end

describe '#visit_status' do it 'should default to the complete status' do @presenter.visit_status.should == "Complete" end end

describe '#billing_status' do it 'should default to default_billing_status' do @presenter.billing_status.should == BillingsPresenter.default_billing_status end

it 'should use the params[:billing_status] if it exists' do presenter = BillingsPresenter.new({:billing_status => 'use me'}, @user, @practice) presenter.billing_status.should == 'use me' end end

describe '#clinics' do it 'should receive user.allclinics' do @user.should_receive(:allclinics).with(@practice.id) @presenter.clinics end end

describe '#providers' do it 'should get the providers from the visit' do visit = mock_model Visit @presenter.stub!(:visits).and_return([visit]) visit.should_receive(:resource)

@presenter.providers end end

describe '.default_visit_status' do it 'is the capitalized version of COMPLETE' do BillingsPresenter.default_visit_status.should == 'Complete' end end

describe '.default_billing_status' do it 'is the capitalized version of COMPLETE' do BillingsPresenter.default_billing_status.should == 'Pending' end end

describe '#visit_statuses' do it 'is the capitalized version of the visit statuses' do @presenter.visit_statuses.should == [['All', '']] + Visit::Statuses.collect(&:capitalize) end end

describe '#billing_stauses' do it 'is the capitalized version of the billing statuses' do @presenter.billing_statuses.should == [['All', '']] + Visit::Billing_statuses.collect(&:capitalize) end end

end

Thursday, September 9, 2010

Page 90: Refactoring in Practice - Ruby Hoedown 2010

class BillingsPresenter attr_reader :params, :user, :practice def initialize(params, user, practice) @params = params @user = user @practice = practice end

def self.default_visit_status Visit::COMPLETE.capitalize end

def self.default_billing_status Visit::Billing_pending.capitalize end

def visits @visits ||= practice.billable_visits(params[:search]) end

def visit_status params[:visit_status] || self.class.default_visit_status end

def billing_status params[:billing_status] || self.class.default_billing_status end

def clinics @clinics ||= user.allclinics practice.id end

def providers @providers ||= visits.collect(&:resource).uniq end

def visit_statuses [['All', '']] + Visit::Statuses.collect(&:capitalize) end

def billing_statuses [['All', '']] + Visit::Billing_statuses.collect(&:capitalize) endend

Thursday, September 9, 2010

Page 91: Refactoring in Practice - Ruby Hoedown 2010

class BillingsController < ApplicationController def index @presenter = BillingsPresenter.new params, current_user, current_practice

session[:billing_visits] = @presenter.visits.collect{ |v| v.id } endend

Thursday, September 9, 2010

Page 92: Refactoring in Practice - Ruby Hoedown 2010

%th=collection_select(:search, :clinic_id, @presenter.clinics, :id, :name , {:prompt => "All"}, \ :class => 'filterable')%th=select_tag('search[visit_status_eq]', \ options_for_select( Visit::Statuses.collect(&:capitalize), @presenter.visit_status ), { \ :class => 'filterable'})%th=select_tag('search[billing_status_eq]', \ options_for_select([ "Pending", "Rejected", "Processed" ], @presenter.billing_status), {:class => 'filterable'})%th=collection_select(:search, :resource_id, @presenter.providers, :id, :full_name, { :prompt => 'All' }, \ :class => 'filterable')%tbody= render :partial => 'visit', :collection => @presenter.visits

Thursday, September 9, 2010

Page 93: Refactoring in Practice - Ruby Hoedown 2010

WINS

• Much cleaner, more testable controller

• Much less brittle view template

• The behavior of this action actually works

Thursday, September 9, 2010

Page 94: Refactoring in Practice - Ruby Hoedown 2010

Thursday, September 9, 2010

Page 95: Refactoring in Practice - Ruby Hoedown 2010

FAT MODEL / SKINNY CONTROLLER

Thursday, September 9, 2010

Page 96: Refactoring in Practice - Ruby Hoedown 2010

UPDATE ACTION CRAZINESS

• Update a collection of associated objects in the parent object’s update action

Thursday, September 9, 2010

Page 97: Refactoring in Practice - Ruby Hoedown 2010

Medical History

Existing Condition

join model*

*

Object Model

Thursday, September 9, 2010

Page 98: Refactoring in Practice - Ruby Hoedown 2010

STEP 1: READ CODE

Thursday, September 9, 2010

Page 99: Refactoring in Practice - Ruby Hoedown 2010

def update @medical_history = @patient.medical_histories.find(params[:id])

@medical_history.taken_date = Date.today if @medical_history.taken_date.nil?

success = true conditions = @medical_history.existing_conditions_medical_histories.find(:all)

respond_to do |format| if @medical_history.update_attributes(params[:medical_history]) && success format.html { flash[:notice] = 'Medical History was successfully updated.' redirect_to( patient_medical_histories_url(@patient) ) } format.xml { head :ok } format.js else format.html { render :action => "edit" } format.xml { render :xml => @medical_history.errors, :status => :unprocessable_entity } format.js { render :text => "Error Updating History", :status => :unprocessable_entity} end endend

params[:conditions].each_pair do |key,value| condition_id = key.to_s[10..-1].to_i condition = conditions.detect {|x| x.existing_condition_id == condition_id } if condition.nil? success = @medical_history.existing_conditions_medical_histories.create( :existing_condition_id => condition_id, :has_condition => value) && success elsif condition.has_condition != value success = condition.update_attribute(:has_condition, value) && success end end

Thursday, September 9, 2010

Page 100: Refactoring in Practice - Ruby Hoedown 2010

def update @medical_history = @patient.medical_histories.find(params[:id])

@medical_history.taken_date = Date.today if @medical_history.taken_date.nil?

success = true conditions = @medical_history.existing_conditions_medical_histories.find(:all)

respond_to do |format| if @medical_history.update_attributes(params[:medical_history]) && success format.html { flash[:notice] = 'Medical History was successfully updated.' redirect_to( patient_medical_histories_url(@patient) ) } format.xml { head :ok } format.js else format.html { render :action => "edit" } format.xml { render :xml => @medical_history.errors, :status => :unprocessable_entity } format.js { render :text => "Error Updating History", :status => :unprocessable_entity} end endend

params[:conditions].each_pair do |key,value| condition_id = key.to_s[10..-1].to_i condition = conditions.detect {|x| x.existing_condition_id == condition_id } if condition.nil? success = @medical_history.existing_conditions_medical_histories.create( :existing_condition_id => condition_id, :has_condition => value) && success elsif condition.has_condition != value success = condition.update_attribute(:has_condition, value) && success end end

smell-fest

Thursday, September 9, 2010

Page 101: Refactoring in Practice - Ruby Hoedown 2010

STEP 2: DOCUMENT SMELLS

Thursday, September 9, 2010

Page 102: Refactoring in Practice - Ruby Hoedown 2010

Lot’s of violations:

1. Fat model, skinny controller

2. Single responsibility

3. Not modular

4. Repetition of rails

5. Really hard to fit on a slide

Thursday, September 9, 2010

Page 103: Refactoring in Practice - Ruby Hoedown 2010

Let’s focus here

conditions = @medical_history.existing_conditions_medical_histories.find(:all)

params[:conditions].each_pair do |key,value| condition_id = key.to_s[10..-1].to_i condition = conditions.detect {|x| x.existing_condition_id == condition_id } if condition.nil? success = @medical_history.existing_conditions_medical_histories.create( :existing_condition_id => condition_id, :has_condition => value) && success elsif condition.has_condition != value success = condition.update_attribute(:has_condition, value) && success end end

Thursday, September 9, 2010

Page 104: Refactoring in Practice - Ruby Hoedown 2010

STEP 3: IDENTIFY ENGINEERING GOALS

Thursday, September 9, 2010

Page 105: Refactoring in Practice - Ruby Hoedown 2010

I want to refactor in the following ways:

1. Push this code into the model

2. Extract varying logic into separate methods

Thursday, September 9, 2010

Page 106: Refactoring in Practice - Ruby Hoedown 2010

We need to write some characterization tests and

get this action under test so we can figure out what it’s doing.

Thursday, September 9, 2010

Page 107: Refactoring in Practice - Ruby Hoedown 2010

This way we can rely on an automated testing

workflow for instant feedback

Thursday, September 9, 2010

Page 108: Refactoring in Practice - Ruby Hoedown 2010

describe MedicalHistoriesController, "PUT update" do context "successful" do before :each do @user = Factory(:practice_admin) @patient = Factory(:patient_with_medical_histories, :practice => @user.practice) @medical_history = @patient.medical_histories.first @condition1 = Factory(:existing_condition) @condition2 = Factory(:existing_condition)

stub_request_before_filters @user, :practice => true, :clinic => true

params = { :conditions => { "condition_#{@condition1.id}" => "true", "condition_#{@condition2.id}" => "true" }, :id => @medical_history.id, :patient_id => @patient.id }

put :update, params end it { should redirect_to patient_medical_histories_url } it { should_not render_template :edit }

it "should successfully save a collection of conditions" do @medical_history.existing_conditions.should include @condition1 @medical_history.existing_conditions.should include @condition2 end endend

Thursday, September 9, 2010

Page 109: Refactoring in Practice - Ruby Hoedown 2010

conditions = @medical_history.existing_conditions_medical_histories.find(:all)

params[:conditions].each_pair do |key,value| condition_id = key.to_s[10..-1].to_i condition = conditions.detect {|x| x.existing_condition_id == condition_id } if condition.nil? success = @medical_history.existing_conditions_medical_histories.create( :existing_condition_id => condition_id, :has_condition => value) && success elsif condition.has_condition != value success = condition.update_attribute(:has_condition, value) && success end end

we can eliminate this, b/c it’s an association of this model

Thursday, September 9, 2010

Page 110: Refactoring in Practice - Ruby Hoedown 2010

params[:conditions].each_pair do |key,value| condition_id = key.to_s[10..-1].to_i condition = existing_conditions_medical_histories.detect {|x| x.existing_condition_id == condition_id } if condition.nil? success = existing_conditions_medical_histories.create( :existing_condition_id => condition_id, :has_condition => value) && success elsif condition.has_condition != value success = condition.update_attribute(:has_condition, value) && success end end

Next, do extract method on this line

Thursday, September 9, 2010

Page 111: Refactoring in Practice - Ruby Hoedown 2010

params[:conditions].each_pair do |key,value| condition_id = get_id_from_string(key) condition = existing_conditions_medical_histories.detect {|x| x.existing_condition_id == condition_id } if condition.nil? success = existing_conditions_medical_histories.create( :existing_condition_id => condition_id, :has_condition => value) && success elsif condition.has_condition != value success = condition.update_attribute(:has_condition, value) && success end end

Cover this new method w/ tests

Thursday, September 9, 2010

Page 112: Refactoring in Practice - Ruby Hoedown 2010

params[:conditions].each_pair do |key,value| condition_id = get_id_from_string(key) condition = existing_conditions_medical_histories.detect {|x| x.existing_condition_id == condition_id } if condition.nil? success = existing_conditions_medical_histories.create( :existing_condition_id => condition_id, :has_condition => value) && success elsif condition.has_condition != value success = condition.update_attribute(:has_condition, value) && success end end

???

Thursday, September 9, 2010

Page 113: Refactoring in Practice - Ruby Hoedown 2010

params[:conditions].each_pair do |key,value| condition_id = get_id_from_string(key) condition = existing_conditions_medical_histories.detect {|x| x.existing_condition_id == condition_id } if condition.nil? success = existing_conditions_medical_histories.create( :existing_condition_id => condition_id, :has_condition => value) && success elsif condition.has_condition != value success = condition.update_attribute(:has_condition, value) && success end end

i.e. ecmh.find(:first, :conditions => { :existing_condition_id => condition_id })

Thursday, September 9, 2010

Page 114: Refactoring in Practice - Ruby Hoedown 2010

params[:conditions].each_pair do |key,value| condition_id = get_id_from_string(key) condition = existing_conditions_medical_histories.detect {|x| x.existing_condition_id == condition_id } if condition.nil? success = existing_conditions_medical_histories.create( :existing_condition_id => condition_id, :has_condition => value) && success elsif condition.has_condition != value success = condition.update_attribute(:has_condition, value) && success end end

do extract method here...

Thursday, September 9, 2010

Page 115: Refactoring in Practice - Ruby Hoedown 2010

do extract method here...

params[:conditions].each_pair do |key,value| condition_id = get_id_from_string(key) condition = find_existing_condition(condition_id) if condition.nil? success = existing_conditions_medical_histories.create( :existing_condition_id => condition_id, :has_condition => value) && success elsif condition.has_condition != value success = condition.update_attribute(:has_condition, value) && success end end

Thursday, September 9, 2010

Page 116: Refactoring in Practice - Ruby Hoedown 2010

params[:conditions].each_pair do |key,value| condition_id = get_id_from_string(key) condition = find_existing_condition(condition_id) if condition.nil? success = existing_conditions_medical_histories.create( :existing_condition_id => condition_id, :has_condition => value) && success elsif condition.has_condition != value success = condition.update_attribute(:has_condition, value) && success end end

???

Thursday, September 9, 2010

Page 117: Refactoring in Practice - Ruby Hoedown 2010

params[:conditions].each_pair do |key,value| condition_id = get_id_from_string(key) condition = find_existing_condition(condition_id) if condition.nil? success = existing_conditions_medical_histories.create( :existing_condition_id => condition_id, :has_condition => value) && success elsif condition.has_condition != value success = condition.update_attribute(:has_condition, value) && success end end

i.e. create or update

Thursday, September 9, 2010

Page 118: Refactoring in Practice - Ruby Hoedown 2010

describe MedicalHistory, "#update_conditions" do context "when passed a condition that is not an existing conditions" do before do @medical_history = Factory(:medical_history) @condition = Factory(:existing_condition) @medical_history.update_conditions({ "condition_#{@condition.id}" => "true" }) end it "should add the condition to the existing_condtions association" do @medical_history.existing_conditions.should include @condition end it "should set the :has_condition attribute to the value that was passed in" do @medical_history.existing_conditions_medical_histories.first.has_condition.should == true end end

context "when passed a condition that is an existing condition" do before do ecmh = Factory(:existing_conditions_medical_history, :has_condition => true) @medical_history = ecmh.medical_history @condition = ecmh.existing_condition @medical_history.update_conditions({ "condition_#{@condition.id}" => "false" }) end it "should update the existing_condition_medical_history record" do @medical_history.existing_conditions.should_not include @condition end it "should set the :has_condition attribute to the value that was passed in" do @medical_history.existing_conditions_medical_histories.first.has_condition.should == false end end end

Thursday, September 9, 2010

Page 119: Refactoring in Practice - Ruby Hoedown 2010

def update_conditions(conditions_param = {}) conditions_param.each_pair do |key, value| create_or_update_condition(get_id_from_string(key), value) end if conditions_param end private def create_or_update_condition(condition_id, value) condition = existing_conditions_medical_histories.find_by_existing_condition_id(condition_id) condition.nil? ? create_condition(condition_id, value) : update_condition(condition, value) end

def create_condition(condition_id, value) existing_conditions_medical_histories.create( :existing_condition_id => condition_id, :has_condition => value ) end

def update_condition(condition, value) condition.update_attribute(:has_condition, value) unless condition.has_condition == value end

Thursday, September 9, 2010

Page 120: Refactoring in Practice - Ruby Hoedown 2010

def update @medical_history = @patient.medical_histories.find(params[:id])

@medical_history.taken_date = Date.today if @medical_history.taken_date.nil?

success = true conditions = @medical_history.existing_conditions_medical_histories.find(:all)

respond_to do |format| if @medical_history.update_attributes(params[:medical_history]) && success format.html { flash[:notice] = 'Medical History was successfully updated.' redirect_to( patient_medical_histories_url(@patient) ) } format.xml { head :ok } format.js else format.html { render :action => "edit" } format.xml { render :xml => @medical_history.errors, :status => :unprocessable_entity } format.js { render :text => "Error Updating History", :status => :unprocessable_entity} end endend

params[:conditions].each_pair do |key,value| condition_id = key.to_s[10..-1].to_i condition = conditions.detect {|x| x.existing_condition_id == condition_id } if condition.nil? success = @medical_history.existing_conditions_medical_histories.create( :existing_condition_id => condition_id, :has_condition => value) && success elsif condition.has_condition != value success = condition.update_attribute(:has_condition, value) && success end end

We went from this mess in the controller

Thursday, September 9, 2010

Page 121: Refactoring in Practice - Ruby Hoedown 2010

def update @medical_history = @patient.medical_histories.find(params[:id]) @medical_history.taken_date = Date.today if @medical_history.taken_date.nil?

respond_to do |format| if( @medical_history.update_attributes(params[:medical_history]) && @medical_history.update_conditions(params[:conditions]) ) format.html { flash[:notice] = 'Medical History was successfully updated.' redirect_to( patient_medical_histories_url(@patient) ) } format.js else format.html { render :action => "edit" } format.js { render :text => "Error Updating History", :status => :unprocessable_entity } end end end

To this in the controller...

Thursday, September 9, 2010

Page 122: Refactoring in Practice - Ruby Hoedown 2010

and this in the model def update_conditions(conditions_param = {}) conditions_param.each_pair do |key, value| create_or_update_condition(get_id_from_string(key), value) end if conditions_param end private def create_or_update_condition(condition_id, value) condition = existing_conditions_medical_histories.find_by_existing_condition_id(condition_id) condition.nil? ? create_condition(condition_id, value) : update_condition(condition, value) end

def create_condition(condition_id, value) existing_conditions_medical_histories.create( :existing_condition_id => condition_id, :has_condition => value ) end

def update_condition(condition, value) condition.update_attribute(:has_condition, value) unless condition.has_condition == value end

Thursday, September 9, 2010

Page 123: Refactoring in Practice - Ruby Hoedown 2010

IT’S NOT PERFECT...

Thursday, September 9, 2010

Page 124: Refactoring in Practice - Ruby Hoedown 2010

def update @medical_history = @patient.medical_histories.find(params[:id]) @medical_history.taken_date = Date.today if @medical_history.taken_date.nil?

respond_to do |format| if( @medical_history.update_attributes(params[:medical_history]) && @medical_history.update_conditions(params[:conditions]) ) format.html { flash[:notice] = 'Medical History was successfully updated.' redirect_to( patient_medical_histories_url(@patient) ) } format.js else format.html { render :action => "edit" } format.js { render :text => "Error Updating History", :status => :unprocessable_entity } end end end

belongs in model

maybe a DB transaction? maybe nested params?

Thursday, September 9, 2010

Page 125: Refactoring in Practice - Ruby Hoedown 2010

BUT REFACTORING MUST BE DONE IN SMALL STEPS

Thursday, September 9, 2010

Page 126: Refactoring in Practice - Ruby Hoedown 2010

RECAP: WINS

• improved controller action API

• intention-revealing method names communicate what’s occurring at each stage of the update

• clean, testable public model API

Thursday, September 9, 2010

Page 127: Refactoring in Practice - Ruby Hoedown 2010

PROCESS RECAP

Thursday, September 9, 2010

Page 128: Refactoring in Practice - Ruby Hoedown 2010

1. READ CODE

Thursday, September 9, 2010

Page 129: Refactoring in Practice - Ruby Hoedown 2010

2. DOCUMENT SMELLS

Thursday, September 9, 2010

Page 130: Refactoring in Practice - Ruby Hoedown 2010

3. IDENTIFY ENGINEERING GOALS

Thursday, September 9, 2010

Page 131: Refactoring in Practice - Ruby Hoedown 2010

“We’re going to push this nastiness down into the model. That way we can get it tested, so we can more easily change

it in the future.”

Thursday, September 9, 2010

Page 132: Refactoring in Practice - Ruby Hoedown 2010

Rate my talk: http://bit.ly/ajsharp-hoedown-refactoring

Slides: http://slidesha.re/ajsharp-hoedown-refactoring-slides

Thursday, September 9, 2010

Page 133: Refactoring in Practice - Ruby Hoedown 2010

RESOURCES

• Talks/slides

• Rick Bradley’s railsconf slides: http://railsconf2010.rickbradley.com/

• Rick’s (beardless) hoedown 08 talk: http://tinyurl.com/flogtalk

• Books

• Working Effectively with Legacy Code by Michael Feathers

• Refactoring: Ruby Edition by Jay Fields, Martin Fowler, et. al

• Domain Driven Design by Eric Evans

Thursday, September 9, 2010

Page 134: Refactoring in Practice - Ruby Hoedown 2010

CONTACT

[email protected]

• @ajsharp on twitter

Thursday, September 9, 2010

Page 135: Refactoring in Practice - Ruby Hoedown 2010

THANKS!

Thursday, September 9, 2010