SlideShare a Scribd company logo
REFACTORING IN
   PRACTICE
WHO IS TEH ME?

 Alex Sharp
Refactoring in Practice - Sunnyconf 2010
{ :tweets_as => '@ajsharp'
  :blogs_at => 'alexjsharp.com'
  :ships_at => 'github.com/ajsharp' }
_WHY THIS TALK?
APPS GROW UP
APPS GROW UP
   MATURE
IT’S NOT ALL GREEN FIELDS
  AND “SOFT LAUNCHES”
so what is this talk about?
large apps are a completely different
        beast than small apps
domain complexity
tight code coupling across
     domain concepts
these apps become harder to maintain
           as size increases
tight domain coupling exposes itself as a
       symptom of application size
this talk is about
working with large apps
it’s about staying on the
    straight and narrow
it’s about staying on the
         process
it’s about staying on the
           habit
it’s about fixing bad code.
it’s about responsibly fixing bad code.
it’s about responsibly fixing bad and
           improving code.
RULES

    OF

REFACTORING
RULE #1
TESTS ARE CRUCIAL
we’re out of our element without tests
red, green, refactor doesn’t work
         without the red
Refactoring in Practice - Sunnyconf 2010
RULE #2
AVOID RABBIT HOLES
RULE #3:
TAKE SMALL WINS
AVOID THE EPIC REFACTOR
        (IF POSSIBLE)
what do we mean when
we use the term “refactor”
TRADITIONAL DEFINITION

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

          - via Martin Fowler, Kent Beck et. al
in web apps this means that the behavior
   for the end user remains unchanged
COMMON PROBLEMS
 (I.E. ANTI-PATTERNS)
LACK OF TECHNICAL
                 LEADERSHIP
•   Many common anti-pattens violated consistently

       •   DRY

       •   single responsibility

       •   large procedural code blocks
LACK OF PLATFORM
                KNOWLEDGE
•   Often developers re-engineer features provided by the platform

    •   frequent offense, but very low-hanging fruit to fix
LACK OF TESTS

                 A few typical scenarios:

• 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”
POOR DOMAIN MODELING

•   UDD: UI-driven development

•   Unfamiliarity with domain modeling

       •   probably b/c many apps don’t need it
WRANGLING VIEW LOGIC
  WITH A PRESENTER
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
THE BILLINGS SCREEN

•   Core functionality was simply broken, not working

•   This area of the app is somewhat of a “shanty town”
Refactoring in Practice - Sunnyconf 2010
1. READ CODE
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 }
  end
end
var!                                ivar!
                          i                                            @
                       ly@                                 oly
            Ho
class BillingsController < ApplicationController
  def index                                              H
                        ivar!
    @visit_statuses = Visit::Statuses


                oly @                r!
    @visit_status = 'Complete'


            H
    @visit_status = params[:visit_status] unless params[:visit_status].blank?



                                 i va
    @billing_status =


                      !
      if @visit_status.upcase != 'COMPLETE' then ''

                    r           @
      elsif ( params[:billing_status] && params[:billing_status] != '-' ) then params[:billing_status]

                   a



                                                                            r!
      else Visit::Billing_pending


                 iv          ly
      end




                                                                          va
                @
    @noted = 'true'
                           o
                         H


                                                                        i
    @noted = 'false' if params[:noted] == 'false'




                                                                       @
              y
             l H
    @clinics = current_user.allclinics( @practice.id )

           o
    @visits = current_clinic.visits.billable_visits(@billing_status, @visit_status).order_by("visit_date")

                    oly @


                                                                    ly
         H
    session[:billing_visits] = @visits.collect{ |v| v.id }




                                                                                    o
                            ivar!
  end




                                                                                  H
end
%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
%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
SECOND STEP:
DOCUMENT SMELLS
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.
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'                 seems important...maybe for searching?
    @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 }
  end
end
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'     WTF!?!?
    @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 }
  end
end
A QUICK NOTE ABOUT
“PERPETUAL WTF SYNDROME”
TOO MANY WTF’S WILL DRIVE
       YOU NUTS
AWFUL CODE IS
DEMORALIZING
YOU FORGET THAT GOOD
     CODE EXISTS
