• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
Working Effectively With Legacy Code
 

Working Effectively With Legacy Code

on

  • 3,576 views

Rails Conf 2009 presentation by BJ Clark and Pat Maddox of the science department on Working with Legacy Rails Code

Rails Conf 2009 presentation by BJ Clark and Pat Maddox of the science department on Working with Legacy Rails Code

Statistics

Views

Total Views
3,576
Views on SlideShare
3,519
Embed Views
57

Actions

Likes
5
Downloads
0
Comments
0

5 Embeds 57

http://www.refactorsquad.com 16
http://refactorsquad.com 13
http://feeds2.feedburner.com 13
http://translate.googleusercontent.com 9
http://www.slideshare.net 6

Accessibility

Categories

Upload Details

Uploaded via as Apple Keynote

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment
  • <br />
  • <br />
  • The length of time doesn’t matter. It could be 3 weeks <br />
  • <br />
  • Code might be unfactored because you weren’t good enough to see the refactorings earlier. <br />
  • No tests - how do you have confidence? <br /> You could have well-designed code with no tests... “clean room”...falcon circling the sky then strikes <br /> Bad design <br /> Dependencies (Rails makes you not care. Stuff like const_missing is great but hides pain points, association chains) <br />
  • <br />
  • The more debt you have, the harder it is to adapt to changing requirements. <br /> Systems become large, it’s important for them to be designed and architected such that you can reason about subsystems. <br /> Eventually, programmers want the Big Rewrite. We’ve both advocated for the Big Rewrite on a project that hadn’t even launched yet. <br />
  • *** Choosing Rails <br />   - Pros: we know them <br />   - Cons: Locked into Ruby (issue when you need to have multithreaded code).  Opinionated - when your business expands past those opinions, must pay down debt.  Example: AR assumes one database.  Need to write libraries/rearchitect to support clustering <br /> *** Choosing mediawiki <br />   - Pros: Easy to get the site up and running <br />   - Cons: Difficult to extend, difficult to scale <br />   - Result: Spent a year+ replacing it piece-by-piece with Rails <br /> *** Using ActiveScaffold <br />   -  Pros: Get scaffolding quickly and easily <br />   - Cons: Internal code is a mess, untested, difficult to extend <br /> *** Code you write <br />   - Not refactoring / writing tests <br /> - Poorly tested code is almost as bad as not testing at all. <br />
  • has_many =>  has_many :through  (clear migration path) <br /> <br /> one database server => multiple database servers (requires community to create new tools. no clear migration path out of the box) <br />
  • ** Could spend six months designing the system so that it supports all the functionality and has extensibility points <br /> *** We know that doesn't actually work <br /> *** Plus you don't have working software <br /> * Agile approach <br />   ** Do simple things to add value right now <br />   ** Technical debt is central to Agile development - embrace it <br /> <br />
  • ** TATFT <br />   - Testing allows you to refactor <br />   - Refactoring pays down debt <br />   - Virtuous cycle => Testing makes refactoring possible, refactoring makes testing easier <br /> <br /> ** RATFT <br />   - Antipattern: red/green deploy Just because you have tests for your 70 line controller method, doesn't mean it's good or that you're done. <br />   - red/green/REFACTOR deploy <br />   - Get to green, take the time to make your code nice.  You should spend equal or more time refactoring than making your tests green. <br /> <br /> AboutUs: 2-3 deployments per day. No staging. <br />
  • <br />
  • Legacy systems provide existing value.  The foremost requirement when making changes to a system is not to lose the existing value. Automated tests provide the safety net. <br />
  • * You need to write tests, what do you do? Unit Tests or Acceptance Tests? <br /> A. Acceptance (originally called functional) <br /> <br /> Cuke: <br /> - Uses full rails stack. <br /> - Tests multiple requests in a single test. <br /> - Hits multiple models and controllers, session, external services, etc. <br /> * What's the point? <br /> - 1 cucumber test covers the same amount of code as 25 unit tests <br /> - Level of abstraction - reasoning about usage of the system, as opposed to one tiny little piece out of context <br /> - Captures existing system functionality <br />
  • <br />
  • <br />
  • Whenever you add code, examine areas that use the code, write tests to exercise those areas (just in time) (Kanban for the ninjas out there) <br /> <br /> Characterization tests - let you know how the system behaves currently. May even expose bugs but you don’t fix them just yet! Make a note or a story in tracker and move on. <br /> <br /> ** Pushback <br /> - Writing tests is too hard.  No it's not <br /> - No, really, it is too hard <br />
  • ** Silo code <br /> - Push it behind a webservice.  Write simple integration tests.  Example: AboutUs uses mediawiki as a parsing engine.  Easy to write Rails-app level tests for transformations, then push it off to mediawiki service <br /> - Don’t touch it after you silo it. <br /> <br /> - Rails 3: mountable apps <br />
  • <br />
  • ActiveSupport does a looooooot of stuff <br />
  • ** Use existing frameworks - resource_controller <br /> ** Write your own - Pivotal's authorization system (can_create, can_update auto-called from controller) <br /> ** Extraction is a very valid refactoring technique. <br />
  • <br />
  • <br />
  • <br />
  • * How many actions have people written that were like this? <br /> <br />
  • <br />
  • * Added one method (load_model) <br /> * Got behavior for free <br />
  • <br />
  • * Con isn’t really a con. New Ruby programmers won’t stay new for long <br /> * If they learn this, they’ll start to write code this way <br />
  • <br />
  • <br />
  • * I didn’t realize how easy that was until I did it from scratch <br /> * It “just worked” <br />
  • *This is considdered by most people as a “clean/skinny” controller. <br />
  • Why refactor? So you can do this <br /> We realize that legacy controllers aren’t even this clean. <br /> We never said it would be easy. <br /> Refactor your code, then you can make it sex <br /> This is the goal <br />
  • <br />
  • <br />
  • <br />
  • <br />
  • <br />
  • Transactions in the controller are an anti-pattern. <br />
  • <br />
  • <br />
  • <br />
  • <br />
  • No explicit transactions <br /> You can use a framework <br /> <br /> Code is tougher to understand due to indirection <br />
  • <br />
  • <br />
  • Transaction semantics without an explicit transaction <br /> Account and project focused on domain responsibilities <br /> AccountRegistration provides natural point for stuff like sending a verification email (also helps with testing) <br /> AccountRegistration can get sophisticated without muddling model - validates_associated project and video, if you want <br /> You could write a script to clean up AccountRegistration records when they’re no longer needed, depending on domain <br />
  • <br />
  • <br />
  • <br />
  • * Problems with this test <br /> ** Magic number (california sales tax) <br /> ** coupled to TaxCalculator implementation <br />
  • * Can use a mock or a simpler fake object <br /> * Every Java programmer asks “what library to use for DI” <br /> * Ruby programmers say “don’t use it” <br /> * Misses the point. Don’t use a framework. Use Ruby <br />
  • * Problem: this will break all clients <br />
  • <br />
  • <br />
  • Learn the hooks (inherit, included, etc) <br /> Understand how has_many works - it’s not magic! <br /> This lets you be very creative and have fun <br /> Working Effectively...gives you concrete strategies for getting a code base under test <br />
  • <br />

