Restructuring Rails Eric Marthinsen CTO and Founder, Agile Commerce A Refactoring Story
Restructuring Rails
Eric MarthinsenCTO and Founder, Agile Commerce
A Refactoring Story
Overview• CTO and Founder of Agile
Commerce.
• Part of team that bought Sortfolio from 37signals in 2012.
• Have been enhancing it for about a year.
How wastheir code?
Rule
Stable code running a profitable company is
always successful code.
However• Sortfolio was a successful MVP.
• 37signals’ testing philosophy differs from my own.
• Rails 2.3.8
• Needed some work to prepare for new features.
Refactoring Result• Improved from a CodeClimate
score of 2.78 to 3.51.
• Drastically improved test coverage, while decreasing test run time.
• Doubled, roughly, our development velocity.
Today’s Objective
Share the principles I used to improve the Sortfolio codebase in a way that you can apply these principles to your own projects.
Reduce callbacks
Embrace CSS3
Inline concerns
Add decorators
STI
Create services
Create gateways
Custom presenters
DRY
Config to ENV
ElasticSearch
Method extraction
Fewer static methods
Extract class
Extract Class
Benefits• Better domain model
• Easier comprehension
•More granular tests
• Faster tests
•More hooks for mocking
•Overall, better software
My ThesisI believe that having classes with too many responsibilities is the primary cause of poor object-oriented code.
This is true of any object-oriented platform, but doubly true of Rails.
Why Doubly True?• ActiveRecord combines data and
business layers.
• View code often leaks into models and controllers.
• Helpers are dumping grounds for procedural code.
• Rails conventions support overburdened objects.
“Fat model,skinny controller”
Wrong - everything should be skinny.
Example: Search
Step 0: Startclass Listing < ActiveRecord::Base # associations # validations
def self.get_pro_listings(location_id, budget_id, start, offset) # lots of code end
def self.get_free_listings(location_id, budget_id, start, offset) # lots of similar code end
end
Object-Orientation• An object is data and the methods
that operate on that data.
• An object should represent a single thing.
• A class defines the type of the object.
Problems•Why does a listing know how to
find its peers?
•We’re attaching collection methods to a type declaration.
• A domain concept is being hidden.
Step 1class Listing < ActiveRecord::Base # associations # validationsend
class ListingIndex def self.get_pro_listings(location_id, budget_id, start, offset) # lots of code end
def self.get_free_listings(location_id, budget_id, start, offset) # lots of similar code endend
Progress• Now expressing the collection of
Listings in the domain model.
• Creating a “preferred” interface.
• But, not DRY, and two domain concepts remain unexpressed.
Step 2class Listing < ActiveRecord::Base # associations # validationsend
class ListingIndex def self.get_listings(is_pro, location_id, budget_id, start, offset) # lots of code endend
Progress• ListingIndex is now DRY
• Still a couple domain concepts hidden in there.
• Adding arguments is not scalable.
Step 3class Listing < ActiveRecord::Base # associations # validationsend
class ListingIndex def self.search(query) # less code endend
class ListingQuery def initialize(options) # set properties endend
Progress•We’ve teased out another domain
concept.
•Queries are now scalable and easy to augment.
• ListingIndex has an intuitive #search method.
• Still one domain concept hiding in there.
Step 4class ListingIndex def self.search(query) # less code endend
class ListingQuery def initialize(options) # set properties endend
class ListingResults # mostly attrsend
Progress• Results are now expressed in the
domain model.
• Big improvements to other areas of the code.
• Set up for success and making big changes behind a stable interface.
Step 5: Finalclass ListingIndex def self.search(query) # less code end
def self.add_listing(listing) # trivial end
def self.remove_listing(listing) # trivial endend
class ListingQuery # no changeend
class ListingResults # mostly attrsend
Results• Listing was split into Listing,
ListingIndex, ListingQuery, and ListingResults.
• Better design allowed for trivial introduction of ElasticSearch.
•Much easier and faster to test.
Takeaways
Your app probably has fewer classes
than it should.
It’s easier to combine classes than to separate
them.
Models don’t need to inherit from
ActiveRecord::Base
Conventions don’t always apply.
More InfoEric Marthinsen
www.agilecommerce.com/blog
@agilecommerce
@ericmarthinsen
THANKS!