You might “Are Gonna Need It”

Avoiding the MonoRail

Dave Copeland / @davetron5000

Bad

It will happen

Two MonoRail Stories

Lead Engineer @ Stitch Fix

We're avoiding a MonoRail

Former Engineer @ LivingSocial

We killed a MonoRail

Basic Strategies

Split existing application

Create new application

Extract services

Consequences

Can we prepare ourselves?

YAGNI

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

1

Make Provisioning & Deployment Easy

Provisioning & Deployment

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

Find a place to deploy

Deploy in one step

Once deployed

If all else fails…

…use Rails engines to keep yourself honest

2

Use Your Database

The Golden Path

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);"

Check constraints

3

Better-Factored Code

Not demonstrating Rails' features

Deliver value…

…through software…

…using Rails

Two steps to better-factored code

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

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

3.2

Not All Code Belongs to a Model

How Stitch Fix processes returns

What model does this go with?

KISS

Keep It Simple

(Stupid)

Consider Service Objects

class ReturnProcessor
  def self.process!(the_return,logged_in_user)
    # All that crazy logic
  end
end

Better

Advantages

This isn't YAGNI,

it's design decisions.

To Review

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