Working Effectively With Legacy Code Working Effectively With Legacy Code Presentation Transcript

  • WORKING EFFECTIVELY WITH LEGACY RAILS Pat Maddox & BJ Clark
  • Why Care About Legacy Code? Rails is almost 5 years old Lots of $$ to be made Everyone writes legacy code
  • What is Legacy Code? Code written >3 months ago Code written by someone else
  • What is Legacy Code? Michael Feathers - “Code Without Tests” Ward Cunningham - “Accumulated technical debt” DHH - “Code you wrote when weren’t as good as you are now”
  • Code that is difficult to change
  • TECHNICAL DEBT
  • What is Technical Debt? “During the planning or execution of a software project, decisions are made to defer necessary work, this is creating technical debt” http://c2.com/cgi/wiki?TechnicalDebt Why is it bad? Not necessarily bad (good vs bad debt)
  • Examples of Technical Debt Rails MediaWiki ActiveScaffold Code you write
  • Recognizing Debt Easy upfront gains Difficult to extend long-term (does the tool support new requirements) No clear migration path
  • Managing Technical Debt “Remove cruft as you go. Build simplicity and clarity in from the beginning, and never relent in keeping them in. “ -Ron Jeffries Big Design Up Front Agile
  • TATFT & RATFT Virtuous cycle - Symbiotic Relationship Anti-pattern: Red. Green. Deploy. Pattern: Red. Green. Refactor. Deploy.
  • TESTING TO MINIMIZE DEBT
  • Goals Maintain existing value Add new value
  • Start Testing Now What level? Unit vs Acceptance Cast a wide net
  • Ok, lets test Take 6 weeks off to write tests for your code
  • Ok, lets test Take 6 weeks off to write tests for your code
  • Ok, lets test Just in time Characterization tests Cover existing functionality Never red, always green Test-drive new features
  • Siloing Code Webservice/SOA Rails 3.0 mounted applications
  • REFACTORING TECHNIQUES
  • Rails already does it Caching has_many :through Delegation, memoization, serialization Validations and the .valid? api
  • Make everything a framework All the benefits of abstracted code Use existing frameworks Write your own mini-frameworks
  • EXAMPLE
  • The Tests it quot;works when updated by ownerquot; do describe BoatsController do before(:each) do do_update @owner @owner = User.create! @boat.reload.name.should == quot;SS Minnowquot; @boat = Boat.create! end :name => quot;Pequodquot;, :owner => @owner it quot;works when updated by adminquot; do end admin = User.create! :admin => true do_update admin def do_update(user) @boat.reload.name.should == quot;SS Minnowquot; login_as user end put :update, :id => @boat.to_param, it quot;fails when updated by non-ownerquot; do :boat => nonowner = User.create! {:name => quot;SS Minnowquot;} do_update nonowner end @boat.reload.name.should == quot;Pequodquot; end
  • The model class Boat < ActiveRecord::Base belongs_to :owner, :class_name => quot;Userquot; def can_update?(user) user == owner || user.admin? || owner.blank? end end
  • The controller class BoatsController < ApplicationController def update @boat = Boat.find params[:id] if @boat.can_update?(current_user) if @boat.update_attributes(params[:boat]) redirect_to boat_url(@boat) else render :action => quot;editquot; end else redirect_to boat_url(@boat) end end end
  • WRITE A MINI-FRAMEWORK
  • The new controller class BoatsController < ApplicationController require_authorization :update def update if @boat.update_attributes(params[:boat]) redirect_to @boat else render :action => quot;editquot; end end def load_model @boat ||= Boat.find(params[:id]) end end
  • The “magic” class ApplicationController < ActionController::Base def self.require_authorization(*actions) actions.each do |a| before_filter :check_authorization, :only => a end end def check_authorization model = load_model unless model.send(quot;can_#{action_name}?quot;, current_user) redirect_to model end end end
  • What we’ve learned Pros Easy to read Behavior for free Cons Not as accessible to brand new Ruby programmers
  • USE EXISTING FRAMEWORKS
  • A little cleanup class BoatsController < ApplicationController ... def object @boat ||= Boat.find(params[:id]) end end class ApplicationController < ActionController::Base ... def check_authorization unless object.send(quot;can_#{action_name}?quot;, current_user) redirect_to model end end end
  • resource_controller class BoatsController < ApplicationController resource_controller # thanks James!!! require_authorization :update end
  • Before class BoatsController < ApplicationController def update @boat = Boat.find params[:id] if @boat.can_update?(current_user) if @boat.update_attributes(params[:boat]) redirect_to boat_url(@boat) else render :action => quot;editquot; end else redirect_to boat_url(@boat) end end end
  • After class BoatsController < ApplicationController resource_controller # thanks James!!! require_authorization :update end
  • METAPROGRAMMING ROCKS
  • METAPROGRAMMING ROCKS (with great power comes great responsibility)
  • LEVERAGE RAILS
  • Rails Extension Points before_*/after_* hooks AR observers ActionController filters
  • The Test describe AccountsController, quot;POST createquot; do def do_post post :create, :account => {:name => quot;Nikequot;} end it quot;should create a project for the accountquot; do do_post Account.first.should have(1).project end end
  • The Controller def create @account = Account.new params[:account] Account.transaction do if @account.save @account.projects.create :name => quot;#{@account.name}'s First Projectquot; redirect_to @account else render :template => quot;newquot; end end end
  • Using Callbacks class Account < ActiveRecord::Base has_many :projects after_create :create_project def create_project projects.create! :name => quot;#{name}'s First Projectquot; end end
  • Simpler Controller def create @account = Account.new params[:account] if @account.save redirect_to @account else render :template => quot;newquot; end end
  • A little more describe AccountsController, quot;POST createquot; do def do_post post :create, :account => {:name => quot;Nikequot;} end it quot;should create a project for the accountquot; do do_post Account.first.should have(1).project end it quot;should create a video for the projectquot; do do_post Account.first.projects.first.should have(1).video end end
  • The Model class Account < ActiveRecord::Base after_create :create_project ... end class Project < ActiveRecord::Base has_many :videos belongs_to :account after_create :create_video def create_video videos.create! :name => quot;#{account.name}'s First Videoquot; end end
  • Tradeoffs Cons Pros Indirection Free transaction semantics Testing is a bit slower/ Skinny controller, fat tougher model
  • Modeling a Business Event class AccountRegistration < ActiveRecord::Base belongs_to :account before_create :setup_account validates_associated :account attr_writer :name def setup_account self.account = Account.create :name => @name project = account.projects.create! :name => quot;#{@name}'s First Projectquot; project.videos.create! :name => quot;#{@name}'s First Videoquot; end end
  • Controller & Other Models # Use resource_controller def create @registration = AccountRegistration.new params[:account] if @registration.save redirect_to @registration.account else render :template => quot;newquot; end end class Project < ActiveRecord::Base has_many :videos end class Account < ActiveRecord::Base has_many :projects end
  • Modeling a Business Event class Account < ActiveRecord::Base class AccountRegistration < AR::Base has_many :projects belongs_to :account end before_create :setup_account validates_associated :account class Project < ActiveRecord::Base attr_writer :name has_many :videos end def setup_account self.account = Account.create :name => @name project = account.projects.create! :name => quot;#{@name}'s First Projectquot; project.videos.create! :name => quot;#{@name}'s First Videoquot; end end
  • SEAMS
  • Seams Modify or sense behavior of code without changing it OO - polymorphism Dependency management
  • Ruby seams alias_method_chain method_missing send / eval
  • class Order def initialize(us_state) @us_state = us_state @subtotal = 0 end def add_item(item, quantity) @subtotal += (item.cost * quantity) end def tax TaxCalculator.calculate @subtotal, @us_state end end describe Order, quot;calculating taxquot; do it quot;should add tax onto the totalquot; do o = Order.new quot;CAquot; bacon = Item.new quot;Chunky baconquot;, 42 o.add_item bacon, 1 o.tax.should == 3.26 end end
  • DI to the rescue class Order def tax(calculator) calculator.calculate @subtotal, @us_state end end describe Order, quot;calculating taxquot; do it quot;should add tax onto the totalquot; do fake_calculator = mock('calculator') fake_calculator.should_receive(:calculate). with(42, quot;CAquot;).and_return 3.26 o = Order.new quot;CAquot; bacon = Item.new quot;Chunky baconquot;, 42 o.add_item bacon, 1 o.tax(fake_calculator).should == 3.26 end end
  • DI to the rescue class Order Breaks existing client code def tax(calculator) calculator.calculate @subtotal, @us_state end end describe Order, quot;calculating taxquot; do it quot;should add tax onto the totalquot; do fake_calculator = mock('calculator') fake_calculator.should_receive(:calculate). with(42, quot;CAquot;).and_return 3.26 o = Order.new quot;CAquot; bacon = Item.new quot;Chunky baconquot;, 42 o.add_item bacon, 1 o.tax(fake_calculator).should == 3.26 end end
  • Free DI to the rescue class Order Clients continue to def tax(calculator=TaxCalculator) work unchanged calculator.calculate @subtotal, @us_state end end describe Order, quot;calculating taxquot; do it quot;should add tax onto the totalquot; do fake_calculator = mock('calculator') fake_calculator.should_receive(:calculate). with(42, quot;CAquot;).and_return 3.26 o = Order.new quot;CAquot; bacon = Item.new quot;Chunky baconquot;, 42 o.add_item bacon, 1 o.tax(fake_calculator).should == 3.26 end end
  • Partial mocking class Order def tax TaxCalculator.calculate @subtotal, @us_state end end describe Order, quot;calculating taxquot; do it quot;should add tax onto the totalquot; do o = Order.new quot;CAquot; bacon = Item.new quot;Chunky baconquot;, 42 o.add_item bacon, 1 TaxCalculator.should_receive(:calculate). with(42, quot;CAquot;).and_return 3.26 o.tax.should == 3.26 end end
  • What To Do From Here Develop a deep understanding of Ruby Make good use of Rails Read “Working Effectively with Legacy Code” by Michael Feathers Check out our new blog: www.refactorsquad.com
  • QUESTIONS