SlideShare ist ein Scribd-Unternehmen logo
1 von 103
Downloaden Sie, um offline zu lesen
REFACTORING IN
   PRACTICE
WHO IS TEH ME?

 Alex Sharp
{ :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
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”
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!

Weitere ähnliche Inhalte

Was ist angesagt?

ZFConf 2010: Zend Framework & MVC, Model Implementation (Part 2, Dependency I...
ZFConf 2010: Zend Framework & MVC, Model Implementation (Part 2, Dependency I...ZFConf 2010: Zend Framework & MVC, Model Implementation (Part 2, Dependency I...
ZFConf 2010: Zend Framework & MVC, Model Implementation (Part 2, Dependency I...
ZFConf Conference
 
Modern JavaScript Engine Performance
Modern JavaScript Engine PerformanceModern JavaScript Engine Performance
Modern JavaScript Engine Performance
Catalin Dumitru
 

Was ist angesagt? (20)

ZFConf 2010: Zend Framework & MVC, Model Implementation (Part 2, Dependency I...
ZFConf 2010: Zend Framework & MVC, Model Implementation (Part 2, Dependency I...ZFConf 2010: Zend Framework & MVC, Model Implementation (Part 2, Dependency I...
ZFConf 2010: Zend Framework & MVC, Model Implementation (Part 2, Dependency I...
 
Zend framework 04 - forms
Zend framework 04 - formsZend framework 04 - forms
Zend framework 04 - forms
 
Tools for Making Machine Learning more Reactive
Tools for Making Machine Learning more ReactiveTools for Making Machine Learning more Reactive
Tools for Making Machine Learning more Reactive
 
RichFaces: more concepts and features
RichFaces: more concepts and featuresRichFaces: more concepts and features
RichFaces: more concepts and features
 
Refactoring using Codeception
Refactoring using CodeceptionRefactoring using Codeception
Refactoring using Codeception
 
Disregard Inputs, Acquire Zend_Form
Disregard Inputs, Acquire Zend_FormDisregard Inputs, Acquire Zend_Form
Disregard Inputs, Acquire Zend_Form
 
Single Page Web Apps with Backbone.js and Rails
Single Page Web Apps with Backbone.js and RailsSingle Page Web Apps with Backbone.js and Rails
Single Page Web Apps with Backbone.js and Rails
 
Hacking Your Way To Better Security - Dutch PHP Conference 2016
Hacking Your Way To Better Security - Dutch PHP Conference 2016Hacking Your Way To Better Security - Dutch PHP Conference 2016
Hacking Your Way To Better Security - Dutch PHP Conference 2016
 
Angular Tutorial Freshers and Experienced
Angular Tutorial Freshers and ExperiencedAngular Tutorial Freshers and Experienced
Angular Tutorial Freshers and Experienced
 
Tony Vitabile .Net Portfolio
Tony Vitabile .Net PortfolioTony Vitabile .Net Portfolio
Tony Vitabile .Net Portfolio
 
Ruby on rails
Ruby on rails Ruby on rails
Ruby on rails
 
Framework
FrameworkFramework
Framework
 
Everything you always wanted to know about forms* *but were afraid to ask
Everything you always wanted to know about forms* *but were afraid to askEverything you always wanted to know about forms* *but were afraid to ask
Everything you always wanted to know about forms* *but were afraid to ask
 
Sylius and Api Platform The story of integration
Sylius and Api Platform The story of integrationSylius and Api Platform The story of integration
Sylius and Api Platform The story of integration
 
Modern JavaScript Engine Performance
Modern JavaScript Engine PerformanceModern JavaScript Engine Performance
Modern JavaScript Engine Performance
 
Adding Dependency Injection to Legacy Applications
Adding Dependency Injection to Legacy ApplicationsAdding Dependency Injection to Legacy Applications
Adding Dependency Injection to Legacy Applications
 
Php Enums
Php EnumsPhp Enums
Php Enums
 
Injection de dépendances dans Symfony >= 3.3
Injection de dépendances dans Symfony >= 3.3Injection de dépendances dans Symfony >= 3.3
Injection de dépendances dans Symfony >= 3.3
 
Leveraging Symfony2 Forms
Leveraging Symfony2 FormsLeveraging Symfony2 Forms
Leveraging Symfony2 Forms
 
Why is crud a bad idea - focus on real scenarios
Why is crud a bad idea - focus on real scenariosWhy is crud a bad idea - focus on real scenarios
Why is crud a bad idea - focus on real scenarios
 

Andere mochten auch

Andere mochten auch (6)

Estácio: 1Q11 Conference Call Presentation
Estácio: 1Q11 Conference Call PresentationEstácio: 1Q11 Conference Call Presentation
Estácio: 1Q11 Conference Call Presentation
 
Blending the University: Beyond MOOCs
Blending the University: Beyond MOOCsBlending the University: Beyond MOOCs
Blending the University: Beyond MOOCs
 
Digitale communicatie
Digitale communicatieDigitale communicatie
Digitale communicatie
 
JCI Estonia Treeninginstituut
JCI Estonia TreeninginstituutJCI Estonia Treeninginstituut
JCI Estonia Treeninginstituut
 
The Reprints Revolution will not be Televised
The Reprints Revolution will not be TelevisedThe Reprints Revolution will not be Televised
The Reprints Revolution will not be Televised
 
Teams as the unit of organization scale
Teams as the unit of organization scale Teams as the unit of organization scale
Teams as the unit of organization scale
 

Ähnlich wie Refactoring in Practice - Sunnyconf 2010

OSDC 2009 Rails Turtorial
OSDC 2009 Rails TurtorialOSDC 2009 Rails Turtorial
OSDC 2009 Rails Turtorial
Yi-Ting Cheng
 
Ruby on Rails For Java Programmers
Ruby on Rails For Java ProgrammersRuby on Rails For Java Programmers
Ruby on Rails For Java Programmers
elliando dias
 
Desenvolvimento web com Ruby on Rails (extras)
Desenvolvimento web com Ruby on Rails (extras)Desenvolvimento web com Ruby on Rails (extras)
Desenvolvimento web com Ruby on Rails (extras)
Joao Lucas Santana
 

Ähnlich wie Refactoring in Practice - Sunnyconf 2010 (20)

Advanced RESTful Rails
Advanced RESTful RailsAdvanced RESTful Rails
Advanced RESTful Rails
 
Advanced RESTful Rails
Advanced RESTful RailsAdvanced RESTful Rails
Advanced RESTful Rails
 
Simplify Your Rails Controllers With a Vengeance
Simplify Your Rails Controllers With a VengeanceSimplify Your Rails Controllers With a Vengeance
Simplify Your Rails Controllers With a Vengeance
 
More to RoC weibo
More to RoC weiboMore to RoC weibo
More to RoC weibo
 
PHPSpec - the only Design Tool you need - 4Developers
PHPSpec - the only Design Tool you need - 4DevelopersPHPSpec - the only Design Tool you need - 4Developers
PHPSpec - the only Design Tool you need - 4Developers
 
Dependency injection - the right way
Dependency injection - the right wayDependency injection - the right way
Dependency injection - the right way
 
Zend framework service
Zend framework serviceZend framework service
Zend framework service
 
Zend framework service
Zend framework serviceZend framework service
Zend framework service
 
Introduction to Zend Framework web services
Introduction to Zend Framework web servicesIntroduction to Zend Framework web services
Introduction to Zend Framework web services
 
Rails is not just Ruby
Rails is not just RubyRails is not just Ruby
Rails is not just Ruby
 
Rails MVC by Sergiy Koshovyi
Rails MVC by Sergiy KoshovyiRails MVC by Sergiy Koshovyi
Rails MVC by Sergiy Koshovyi
 
前后端mvc经验 - webrebuild 2011 session
前后端mvc经验 - webrebuild 2011 session前后端mvc经验 - webrebuild 2011 session
前后端mvc经验 - webrebuild 2011 session
 
Rails2 Pr
Rails2 PrRails2 Pr
Rails2 Pr
 
OSDC 2009 Rails Turtorial
OSDC 2009 Rails TurtorialOSDC 2009 Rails Turtorial
OSDC 2009 Rails Turtorial
 
Ruby on Rails For Java Programmers
Ruby on Rails For Java ProgrammersRuby on Rails For Java Programmers
Ruby on Rails For Java Programmers
 
Dutch PHP Conference - PHPSpec 2 - The only Design Tool you need
Dutch PHP Conference - PHPSpec 2 - The only Design Tool you needDutch PHP Conference - PHPSpec 2 - The only Design Tool you need
Dutch PHP Conference - PHPSpec 2 - The only Design Tool you need
 
Ruby/Rails
Ruby/RailsRuby/Rails
Ruby/Rails
 
Desenvolvimento web com Ruby on Rails (extras)
Desenvolvimento web com Ruby on Rails (extras)Desenvolvimento web com Ruby on Rails (extras)
Desenvolvimento web com Ruby on Rails (extras)
 
Crie seu sistema REST com JAX-RS e o futuro
Crie seu sistema REST com JAX-RS e o futuroCrie seu sistema REST com JAX-RS e o futuro
Crie seu sistema REST com JAX-RS e o futuro
 
RSpec User Stories
RSpec User StoriesRSpec User Stories
RSpec User Stories
 

Mehr von Alex Sharp (11)

Bldr: A Minimalist JSON Templating DSL
Bldr: A Minimalist JSON Templating DSLBldr: A Minimalist JSON Templating DSL
Bldr: A Minimalist JSON Templating DSL
 
Bldr - Rubyconf 2011 Lightning Talk
Bldr - Rubyconf 2011 Lightning TalkBldr - Rubyconf 2011 Lightning Talk
Bldr - Rubyconf 2011 Lightning Talk
 
Mysql to mongo
Mysql to mongoMysql to mongo
Mysql to mongo
 
Refactoring in Practice - Ruby Hoedown 2010
Refactoring in Practice - Ruby Hoedown 2010Refactoring in Practice - Ruby Hoedown 2010
Refactoring in Practice - Ruby Hoedown 2010
 
Practical Ruby Projects with MongoDB - Ruby Kaigi 2010
Practical Ruby Projects with MongoDB - Ruby Kaigi 2010Practical Ruby Projects with MongoDB - Ruby Kaigi 2010
Practical Ruby Projects with MongoDB - Ruby Kaigi 2010
 
Practical Ruby Projects with MongoDB - Ruby Midwest
Practical Ruby Projects with MongoDB - Ruby MidwestPractical Ruby Projects with MongoDB - Ruby Midwest
Practical Ruby Projects with MongoDB - Ruby Midwest
 
Practical Ruby Projects with MongoDB - MongoSF
Practical Ruby Projects with MongoDB - MongoSFPractical Ruby Projects with MongoDB - MongoSF
Practical Ruby Projects with MongoDB - MongoSF
 
Practical Ruby Projects With Mongo Db
Practical Ruby Projects With Mongo DbPractical Ruby Projects With Mongo Db
Practical Ruby Projects With Mongo Db
 
Intro To MongoDB
Intro To MongoDBIntro To MongoDB
Intro To MongoDB
 
Getting Comfortable with BDD
Getting Comfortable with BDDGetting Comfortable with BDD
Getting Comfortable with BDD
 
Testing Has Many Purposes
Testing Has Many PurposesTesting Has Many Purposes
Testing Has Many Purposes
 

Kürzlich hochgeladen

CNv6 Instructor Chapter 6 Quality of Service
CNv6 Instructor Chapter 6 Quality of ServiceCNv6 Instructor Chapter 6 Quality of Service
CNv6 Instructor Chapter 6 Quality of Service
giselly40
 
EIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptx
EIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptxEIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptx
EIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptx
Earley Information Science
 

Kürzlich hochgeladen (20)

Exploring the Future Potential of AI-Enabled Smartphone Processors
Exploring the Future Potential of AI-Enabled Smartphone ProcessorsExploring the Future Potential of AI-Enabled Smartphone Processors
Exploring the Future Potential of AI-Enabled Smartphone Processors
 
Data Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt RobisonData Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt Robison
 
Bajaj Allianz Life Insurance Company - Insurer Innovation Award 2024
Bajaj Allianz Life Insurance Company - Insurer Innovation Award 2024Bajaj Allianz Life Insurance Company - Insurer Innovation Award 2024
Bajaj Allianz Life Insurance Company - Insurer Innovation Award 2024
 
A Domino Admins Adventures (Engage 2024)
A Domino Admins Adventures (Engage 2024)A Domino Admins Adventures (Engage 2024)
A Domino Admins Adventures (Engage 2024)
 
08448380779 Call Girls In Greater Kailash - I Women Seeking Men
08448380779 Call Girls In Greater Kailash - I Women Seeking Men08448380779 Call Girls In Greater Kailash - I Women Seeking Men
08448380779 Call Girls In Greater Kailash - I Women Seeking Men
 
CNv6 Instructor Chapter 6 Quality of Service
CNv6 Instructor Chapter 6 Quality of ServiceCNv6 Instructor Chapter 6 Quality of Service
CNv6 Instructor Chapter 6 Quality of Service
 
Breaking the Kubernetes Kill Chain: Host Path Mount
Breaking the Kubernetes Kill Chain: Host Path MountBreaking the Kubernetes Kill Chain: Host Path Mount
Breaking the Kubernetes Kill Chain: Host Path Mount
 
How to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected WorkerHow to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected Worker
 
Automating Google Workspace (GWS) & more with Apps Script
Automating Google Workspace (GWS) & more with Apps ScriptAutomating Google Workspace (GWS) & more with Apps Script
Automating Google Workspace (GWS) & more with Apps Script
 
08448380779 Call Girls In Civil Lines Women Seeking Men
08448380779 Call Girls In Civil Lines Women Seeking Men08448380779 Call Girls In Civil Lines Women Seeking Men
08448380779 Call Girls In Civil Lines Women Seeking Men
 
Slack Application Development 101 Slides
Slack Application Development 101 SlidesSlack Application Development 101 Slides
Slack Application Development 101 Slides
 
A Year of the Servo Reboot: Where Are We Now?
A Year of the Servo Reboot: Where Are We Now?A Year of the Servo Reboot: Where Are We Now?
A Year of the Servo Reboot: Where Are We Now?
 
Advantages of Hiring UIUX Design Service Providers for Your Business
Advantages of Hiring UIUX Design Service Providers for Your BusinessAdvantages of Hiring UIUX Design Service Providers for Your Business
Advantages of Hiring UIUX Design Service Providers for Your Business
 
Finology Group – Insurtech Innovation Award 2024
Finology Group – Insurtech Innovation Award 2024Finology Group – Insurtech Innovation Award 2024
Finology Group – Insurtech Innovation Award 2024
 
EIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptx
EIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptxEIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptx
EIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptx
 
The 7 Things I Know About Cyber Security After 25 Years | April 2024
The 7 Things I Know About Cyber Security After 25 Years | April 2024The 7 Things I Know About Cyber Security After 25 Years | April 2024
The 7 Things I Know About Cyber Security After 25 Years | April 2024
 
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdf
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdfThe Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdf
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdf
 
From Event to Action: Accelerate Your Decision Making with Real-Time Automation
From Event to Action: Accelerate Your Decision Making with Real-Time AutomationFrom Event to Action: Accelerate Your Decision Making with Real-Time Automation
From Event to Action: Accelerate Your Decision Making with Real-Time Automation
 
Understanding Discord NSFW Servers A Guide for Responsible Users.pdf
Understanding Discord NSFW Servers A Guide for Responsible Users.pdfUnderstanding Discord NSFW Servers A Guide for Responsible Users.pdf
Understanding Discord NSFW Servers A Guide for Responsible Users.pdf
 
Presentation on how to chat with PDF using ChatGPT code interpreter
Presentation on how to chat with PDF using ChatGPT code interpreterPresentation on how to chat with PDF using ChatGPT code interpreter
Presentation on how to chat with PDF using ChatGPT code interpreter
 

Refactoring in Practice - Sunnyconf 2010

  • 1. REFACTORING IN PRACTICE
  • 2. WHO IS TEH ME? Alex Sharp
  • 3.
  • 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
  • 26.
  • 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”
  • 41.
  • 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