Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Working Effectively With Legacy Code

3,073 views

Published on

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

Published in: Technology
  • Be the first to comment

Working Effectively With Legacy Code

  1. 1. WORKING EFFECTIVELY WITH LEGACY RAILS Pat Maddox & BJ Clark
  2. 2. Why Care About Legacy Code? Rails is almost 5 years old Lots of $$ to be made Everyone writes legacy code
  3. 3. What is Legacy Code? Code written >3 months ago Code written by someone else
  4. 4. 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”
  5. 5. Code that is difficult to change
  6. 6. TECHNICAL DEBT
  7. 7. 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)
  8. 8. Examples of Technical Debt Rails MediaWiki ActiveScaffold Code you write
  9. 9. Recognizing Debt Easy upfront gains Difficult to extend long-term (does the tool support new requirements) No clear migration path
  10. 10. 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
  11. 11. TATFT & RATFT Virtuous cycle - Symbiotic Relationship Anti-pattern: Red. Green. Deploy. Pattern: Red. Green. Refactor. Deploy.
  12. 12. TESTING TO MINIMIZE DEBT
  13. 13. Goals Maintain existing value Add new value
  14. 14. Start Testing Now What level? Unit vs Acceptance Cast a wide net
  15. 15. Ok, lets test Take 6 weeks off to write tests for your code
  16. 16. Ok, lets test Take 6 weeks off to write tests for your code
  17. 17. Ok, lets test Just in time Characterization tests Cover existing functionality Never red, always green Test-drive new features
  18. 18. Siloing Code Webservice/SOA Rails 3.0 mounted applications
  19. 19. REFACTORING TECHNIQUES
  20. 20. Rails already does it Caching has_many :through Delegation, memoization, serialization Validations and the .valid? api
  21. 21. Make everything a framework All the benefits of abstracted code Use existing frameworks Write your own mini-frameworks
  22. 22. EXAMPLE
  23. 23. 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
  24. 24. 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
  25. 25. 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
  26. 26. WRITE A MINI-FRAMEWORK
  27. 27. 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
  28. 28. 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
  29. 29. What we’ve learned Pros Easy to read Behavior for free Cons Not as accessible to brand new Ruby programmers
  30. 30. USE EXISTING FRAMEWORKS
  31. 31. 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
  32. 32. resource_controller class BoatsController < ApplicationController resource_controller # thanks James!!! require_authorization :update end
  33. 33. 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
  34. 34. After class BoatsController < ApplicationController resource_controller # thanks James!!! require_authorization :update end
  35. 35. METAPROGRAMMING ROCKS
  36. 36. METAPROGRAMMING ROCKS (with great power comes great responsibility)
  37. 37. LEVERAGE RAILS
  38. 38. Rails Extension Points before_*/after_* hooks AR observers ActionController filters
  39. 39. 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
  40. 40. 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
  41. 41. 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
  42. 42. Simpler Controller def create @account = Account.new params[:account] if @account.save redirect_to @account else render :template => quot;newquot; end end
  43. 43. 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
  44. 44. 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
  45. 45. Tradeoffs Cons Pros Indirection Free transaction semantics Testing is a bit slower/ Skinny controller, fat tougher model
  46. 46. 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
  47. 47. 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
  48. 48. 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
  49. 49. SEAMS
  50. 50. Seams Modify or sense behavior of code without changing it OO - polymorphism Dependency management
  51. 51. Ruby seams alias_method_chain method_missing send / eval
  52. 52. 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
  53. 53. 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
  54. 54. 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
  55. 55. 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
  56. 56. 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
  57. 57. 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
  58. 58. QUESTIONS

×