2. Why Care About Legacy Code?
Rails is almost 5 years old
Lots of $$ to be made
Everyone writes legacy code
3. What is Legacy Code?
Code written >3 months ago
Code written by someone else
4.
5. 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”
8. 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)
10. Recognizing Debt
Easy upfront gains
Difficult to extend long-term (does the tool support new
requirements)
No clear migration path
11. 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
24. 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
25. 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
26. 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
28. 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
29. 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
30. What we’ve learned
Pros
Easy to read
Behavior for free
Cons
Not as accessible to brand new Ruby programmers
32. 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
34. 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
35. After
class BoatsController < ApplicationController
resource_controller # thanks James!!!
require_authorization :update
end
40. 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
41. 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
42. 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
43. Simpler Controller
def create
@account = Account.new params[:account]
if @account.save
redirect_to @account
else
render :template => quot;newquot;
end
end
44. 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
45. 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
46. Tradeoffs
Cons
Pros
Indirection
Free transaction semantics
Testing is a bit slower/
Skinny controller, fat
tougher
model
47. 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
48. 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
49. 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
53. 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
54. 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
55. 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
56. 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
57. 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
58. 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
The length of time doesn’t matter. It could be 3 weeks
Code might be unfactored because you weren’t good enough to see the refactorings earlier.
No tests - how do you have confidence?
You could have well-designed code with no tests... “clean room”...falcon circling the sky then strikes
Bad design
Dependencies (Rails makes you not care. Stuff like const_missing is great but hides pain points, association chains)
The more debt you have, the harder it is to adapt to changing requirements.
Systems become large, it’s important for them to be designed and architected such that you can reason about subsystems.
Eventually, programmers want the Big Rewrite. We’ve both advocated for the Big Rewrite on a project that hadn’t even launched yet.
*** Choosing Rails
  - Pros: we know them
  - 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
*** Choosing mediawiki
  - Pros: Easy to get the site up and running
  - Cons: Difficult to extend, difficult to scale
  - Result: Spent a year+ replacing it piece-by-piece with Rails
*** Using ActiveScaffold
  -  Pros: Get scaffolding quickly and easily
  - Cons: Internal code is a mess, untested, difficult to extend
*** Code you write
  - Not refactoring / writing tests
- Poorly tested code is almost as bad as not testing at all.
has_many =>  has_many :through  (clear migration path)
one database server => multiple database servers (requires community to create new tools. no clear migration path out of the box)
** Could spend six months designing the system so that it supports all the functionality and has extensibility points
*** We know that doesn't actually work
*** Plus you don't have working software
* Agile approach
  ** Do simple things to add value right now
  ** Technical debt is central to Agile development - embrace it
** TATFT
  - Testing allows you to refactor
  - Refactoring pays down debt
  - Virtuous cycle => Testing makes refactoring possible, refactoring makes testing easier
** RATFT
  - 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.
  - red/green/REFACTOR deploy
  - Get to green, take the time to make your code nice.  You should spend equal or more time refactoring than making your tests green.
AboutUs: 2-3 deployments per day. No staging.
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.
* You need to write tests, what do you do? Unit Tests or Acceptance Tests?
A. Acceptance (originally called functional)
Cuke:
- Uses full rails stack.
- Tests multiple requests in a single test.
- Hits multiple models and controllers, session, external services, etc.
* What's the point?
- 1 cucumber test covers the same amount of code as 25 unit tests
- Level of abstraction - reasoning about usage of the system, as opposed to one tiny little piece out of context
- Captures existing system functionality
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)
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.
** Pushback
- Writing tests is too hard.  No it's not
- No, really, it is too hard
** Silo code
- 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
- Don’t touch it after you silo it.
- Rails 3: mountable apps
ActiveSupport does a looooooot of stuff
** Use existing frameworks - resource_controller
** Write your own - Pivotal's authorization system (can_create, can_update auto-called from controller)
** Extraction is a very valid refactoring technique.
* How many actions have people written that were like this?
* Added one method (load_model)
* Got behavior for free
* Con isn’t really a con. New Ruby programmers won’t stay new for long
* If they learn this, they’ll start to write code this way
* I didn’t realize how easy that was until I did it from scratch
* It “just worked”
*This is considdered by most people as a “clean/skinny” controller.
Why refactor? So you can do this
We realize that legacy controllers aren’t even this clean.
We never said it would be easy.
Refactor your code, then you can make it sex
This is the goal
Transactions in the controller are an anti-pattern.
No explicit transactions
You can use a framework
Code is tougher to understand due to indirection
Transaction semantics without an explicit transaction
Account and project focused on domain responsibilities
AccountRegistration provides natural point for stuff like sending a verification email (also helps with testing)
AccountRegistration can get sophisticated without muddling model - validates_associated project and video, if you want
You could write a script to clean up AccountRegistration records when they’re no longer needed, depending on domain
* Problems with this test
** Magic number (california sales tax)
** coupled to TaxCalculator implementation
* Can use a mock or a simpler fake object
* Every Java programmer asks “what library to use for DI”
* Ruby programmers say “don’t use it”
* Misses the point. Don’t use a framework. Use Ruby
* Problem: this will break all clients
Learn the hooks (inherit, included, etc)
Understand how has_many works - it’s not magic!
This lets you be very creative and have fun
Working Effectively...gives you concrete strategies for getting a code base under test