You might “Are Gonna Need It”
Avoiding the MonoRail
Dave Copeland / @davetron5000
Bad
- Slow to change
- Increased failures
Lead Engineer @ Stitch Fix
We're avoiding a MonoRail
Former Engineer @ LivingSocial
We killed a MonoRail
Split existing application
Consequences
- One database, many apps
- DB queries become HTTP calls
- Records lose “activeness”
Can we prepare ourselves?
Always implement things when you actually need them, never when you just foresee that you need them.
YAGNI is about features
not design decisions
How do we approach our design decisions?
Design Decisions
- Make provisioning and deployment easy
- Use the database
- Better-factored code
1
Make Provisioning & Deployment Easy
Provisioning & Deployment
- Minimum Deployable Application
- Find a place to deploy
- Deploy in one step
Minimum Deployable Application
> rails new your_app
Generates a generic rails app
> rails new your_app --builder=http://dev.yourcompany.com/service_builder.txt
Generates an app tailor-made for YOUR team
Deploy in one step
- Capistrano
- git hooks
- script it yourself
Once deployed
- Verify configuration
- Re-deploy if needed
- Then add features
If all else fails…
…use Rails engines to keep yourself honest
The Golden Path
- No other apps touch the database
- Validations ensure data integrity
def up
create_table :orders do |t|
t.belongs_to :customer
t.string :name
t.integer :order_type
t.date :shipped_on
t.timestamps
end
end
create_table :orders do |t|
t.belongs_to :customer, null: false
t.string :name, null: true
t.integer :order_type, null: false
t.date :shipped_on, null: true
t.timestamps
end
execute "ALTER table things
ADD FOREIGN KEY
(customer_id) REFERENCES customer(id);"
create_table :orders do |t|
t.belongs_to :customer, null: false
t.string :name, null: true
t.integer :order_type, null: false
t.date :shipped_on, null: true
t.timestamps
end
execute "ALTER table things
ADD FOREIGN KEY
(customer_id) REFERENCES customer(id);"
create_table :orders do |t|
t.belongs_to :customer, null: false
t.string :name, null: true
t.integer :order_type, null: false
t.date :shipped_on, null: true
t.timestamps
end
execute "ALTER table things
ADD FOREIGN KEY
(customer_id) REFERENCES customer(id);"
Not demonstrating Rails' features
Deliver value…
…through software…
…using Rails
Two steps to better-factored code
- Not everything is an ActiveRecord object
- Not all code belongs on a model
3.1
Not everything is an ActiveRecord object
<h1>Purchase <%= @purchase.id %></h1>
<h2>for <%= @purchase.customer.name %></h2>
<dl>
<dt>Item</dt>
<dd><%= @purchase.item.description %></dd>
<dt>Price</dt>
<dd><%= @purchase.price %></dd>
</dl>
Bad
- Changes to purchase, customer, and item all cause this to change
- If data comes from API call, this gets messy
class PurchasePresenter
def initialize(purchase)
@purchase = purchase
@customer = purchase.customer
@item = purchase.item
end
def customer_name; @customer.name; end
def purchase_price; @purchase.price; end
def purchase_id; @purchase.id; end
def item_description; @item.description; end
end
Just kicking the can down the road
class PurchaseRecord
extend ActiveModel::Naming
include ActiveModel::Conversion
def persisted?; true; end
attr_reader :customer_name,
:purchase_price,
:purchase_id,
:item_description
def initialize(attribtues={})
@customer_name = attributes[:customer_name]
@purchase_price = attributes[:purchase_price]
@purchase_id = attributes[:purchase_id]
@item_description = attributes[:item_description]
end
end
class PurchaseRecord
extend ActiveModel::Naming
include ActiveModel::Conversion
def persisted?; true; end
attr_reader :customer_name,
:purchase_price,
:purchase_id,
:item_description
def initialize(attribtues={})
@customer_name = attributes[:customer_name]
@purchase_price = attributes[:purchase_price]
@purchase_id = attributes[:purchase_id]
@item_description = attributes[:item_description]
end
end
class PurchaseRecord
extend ActiveModel::Naming
include ActiveModel::Conversion
def persisted?; true; end
attr_reader :customer_name,
:purchase_price,
:purchase_id,
:item_description
def initialize(attribtues={})
@customer_name = attributes[:customer_name]
@purchase_price = attributes[:purchase_price]
@purchase_id = attributes[:purchase_id]
@item_description = attributes[:item_description]
end
end
class PurchaseRecord
extend ActiveModel::Naming
include ActiveModel::Conversion
def persisted?; true; end
attr_reader :customer_name,
:purchase_price,
:purchase_id,
:item_description
def initialize(attribtues={})
@customer_name = attributes[:customer_name]
@purchase_price = attributes[:purchase_price]
@purchase_id = attributes[:purchase_id]
@item_description = attributes[:item_description]
end
end
class PurchaseRecord
def self.from_purchase(purchase)
self.new(customer_name: purchase.customer.name,
purchase_price: purchase.price,
purchase_id: purchase.id,
item_description: purchase.item.description)
end
end
class PurchaseRecordController
private
def from_purchase(purchase)
self.new(customer_name: purchase.customer.name,
purchase_price: purchase.price,
purchase_id: purchase.id,
item_description: purchase.item.description)
end
end
Better
- View has fewer reasons to change
- Our data structure has fewer reasons to change
- Reasons to change localized to factory method
- Can be replaced by
OpenStruct
3.2
Not All Code Belongs to a Model
How Stitch Fix processes returns
- Shipment is marked as received
- Check if user has paid for kept items
- Charge customer if they haven't paid
- Check if unpaid items were returned
- Record status of each item - sold, recieved, damanged
- Email customer service any outliers
What model does this go with?
Return
? Why would it know about charging customers?
Shipment
? Why would it know emailing customer service?
Customer
? Why would it know about item statuses?
- A series of observers, hooks, callbacks and delegators?
class ReturnProcessor
def self.process!(the_return,logged_in_user)
# All that crazy logic
end
end
Better
- Logic around business process is in one place
- Tests to assert things are working are in one place
- Can be extracted with few code changes
Advantages
- ActiveRecord objects change infrequently and be shared or copied
- Service Objects can be shared in gems
- Extracting REST-like services is straightforward
This isn't YAGNI,
it's design decisions.
Make setting up a new app dead simple
Use your database to enforce data integrity
Use structs/plain models & Service Objects to organize your codebase
The End
@davetron5000
http://www.naildrivin5.com
Come Work for Us
http://www.stitchfix.com
http://stitchfixjobs.com