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.
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
3 Favorites
Andrea Polci, Software Developer at Engineering Ingegneria Informatica S.p.A., favorited this 1 month ago
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
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 \"works when updated by owner\" do
describe BoatsController do
before(:each) do do_update @owner
@owner = User.create! @boat.reload.name.should == \"SS Minnow\"
@boat = Boat.create! end
:name => \"Pequod\",
:owner => @owner it \"works when updated by admin\" do
end admin = User.create! :admin => true
do_update admin
def do_update(user) @boat.reload.name.should == \"SS Minnow\"
login_as user end
put :update,
:id => @boat.to_param,
it \"fails when updated by non-owner\" do
:boat =>
nonowner = User.create!
{:name => \"SS Minnow\"}
do_update nonowner
end
@boat.reload.name.should == \"Pequod\"
end
The model
class Boat < ActiveRecord::Base
belongs_to :owner, :class_name => \"User\"
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 => \"edit\"
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 => \"edit\"
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(\"can_#{action_name}?\", 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(\"can_#{action_name}?\", 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 => \"edit\"
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, \"POST create\" do
def do_post
post :create, :account => {:name => \"Nike\"}
end
it \"should create a project for the account\" 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 => \"#{@account.name}'s First Project\"
redirect_to @account
else
render :template => \"new\"
end
end
end
Using Callbacks
class Account < ActiveRecord::Base
has_many :projects
after_create :create_project
def create_project
projects.create! :name => \"#{name}'s First Project\"
end
end
Simpler Controller
def create
@account = Account.new params[:account]
if @account.save
redirect_to @account
else
render :template => \"new\"
end
end
A little more
describe AccountsController, \"POST create\" do
def do_post
post :create, :account => {:name => \"Nike\"}
end
it \"should create a project for the account\" do
do_post
Account.first.should have(1).project
end
it \"should create a video for the project\" 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 => \"#{account.name}'s First Video\"
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 => \"#{@name}'s First Project\"
project.videos.create! :name => \"#{@name}'s First Video\"
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 => \"new\"
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 => \"#{@name}'s First Project\"
project.videos.create!
:name => \"#{@name}'s First Video\"
end
end
SEAMS
Seams
Modify or sense behavior of code without changing it
OO - polymorphism
Dependency management
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, \"calculating tax\" do
it \"should add tax onto the total\" do
o = Order.new \"CA\"
bacon = Item.new \"Chunky bacon\", 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, \"calculating tax\" do
it \"should add tax onto the total\" do
fake_calculator = mock('calculator')
fake_calculator.should_receive(:calculate).
with(42, \"CA\").and_return 3.26
o = Order.new \"CA\"
bacon = Item.new \"Chunky bacon\", 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, \"calculating tax\" do
it \"should add tax onto the total\" do
fake_calculator = mock('calculator')
fake_calculator.should_receive(:calculate).
with(42, \"CA\").and_return 3.26
o = Order.new \"CA\"
bacon = Item.new \"Chunky bacon\", 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, \"calculating tax\" do
it \"should add tax onto the total\" do
fake_calculator = mock('calculator')
fake_calculator.should_receive(:calculate).
with(42, \"CA\").and_return 3.26
o = Order.new \"CA\"
bacon = Item.new \"Chunky bacon\", 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, \"calculating tax\" do
it \"should add tax onto the total\" do
o = Order.new \"CA\"
bacon = Item.new \"Chunky bacon\", 42
o.add_item bacon, 1
TaxCalculator.should_receive(:calculate).
with(42, \"CA\").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
0 comments
Post a comment