THAT’S ONE REASON WHY
  THE NEXT STEP IS SO
       IMPORTANT
IT’S KEEPS THE DIALOGUE
         POSITIVE
3. IDENTIFY
ENGINEERING GOALS
ENGINEERING GOALS

•   Program to an interface, not an implementation

•   Only one instance variable

•   Use extract class to wrap up presentation logic
describe BillingsPresenter do
  before do
    @practice = mock_model(Practice)
    @user     = mock_model(User, :practice => @practice)              visit = mock_model Visit
    @presenter = BillingsPresenter.new({}, @user, @practice)          @presenter.stub!(:visits).and_return([visit])
  end                                                                 visit.should_receive(:resource)

  describe '#visits' do                                               @presenter.providers
    it 'should receive billable_visits' do                          end
      @practice.should_receive(:billable_visits)                  end
      @presenter.visits
    end                                                           describe '.default_visit_status' do
  end                                                               it 'is the capitalized version of COMPLETE' do
                                                                      BillingsPresenter.default_visit_status.should ==
  describe '#visit_status' do                                   'Complete'
    it 'should default to the complete status' do                   end
      @presenter.visit_status.should == "Complete"                end
    end
  end                                                             describe '.default_billing_status' do
                                                                    it 'is the capitalized version of COMPLETE' do
  describe '#billing_status' do                                       BillingsPresenter.default_billing_status.should ==
    it 'should default to default_billing_status' do            'Pending'
      @presenter.billing_status.should ==                           end
BillingsPresenter.default_billing_status                          end
    end
                                                                  describe '#visit_statuses' do
     it 'should use the params[:billing_status] if it exists'       it 'is the capitalized version of the visit statuses' do
do                                                                    @presenter.visit_statuses.should == [['All', '']] +
      presenter = BillingsPresenter.new({:billing_status =>     Visit::Statuses.collect(&:capitalize)
'use me'}, @user, @practice)                                        end
      presenter.billing_status.should == 'use me'                 end
    end
  end                                                             describe '#billing_stauses' do
                                                                    it 'is the capitalized version of the billing statuses' do
  describe '#clinics' do                                              @presenter.billing_statuses.should == [['All', '']] +
    it 'should receive user.allclinics' do                      Visit::Billing_statuses.collect(&:capitalize)
      @user.should_receive(:allclinics).with(@practice.id)          end
      @presenter.clinics                                          end
    end
  end                                                           end

  describe '#providers' do
    it 'should get the providers from the visit' do
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)
  end
end
class BillingsController < ApplicationController
  def index
    @presenter = BillingsPresenter.new params, current_user, current_practice

    session[:billing_visits] = @presenter.visits.collect{ |v| v.id }
  end
end
%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
WINS

•   Much cleaner, more testable controller

•   Much less brittle view template

•   The behavior of this action actually works
FAT MODEL /
SKINNY CONTROLLER
UPDATE ACTION CRAZINESS

•   Update a collection of associated objects in the parent object’s
    update action
Object Model
   Medical History

           *
     join model
           *

  Existing Condition
STEP 1: READ CODE
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)
  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
  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
  end
end
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)
  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                                                 smell-fest
      success = condition.update_attribute(:has_condition, value) && success
    end
  end
  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
  end
end
STEP 2: DOCUMENT SMELLS
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
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
STEP 3: IDENTIFY
ENGINEERING GOALS
I want to refactor in the following ways:

   1. Push this code into the model

   2. Extract varying logic into separate methods
We need to write some characterization tests and
 get this action under test so we can figure out
                  what it’s doing.
This way we can rely on an automated testing
       workflow for instant feedback
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
  end
end
we can eliminate this, b/c it’s an association of this
                      model

 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
Next, do extract method on this line
  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
Cover this new method w/ tests
  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
???
  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 })
  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...
  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
???
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
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
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
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
We went from this mess in the controller
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)
  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
  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
  end
end
To this in the controller...

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
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
IT’S NOT PERFECT...
belongs in model
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


     maybe a DB transaction? maybe nested params?
BUT REFACTORING MUST BE
  DONE IN SMALL STEPS
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
PROCESS RECAP
1. READ CODE
2. DOCUMENT SMELLS
3. IDENTIFY ENGINEERING
          GOALS
“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.”
Rate my talk: http://bit.ly/ajsharp-sunnyconf-refactoring

Slides: http://slidesha.re/ajsharp-sunnyconf-refactoring-slides
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
CONTACT

ajsharp@gmail.com
@ajsharp on twitter
THANKS!

More Related Content

What's hot (20)

PPT
ZFConf 2010: Zend Framework & MVC, Model Implementation (Part 2, Dependency I...
ZFConf Conference
 
PPT
Zend framework 04 - forms
Tricode (part of Dept)
 
PPTX
Tools for Making Machine Learning more Reactive
Jeff Smith
 
PDF
RichFaces: more concepts and features
Max Katz
 
PDF
Refactoring using Codeception
Jeroen van Dijk
 
PDF
Disregard Inputs, Acquire Zend_Form
Daniel Cousineau
 
KEY
Single Page Web Apps with Backbone.js and Rails
Prateek Dayal
 
PPTX
Hacking Your Way To Better Security - Dutch PHP Conference 2016
Colin O'Dell
 
PPTX
Angular Tutorial Freshers and Experienced
rajkamaltibacademy
 
DOCX
Tony Vitabile .Net Portfolio
vitabile
 
ODP
Ruby on rails
Mohit Jain
 
PPT
Framework
Nguyen Linh
 
PDF
Everything you always wanted to know about forms* *but were afraid to ask
Andrea Giuliano
 
PDF
Sylius and Api Platform The story of integration
Łukasz Chruściel
 
PPTX
Modern JavaScript Engine Performance
Catalin Dumitru
 
PPTX
Adding Dependency Injection to Legacy Applications
Sam Hennessy
 
PDF
Php Enums
Ayesh Karunaratne
 
PPTX
Injection de dépendances dans Symfony >= 3.3
Vladyslav Riabchenko
 
PDF
Leveraging Symfony2 Forms
Bernhard Schussek
 
PDF
Why is crud a bad idea - focus on real scenarios
Divante
 
ZFConf 2010: Zend Framework & MVC, Model Implementation (Part 2, Dependency I...
ZFConf Conference
 
Zend framework 04 - forms
Tricode (part of Dept)
 
Tools for Making Machine Learning more Reactive
Jeff Smith
 
RichFaces: more concepts and features
Max Katz
 
Refactoring using Codeception
Jeroen van Dijk
 
Disregard Inputs, Acquire Zend_Form
Daniel Cousineau
 
Single Page Web Apps with Backbone.js and Rails
Prateek Dayal
 
Hacking Your Way To Better Security - Dutch PHP Conference 2016
Colin O'Dell
 
Angular Tutorial Freshers and Experienced
rajkamaltibacademy
 
Tony Vitabile .Net Portfolio
vitabile
 
Ruby on rails
Mohit Jain
 
Framework
Nguyen Linh
 
Everything you always wanted to know about forms* *but were afraid to ask
Andrea Giuliano
 
Sylius and Api Platform The story of integration
Łukasz Chruściel
 
Modern JavaScript Engine Performance
Catalin Dumitru
 
Adding Dependency Injection to Legacy Applications
Sam Hennessy
 
Injection de dépendances dans Symfony >= 3.3
Vladyslav Riabchenko
 
Leveraging Symfony2 Forms
Bernhard Schussek
 
Why is crud a bad idea - focus on real scenarios
Divante
 

Viewers also liked (6)

PDF
Estácio: 1Q11 Conference Call Presentation
Estácio Participações
 
PPT
Blending the University: Beyond MOOCs
Gigi Johnson
 
PPTX
Digitale communicatie
Athalie Stegeman
 
PDF
JCI Estonia Treeninginstituut
Harald Lepisk
 
PDF
The Reprints Revolution will not be Televised
Ian Palmer
 
PPT
Teams as the unit of organization scale
cfry
 
Estácio: 1Q11 Conference Call Presentation
Estácio Participações
 
Blending the University: Beyond MOOCs
Gigi Johnson
 
Digitale communicatie
Athalie Stegeman
 
JCI Estonia Treeninginstituut
Harald Lepisk
 
The Reprints Revolution will not be Televised
Ian Palmer
 
Teams as the unit of organization scale
cfry
 
Ad

Similar to Refactoring in Practice - Sunnyconf 2010 (20)

PDF
Advanced RESTful Rails
Viget Labs
 
PDF
Advanced RESTful Rails
Ben Scofield
 
PDF
Simplify Your Rails Controllers With a Vengeance
brianauton
 
KEY
More to RoC weibo
shaokun
 
PDF
PHPSpec - the only Design Tool you need - 4Developers
Kacper Gunia
 
PPTX
Dependency injection - the right way
Thibaud Desodt
 
KEY
Zend framework service
Michelangelo van Dam
 
KEY
Zend framework service
Michelangelo van Dam
 
PDF
Introduction to Zend Framework web services
Michelangelo van Dam
 
PDF
Rails is not just Ruby
Marco Otte-Witte
 
PDF
Rails MVC by Sergiy Koshovyi
Pivorak MeetUp
 
PDF
前后端mvc经验 - webrebuild 2011 session
RANK LIU
 
PDF
Rails2 Pr
xibbar
 
PDF
OSDC 2009 Rails Turtorial
Yi-Ting Cheng
 
PDF
Ruby on Rails For Java Programmers
elliando dias
 
PDF
Dutch PHP Conference - PHPSpec 2 - The only Design Tool you need
Kacper Gunia
 
KEY
Ruby/Rails
rstankov
 
PDF
Desenvolvimento web com Ruby on Rails (extras)
Joao Lucas Santana
 
KEY
Crie seu sistema REST com JAX-RS e o futuro
Guilherme Silveira
 
PDF
RSpec User Stories
rahoulb
 
Advanced RESTful Rails
Viget Labs
 
Advanced RESTful Rails
Ben Scofield
 
Simplify Your Rails Controllers With a Vengeance
brianauton
 
More to RoC weibo
shaokun
 
PHPSpec - the only Design Tool you need - 4Developers
Kacper Gunia
 
Dependency injection - the right way
Thibaud Desodt
 
Zend framework service
Michelangelo van Dam
 
Zend framework service
Michelangelo van Dam
 
Introduction to Zend Framework web services
Michelangelo van Dam
 
Rails is not just Ruby
Marco Otte-Witte
 
Rails MVC by Sergiy Koshovyi
Pivorak MeetUp
 
前后端mvc经验 - webrebuild 2011 session
RANK LIU
 
Rails2 Pr
xibbar
 
OSDC 2009 Rails Turtorial
Yi-Ting Cheng
 
Ruby on Rails For Java Programmers
elliando dias
 
Dutch PHP Conference - PHPSpec 2 - The only Design Tool you need
Kacper Gunia
 
Ruby/Rails
rstankov
 
Desenvolvimento web com Ruby on Rails (extras)
Joao Lucas Santana
 
Crie seu sistema REST com JAX-RS e o futuro
Guilherme Silveira
 
RSpec User Stories
rahoulb
 
Ad

More from Alex Sharp (11)

PDF
Bldr: A Minimalist JSON Templating DSL
Alex Sharp
 
PDF
Bldr - Rubyconf 2011 Lightning Talk
Alex Sharp
 
PDF
Mysql to mongo
Alex Sharp
 
PDF
Refactoring in Practice - Ruby Hoedown 2010
Alex Sharp
 
PDF
Practical Ruby Projects with MongoDB - Ruby Kaigi 2010
Alex Sharp
 
PDF
Practical Ruby Projects with MongoDB - Ruby Midwest
Alex Sharp
 
KEY
Practical Ruby Projects with MongoDB - MongoSF
Alex Sharp
 
KEY
Practical Ruby Projects With Mongo Db
Alex Sharp
 
PDF
Intro To MongoDB
Alex Sharp
 
KEY
Getting Comfortable with BDD
Alex Sharp
 
KEY
Testing Has Many Purposes
Alex Sharp
 
Bldr: A Minimalist JSON Templating DSL
Alex Sharp
 
Bldr - Rubyconf 2011 Lightning Talk
Alex Sharp
 
Mysql to mongo
Alex Sharp
 
Refactoring in Practice - Ruby Hoedown 2010
Alex Sharp
 
Practical Ruby Projects with MongoDB - Ruby Kaigi 2010
Alex Sharp
 
Practical Ruby Projects with MongoDB - Ruby Midwest
Alex Sharp
 
Practical Ruby Projects with MongoDB - MongoSF
Alex Sharp
 
Practical Ruby Projects With Mongo Db
Alex Sharp
 
Intro To MongoDB
Alex Sharp
 
Getting Comfortable with BDD
Alex Sharp
 
Testing Has Many Purposes
Alex Sharp
 

Recently uploaded (20)

PPTX
AI in Daily Life: How Artificial Intelligence Helps Us Every Day
vanshrpatil7
 
PPTX
Applied-Statistics-Mastering-Data-Driven-Decisions.pptx
parmaryashparmaryash
 
PDF
How Open Source Changed My Career by abdelrahman ismail
a0m0rajab1
 
PDF
Google I/O Extended 2025 Baku - all ppts
HusseinMalikMammadli
 
PDF
Structs to JSON: How Go Powers REST APIs
Emily Achieng
 
PDF
Build with AI and GDG Cloud Bydgoszcz- ADK .pdf
jaroslawgajewski1
 
PDF
Peak of Data & AI Encore - Real-Time Insights & Scalable Editing with ArcGIS
Safe Software
 
PDF
Per Axbom: The spectacular lies of maps
Nexer Digital
 
PPTX
Dev Dives: Automate, test, and deploy in one place—with Unified Developer Exp...
AndreeaTom
 
PDF
Tea4chat - another LLM Project by Kerem Atam
a0m0rajab1
 
PPTX
AI and Robotics for Human Well-being.pptx
JAYMIN SUTHAR
 
PDF
Economic Impact of Data Centres to the Malaysian Economy
flintglobalapac
 
PPTX
AI Code Generation Risks (Ramkumar Dilli, CIO, Myridius)
Priyanka Aash
 
PDF
Make GenAI investments go further with the Dell AI Factory
Principled Technologies
 
PDF
Responsible AI and AI Ethics - By Sylvester Ebhonu
Sylvester Ebhonu
 
PPTX
cloud computing vai.pptx for the project
vaibhavdobariyal79
 
PDF
OFFOFFBOX™ – A New Era for African Film | Startup Presentation
ambaicciwalkerbrian
 
PDF
Researching The Best Chat SDK Providers in 2025
Ray Fields
 
PDF
The Future of Mobile Is Context-Aware—Are You Ready?
iProgrammer Solutions Private Limited
 
PPTX
The Future of AI & Machine Learning.pptx
pritsen4700
 
AI in Daily Life: How Artificial Intelligence Helps Us Every Day
vanshrpatil7
 
Applied-Statistics-Mastering-Data-Driven-Decisions.pptx
parmaryashparmaryash
 
How Open Source Changed My Career by abdelrahman ismail
a0m0rajab1
 
Google I/O Extended 2025 Baku - all ppts
HusseinMalikMammadli
 
Structs to JSON: How Go Powers REST APIs
Emily Achieng
 
Build with AI and GDG Cloud Bydgoszcz- ADK .pdf
jaroslawgajewski1
 
Peak of Data & AI Encore - Real-Time Insights & Scalable Editing with ArcGIS
Safe Software
 
Per Axbom: The spectacular lies of maps
Nexer Digital
 
Dev Dives: Automate, test, and deploy in one place—with Unified Developer Exp...
AndreeaTom
 
Tea4chat - another LLM Project by Kerem Atam
a0m0rajab1
 
AI and Robotics for Human Well-being.pptx
JAYMIN SUTHAR
 
Economic Impact of Data Centres to the Malaysian Economy
flintglobalapac
 
AI Code Generation Risks (Ramkumar Dilli, CIO, Myridius)
Priyanka Aash
 
Make GenAI investments go further with the Dell AI Factory
Principled Technologies
 
Responsible AI and AI Ethics - By Sylvester Ebhonu
Sylvester Ebhonu
 
cloud computing vai.pptx for the project
vaibhavdobariyal79
 
OFFOFFBOX™ – A New Era for African Film | Startup Presentation
ambaicciwalkerbrian
 
Researching The Best Chat SDK Providers in 2025
Ray Fields
 
The Future of Mobile Is Context-Aware—Are You Ready?
iProgrammer Solutions Private Limited
 
The Future of AI & Machine Learning.pptx
pritsen4700
 

Refactoring in Practice - Sunnyconf 2010

  • 1. REFACTORING IN PRACTICE
  • 2. WHO IS TEH ME? Alex Sharp
  • 4. { :tweets_as => '@ajsharp' :blogs_at => 'alexjsharp.com' :ships_at => 'github.com/ajsharp' }
  • 7. APPS GROW UP MATURE
  • 8. IT’S NOT ALL GREEN FIELDS AND “SOFT LAUNCHES”
  • 9. so what is this talk about?
  • 10. large apps are a completely different beast than small apps
  • 12. tight code coupling across domain concepts
  • 13. these apps become harder to maintain as size increases
  • 14. tight domain coupling exposes itself as a symptom of application size
  • 15. this talk is about working with large apps
  • 16. it’s about staying on the straight and narrow
  • 17. it’s about staying on the process
  • 18. it’s about staying on the habit
  • 19. it’s about fixing bad code.
  • 20. it’s about responsibly fixing bad code.
  • 21. it’s about responsibly fixing bad and improving code.
  • 22. RULES OF REFACTORING
  • 23. RULE #1 TESTS ARE CRUCIAL
  • 24. we’re out of our element without tests
  • 25. red, green, refactor doesn’t work without the red
  • 29. AVOID THE EPIC REFACTOR (IF POSSIBLE)
  • 30. what do we mean when we use the term “refactor”
  • 31. TRADITIONAL DEFINITION To refactor code is to change it’s implementation while maintaining it’s behavior. - via Martin Fowler, Kent Beck et. al
  • 32. in web apps this means that the behavior for the end user remains unchanged
  • 33. COMMON PROBLEMS (I.E. ANTI-PATTERNS)
  • 34. LACK OF TECHNICAL LEADERSHIP • Many common anti-pattens violated consistently • DRY • single responsibility • large procedural code blocks
  • 35. LACK OF PLATFORM KNOWLEDGE • Often developers re-engineer features provided by the platform • frequent offense, but very low-hanging fruit to fix
  • 36. LACK OF TESTS A few typical scenarios: • 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”
  • 37. POOR DOMAIN MODELING • UDD: UI-driven development • Unfamiliarity with domain modeling • probably b/c many apps don’t need it
  • 38. WRANGLING VIEW LOGIC WITH A PRESENTER
  • 39. 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
  • 40. THE BILLINGS SCREEN • Core functionality was simply broken, not working • This area of the app is somewhat of a “shanty town”
  • 43. 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 } end end
  • 44. var! ivar! i @ ly@ oly Ho class BillingsController < ApplicationController def index H ivar! @visit_statuses = Visit::Statuses oly @ r! @visit_status = 'Complete' H @visit_status = params[:visit_status] unless params[:visit_status].blank? i va @billing_status = ! if @visit_status.upcase != 'COMPLETE' then '' r @ elsif ( params[:billing_status] && params[:billing_status] != '-' ) then params[:billing_status] a r! else Visit::Billing_pending iv ly end va @ @noted = 'true' o H i @noted = 'false' if params[:noted] == 'false' @ y l H @clinics = current_user.allclinics( @practice.id ) o @visits = current_clinic.visits.billable_visits(@billing_status, @visit_status).order_by("visit_date") oly @ ly H session[:billing_visits] = @visits.collect{ |v| v.id } o ivar! end H end
  • 45. %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
  • 46. %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
  • 48. 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.
  • 49. 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' seems important...maybe for searching? @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 } end end
  • 50. 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' WTF!?!? @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 } end end
  • 51. A QUICK NOTE ABOUT “PERPETUAL WTF SYNDROME”
  • 52. TOO MANY WTF’S WILL DRIVE YOU NUTS
  • 54. YOU FORGET THAT GOOD CODE EXISTS
  • 55. THAT’S ONE REASON WHY THE NEXT STEP IS SO IMPORTANT
  • 56. IT’S KEEPS THE DIALOGUE POSITIVE
  • 58. ENGINEERING GOALS • Program to an interface, not an implementation • Only one instance variable • Use extract class to wrap up presentation logic
  • 59. describe BillingsPresenter do before do @practice = mock_model(Practice) @user = mock_model(User, :practice => @practice) visit = mock_model Visit @presenter = BillingsPresenter.new({}, @user, @practice) @presenter.stub!(:visits).and_return([visit]) end visit.should_receive(:resource) describe '#visits' do @presenter.providers it 'should receive billable_visits' do end @practice.should_receive(:billable_visits) end @presenter.visits end describe '.default_visit_status' do end it 'is the capitalized version of COMPLETE' do BillingsPresenter.default_visit_status.should == describe '#visit_status' do 'Complete' it 'should default to the complete status' do end @presenter.visit_status.should == "Complete" end end end describe '.default_billing_status' do it 'is the capitalized version of COMPLETE' do describe '#billing_status' do BillingsPresenter.default_billing_status.should == it 'should default to default_billing_status' do 'Pending' @presenter.billing_status.should == end BillingsPresenter.default_billing_status end end describe '#visit_statuses' do it 'should use the params[:billing_status] if it exists' it 'is the capitalized version of the visit statuses' do do @presenter.visit_statuses.should == [['All', '']] + presenter = BillingsPresenter.new({:billing_status => Visit::Statuses.collect(&:capitalize) 'use me'}, @user, @practice) end presenter.billing_status.should == 'use me' end end end describe '#billing_stauses' do it 'is the capitalized version of the billing statuses' do describe '#clinics' do @presenter.billing_statuses.should == [['All', '']] + it 'should receive user.allclinics' do Visit::Billing_statuses.collect(&:capitalize) @user.should_receive(:allclinics).with(@practice.id) end @presenter.clinics end end end end describe '#providers' do it 'should get the providers from the visit' do
  • 60. 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) end end
  • 61. class BillingsController < ApplicationController def index @presenter = BillingsPresenter.new params, current_user, current_practice session[:billing_visits] = @presenter.visits.collect{ |v| v.id } end end
  • 62. %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
  • 63. WINS • Much cleaner, more testable controller • Much less brittle view template • The behavior of this action actually works
  • 64. FAT MODEL / SKINNY CONTROLLER
  • 65. UPDATE ACTION CRAZINESS • Update a collection of associated objects in the parent object’s update action
  • 66. Object Model Medical History * join model * Existing Condition
  • 67. STEP 1: READ CODE
  • 68. 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) 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 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 end end
  • 69. 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) 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 smell-fest success = condition.update_attribute(:has_condition, value) && success end end 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 end end
  • 71. 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
  • 72. 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
  • 74. I want to refactor in the following ways: 1. Push this code into the model 2. Extract varying logic into separate methods
  • 75. We need to write some characterization tests and get this action under test so we can figure out what it’s doing.
  • 76. This way we can rely on an automated testing workflow for instant feedback
  • 77. 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 end end
  • 78. we can eliminate this, b/c it’s an association of this model 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
  • 79. Next, do extract method on this line 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
  • 80. Cover this new method w/ tests 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
  • 81. ??? 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
  • 82. i.e. ecmh.find(:first, :conditions => { :existing_condition_id => condition_id }) 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
  • 83. do extract method here... 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
  • 84. ??? 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
  • 85. i.e. create or update 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
  • 86. 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
  • 87. 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
  • 88. We went from this mess in the controller 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) 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 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 end end
  • 89. To this in the controller... 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
  • 90. 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
  • 92. belongs in model 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 maybe a DB transaction? maybe nested params?
  • 93. BUT REFACTORING MUST BE DONE IN SMALL STEPS
  • 94. 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
  • 99. “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.”
  • 100. Rate my talk: http://bit.ly/ajsharp-sunnyconf-refactoring Slides: http://slidesha.re/ajsharp-sunnyconf-refactoring-slides
  • 101. 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