These are some notes I took while reading the book.
The notes are heavily modified to account for newer versions of Rails (the book was written for Rails 3 and I have updated most of the content to Rails 5). They also include a good amount of my personal opinion based on my experience as a developer.
Feel free to send me a pull request if you want to make an improvement.
You don't have to read everything in order.
The notes have been designed to be fully self-contained by chapter.
Step 1: Find the chapter where your problem is
If you are struggling with a particular topic (e.g Views), jump to that chapter; everything you need is there for you.
Step 2: Find a "Problem" title that describes your problem
ctrl+F
through the Problem:
looking for something that sounds like your problem. Each problem section is also self-contained and suggests at least one solution.
Consider the following case: In your e-commerce app Customers only have one Address and Customers can have many Invoices:
class Address < ActiveRecord::Base
belongs_to :customer
end
class Customer < ActiveRecord::Base
has_one :address
has_many :invoices
end
class Invoice < ActiveRecord::Base
belongs_to :customer
end
In several views you have this kind of code to show details of an invoice:
<%= @invoice.customer.name %>
<%= @invoice.customer.address.street %>
<%= @invoice.customer.address.city %>
<%= @invoice.customer.address.state %>
<%= @invoice.customer.address.zip_code %>
Breaking change: you are now asked to implement the concept of billing address and shipping address. This means that you need to change all of the views to account for that change.
Wrapper methods allow us to ask the direct neighbors for information. If you are asking for information that is not part of you direct neighbor's attributes, it is their responsibility to ask their direct neighbors for the information.
class Address < ActiveRecord::Base
belongs_to :customer
end
class Customer < ActiveRecord::Base
has_one :address
has_many :invoices
def street address.street end
def city address.city end
def state address.state end
def zip_code address.zip_code end
end
class Invoice < ActiveRecord::Base
belongs_to :customer
def customer_name customer.name end
def customer_street customer.street end
def customer_city customer.city end
def customer_state customer.state end
def customer_zip_code customer.zip_code end
end
You could change the view code to the following:
<%= @invoice.customer_name %>
<%= @invoice.customer_street %>
<%= @invoice.customer_city %>
<%= @invoice.customer_state %>
<%= @invoice.customer_zip_code %>
Cons of this approach: your public interface's code gets murky with many methods that are arguably non related to you class (e.g Invoice#customer_state)
Solution 2: Use delegate
Note: Delegation should only be used to talk do direct neighbors. See use wrapper methods for more information.
class Address < ActiveRecord::Base
belongs_to :customer
end
class Customer < ActiveRecord::Base
has_one :address
has_many :invoices
delegate :street, :city, :state, :zip_code, to: :address
end
class Invoice < ActiveRecord::Base
belongs_to :customer
delegate :name, :street, :city, :state, :zip_code, to: :customer, prefix: true
end
The view code remains the same as in solution 1.
<ul>
<% User.find(order: "last_name").each do |user| -%>
<li><%= user.last_name %> <%= user.first_name %></li>
<% end %>
</ul>
This is a problem because you are most likely repeating this code on all views that render a list of users.
Pushing down this to a controller is a bit of an improvement. However, most likely you are repeating the code on all controller actions that require a list of users. Additionally, if you want to change you default user ordering you want to be able to change it everywhere from a single place.
class UsersController < ApplicationController
def index
@users = User.order("last_name")
end
end
class User < ActiveRecord::Base
def self.ordered
order("last_name")
end
# OR AS A NAMED SCOPE
scope :ordered, -> { order("last_name") }
end
Consider the following code
class User < ActiveRecord::Base
has_many :memberships
def find_recent_active_memberships
memberships.where(:active => true).limit(5)
. order("last_active_on DESC")
end
end
In this case, find_recent_active_memberships
knows too much about what an active membership is and how are they supposed to be ordered. This makes User
know too much about the Membership
model.
After moving the scopes you must call the neighboring model's scope instead of repeating the query.
Here are 2 alternatives:
# Alternative 1
class User < ActiveRecord::Base
has_many :memberships
def find_recent_active_memberships
memberships.find_recently_active
end
end
class Membership < ActiveRecord::Base
belongs_to :user
def self.find_recently_active
where(:active => true).limit(5).order("last_active_on DESC")
end
end
# Alternative 2
class User < ActiveRecord::Base
has_many :memberships
def find_recent_active_memberships
memberships.only_active.order_by_activity.limit(5)
end
end
class Membership < ActiveRecord::Base
belongs_to :user
scope :only_active, where(:active => true)
scope :order_by_activity, order('last_active_on DESC')
end
Classes must have only one reason to change. If a class has more than one reason to change, that means it is breaking the single responsibility principle.
Consider the following example:
class Order < ActiveRecord::Base
def self.find_purchased ... end
def self.find_waiting_for_review ... end
def self.find_waiting_for_sign_off ... end
def self.advanced_search(fields, options = {}) ... end
def self.simple_search(terms) ... end
def to_xml ... end
def to_json ... end
def to_csv ... end
def to_pdf ... end
end
This order class has the responsibility of finding orders and also the responsibility of converting them to multiple formats, breaking the single responsibility principle.
Move all the conversion logic to it's own class and let the Order
class deal with order like logic only.
class OrderConverter
attr_reader :order
def initialize(order) @order = order end
def to_xml ... end
def to_json ... end
def to_csv ... end
def to_pdf ... end
end
Now you can compose the OrderConverter
object into the Order
object itself.
class Order < ActiveRecord::Base
# This is composition. Order is composed of OrderConverter
def converter OrderConverter.new(self) end
end
Note: The Rails association methods (for example, has_one, has_many, belongs_to) all create this sort of composition automatically for database-backed models.
At this point you can do @order.converter.to_pdf
. However, note that this breaks the Law of Demeter. Hence, we can use delegation to fix the problem.
class Order < ActiveRecord::Base
delegate :to_xml, :to_json, :to_csv, :to_pdf, to: :converter
def converter OrderConverter.new(self) end
end
The underlying idea is the same. However, we will use ActiveRecord to make the code a little less verbose.
We will use another example to illustrate this solution.
Problematic Code
class BankAccount < ActiveRecord::Base
validates :balance_in_cents, :presence => true
validates :currency, :presence => true
# Bank account logic...transfer, deposit, withdraw, open, close.
# Money-Like logic
def balance_in_other_currency(currency) currency exchange logic... end
def balance balance_in_cents / 100 end
def balance_equal?(other_bank_account)
balance_in_cents ==
other_bank_account.balance_in_other_currency(currency)
end
end
The problem here is that BankAccount
takes both the responsibility of being an account and also the responsibility of money (currency exchange, comparing against other money, etc).
composed_of
takes 3 main options: 1) name of the method that will reference the new object, 2) the name of the object's class, and the mapping of database columns to attributes of the object.
This is the composed_of
solution.
class BankAccount < ActiveRecord::Base
validates :balance_in_cents, :presence => true
validates :currency, :presence => true
composed_of :balance,
class_name: "Money",
mapping: [%w(balance_in_cents amount_in_cents),
%w(currency currency)]
end
# app/models/money.rb
class Money
include Comparable
attr_accessor :amount_in_cents, :currency
def initialize(amount_in_cents, currency)
self.amount_in_cents = amount_in_cents
self.currency = currency
end
def in_currency(other_currency) currency exchange logic... end
def amount amount_in_cents / 100 end
def <=>(other_money)
amount_in_cents <=> other_money.in_currency(currency).amount_in_cents
end
end
Now you can: @bank_account.balance.in_currency(:usd)
and @bank_account.balance > @other_bank_account.balance
.
Extracting the extra responsibility methods in a module that is only included in that class is NOT the way to go. This helps organizing the code a bit, and reduces visual complexity. However, the class still has more than one responsibility.
Here is some code to illustrate how this is done.
# This is the problematic code
class Order < ActiveRecord::Base
# Find Orders by State
def self.find_purchased ... end
def self.find_waiting_for_review ... end
def self.find_waiting_for_sign_off ... end
# General Order Searchers
def self.advanced_search(fields, options = {}) ... end
def self.simple_search(terms) ... end
# Order Converters
def to_xml ... end
def to_json ... end
def to_csv ... end
def to_pdf ... end
end
In the not-so-great solution we identify that all methods inside Order can be grouped in three buckets. We create modules for each bucket and the include/extend them inside order.
# This is the not-so-great solution code
class Order < ActiveRecord::Base
extend OrderStateFinders
extend OrderSearchers
include OrderExporters
end
# lib/order_state_finders.rb
# Omitted for simplicity
# lib/order_searchers.rb or in Rails 4+ could be somewhere in concerns
module OrderSearchers
def advanced_search(fields, options = {}) ... end
def simple_search(terms) ... end
end
# lib/order_exporters.rb or in Rails 4+ could be somewhere in concerns
module OrderExporters
def to_xml ... end
def to_json ... end
def to_csv ... end
def to_pdf ... end
end
Always try to favor composition over inheritance. Inclusion of modules is a type of inheritance.
(and the large transaction blocks that come with them)
Consider the following code:
class Account < ActiveRecord::Base
def create_account!(account_params, user_params)
transaction do
account = Account.create!(account_params)
first_user = User.new(user_params)
first_user.admin = true
first_user.save!
self.users << first_user
account.save!
Mailer.deliver_confirmation(first_user)
return account
end
end
end
There 2 main problems with this code:
- The Account class has the responsibility of manipulating other classes (
User
andMailer
) through thecreate_account!
method. (This is the main problem). - The
transaction
block can be avoided by using standard Rails validations and callbacks.
Note from the summarizer: I agree that this is an anti-pattern and that there are better ways for handling this type of code. However, I don't agree with the book's suggested solution. I include that solution, for the sake of completeness. On the controller chapter I discuss multiple approaches that are better suited to tackle this problem (even if you can't avoid the transaction block): Form Objects and Service Objects.
Proposed Solution: fixing the call to User
by using nested_attributes
and send email with a callback
Part 1: using nested_attributes
All the hand-rolled manipulation that we are doing to the User
model can be covered by Rails' nested_attributes
.
By adding accepts_nested_attributes_for :users
to Account
, the Account model will now be able to manipulate User
when Account#new, Account#create, Account#update_attributes
are called with a user_attributes
subhash.
This in turn means that we need to modify our view, to make the form send the information with the user_attributes
subhash in the proper format.
<%= form_for(@account) do |form| -%>
<%= form.label :name, 'Account name' %>
<%= form.text_field :name %>
<% fields_for :user, User.new do |user_form| -%>
<%= user_form.label :name, 'User name' %>
<%= user_form.text_field :name %>
<%= user_form.label :email %>
<%= user_form.text_field :email %>
<%= user_form.label :password %>
<%= user_form.password_field :password %>
<% end %>
<%= form.submit 'Create', :disable_with => 'Please wait...' %>
<% end %>
At this point your Account
class should look like this. Note that you still need to make the user an admin, hence the make_admin_user
callback.
class Account < ActiveRecord::Base
accepts_nested_attributes_for :users
before_create :make_admin_user
private
def make_admin_user
self.users.first.admin = true
end
# ...
end
Note from the summarizer: I personally think that
nested_attributes
just hides a bad object oriented programming practice. A proof of that is that we ended up implementing amake_admin_user
callback that manipulates the user from the account (And that is not even talking about the problems of callbacks themselves). A smarter approach could be using a form object pattern.
Part 2: Using an after_create
callback to send the email
The final proposed solution is the following:
class Account < ActiveRecord::Base
accepts_nested_attributes_for :users
before_create :make_admin_user
after_create :send_confirmation_email
private
def make_admin_user
self.users.first.admin = true
end
def send_confirmation_email
Mailer.confirmation(users.first).deliver
end
# ...
end
Note from the summarizer: I don't agree with sending the email through a callback. Callbacks give your classes a bunch of unexpected and very hard to debug side effects that you are better-off avoiding. Imagine you now have a face to face account creation process that does not need an email. Bad luck, with a callback every creation will send an email and you can't avoid that.
Note from the summarizer: The result of the proposed refactor is a very small Account class. In my opinion this is at the expense of hiding a lot of complexity behind
nested_attributes
and paying a high cost in committing to always send an email on creation (callback).
Consider the following code:
class PetsController < ApplicationController
def show
@pet = Pet.find(params[:id])
@toys = Toy.where(:pet_id => @pet.id, :cute => true)
end
end
This code has 2 problems:
- The controller is aware of the Toy implementation (it knows what columns does Toy have).
- The association from pet to toys is not being used.
90% of the times you would use this solution. Other solutions are shown below, but they are useful in very specific cases only.
class PetsController < ApplicationController
def show
@pet = Pet.find(params[:id])
@toys = @pet.toys.cute.paginate(params[:page])
end
end
class Toy < ActiveRecord::Base
scope :cute, -> { where(:cute => true) }
end
class Pet < ActiveRecord::Base
has_many :toys
end
Notice that:
- The
Toy
class is the one responsible for defining what#cute
means. - The controller leverages the association.
Rails allows us to "enrich" a declared association with methods by passing in a block OR extending a module. This gives access to the proxy owner (the original caller) inside the method.
Although this is rarely needed, is can be very handy under certain conditions. The following code shows an example when this type of "association enrichment" is the way to go.
class Toy < ActiveRecord::Base
# has column :minimum_age
end
class Pet < ActiveRecord::Base
# has column :age
# The association :toys is being enriched by passing a block.
has_many :toys do
def appropriate
where(["minimum_age < ?", proxy_owner.age])
end
end
end
This association enrichment now allows to do pet.toys.appropriate
. Note that .appropriate
makes use of the pet's age (the caller / proxy owner) inside the scope.
The syntax for association enrichment with modules is the following. A different and non-coherent example is used to illustrate the syntax:
module ToyAssocationMethods
def cute
where(:cute => true)
end
end
class Pet < ActiveRecord::Base
has_many :toys, :extend => ToyAssocationMethods
end
Consider this example for searching songs according to multiple parameters.
class Song < ActiveRecord::Base
def self.search(title, artist, genre, published, order, limit, page)
conditional_values = { title: "%#{title}%",
artist: "%#{artist}%",
genre: "%#{genre}%" }
case order
when "name" : order_clause = "name DESC"
when "length" : order_clause = "length ASC"
when "genre": order_clause = "genre DESC"
else
order_clause = "album DESC"
end
joins = []
conditions = []
conditions << "(title LIKE ':title')" unless title.blank?
conditions << "(artist LIKE ':artist')" unless artist.blank?
conditions << "(genre LIKE ':genre')" unless genre.blank?
unless published.blank?
conditions << "(published_on == :true OR published_on IS NOT NULL)"
end
find_opts = { conditions: [conditions.join(" AND "), condition_values],
joins: joins.join(' '),
limit: limit,
order: order_clause }
page = 1 if page.blank?
paginate(:all, find_opts.merge(:page => page, :per_page => 25))
end
end
This code is a huge mess:
- It implements multiple searches in one (e.g by title, by artist).
- It combines the responsibility of searching, with the responsibility of ordering and paginating.
- Paginating is NOT a responsibility of the model (normally the controller does this).
The solution does this:
- Abstracts repeated finders into a generic
matching
method. - Separates finders, ordering and limiting by implementing separate methods for each responsibility.
- Builds a robust
search
that is made up of simple components.
class Song < ActiveRecord::Base
scope :top, -> (number) { limit(number) }
scope :matching, -> (column, value) {
where(["#{column} like ?", "%#{value}%"])
}
scope :published, -> { where.not(published: nil) }
# Note from the summarizer:
# I am not a big fan of this implementation. However,
# the point being illustrated is to separate the ordering form the finding.
def self.order(col)
sql = case order
when "name" : order_clause = "name DESC"
when "length" : order_clause = "length ASC"
when "genre": order_clause = "genre DESC"
else order_clause = "album DESC"
end
order(sql)
end
def self.search(title, artist, genre, published)
finder = matching(:title, title)
finder = finder.matching(:artist, artist)
finder = finder.matching(:genre, genre)
finder = finder.published unless published.blank? return finder
end
end
# Callers can use the search method like this:
Song.search("fool", "billy", "rock", true).
order("length").
top(10).
paginate(:page => 1)
Full text search is a complicated problem period. For simple searches you may be able to survive with your database's LIKE
search functionality. However, once you start doing fuzzy search in multiple columns at a time, the code gets really ugly really fast.
Here is an example of a hand-rolled search functionality that became a mess:
class User < ActiveRecord::Base
def self.search(terms, page)
columns = %w( name login location country )
tokens = terms.split(/\s+/)
if tokens.empty?
conditions = nil
else
conditions = tokens.collect do |token|
columns.collect do |column|
"#{column} LIKE '%#{connection.quote(token)}%'"
end
end
conditions = conditions.flatten.join(" OR ")
end
paginate :conditions => conditions, :page => page
end
end
class UsersController < ApplicationController
def index
@users = User.search(params[:search], params[:page])
end
end
There are many full-text search engines with smooth integrations with rails. The selection of search engine depends on many factors that are specific to your application and infrastructure. That discussion is beyond the scope of this summary. However, here is a convenient list of many of the search engine integrations available for ruby.
Note: Postgres has a built-in full text search engine which may be good enough for you application. Before adding a new dependency, take a look at the Postgres engine to check if it suits your needs.
The book shows an example on how to integrate with Sphinx using Thinking Sphinx. I will omit this explanation as it would be obsolete for any other engine that is not Sphinx.
Consider this example: a Car
and a Bicycle
class have the exact same implementation of all methods that make them drivable.
class Car << ActiveRecord::Base
validates :direction, presence: true
validates :speed, presence: true
def turn(new_direction) self.direction = new_direction end
def brake self.speed = 0 end
def accelerate self.speed = speed + 10 end
# Other, car-related activities...
end
class Bicycle << ActiveRecord::Base
validates :direction, presence: true
validates :speed, presence: true
def turn(new_direction) self.direction = new_direction end
def brake self.speed = 0 end
def accelerate self.speed = speed + 10 end
# Other, bike-related activities...
end
Note that this code repeats the implementation for turn
, brake
and accelerate
; as well as the validations.
We can abstract the concept by thinking that both Cars and Bicycles are Drivable. With this in mind, we can extract all the behavior that makes something Drivable
into a module.
# lib/drivable.rb
module Drivable
extend ActiveSupport::Concern
included do
validates :direction, presence: true
validates :speed, presence: true
end
def turn(new_direction) self.direction = new_direction end
def brake self.speed = 0 end
def accelerate self.speed = speed + 10 end
end
class Car << ActiveRecord::Base
include Drivable
# Other, car-related activities...
end
class Bicycle << ActiveRecord::Base
include Drivable
# Other, bike-related activities...
end
This could also be achieved by implementing a superclass. However, most of the times it is preferable to use modules instead of super classes, as modules are more flexible and allow for sharing of multiple types of behavior without most of the problems that come with multiple inheritance. (e.g a Car may be both
Drivable
andBookable
).
Behavior in the 2 classes only changes slightly
Go back to the previous example but now imagine we had to add the following constraints:
Cars
have a top speed of 100 mph and accelerate at a rate of 10 mph.Bicycles
have a top speed of 20 mph and accelerate at a rate of 1 mph.
The rest of the behavior that makes them Drivable
stays the same.
In a NON-DRY implementation, the code would look like this:
class Car << ActiveRecord::Base
validates :direction, presence: true
validates :speed, presence: true
def turn(new_direction) self.direction = new_direction end
def brake self.speed = 0 end
def accelerate self.speed = [speed + 10, 100].min end
# Other, car-related activities...
end
class Bicycle << ActiveRecord::Base
validates :direction, presence: true
validates :speed, presence: true
def turn(new_direction) self.direction = new_direction end
def brake self.speed = 0 end
def accelerate self.speed = [speed + 1, 20].min end
# Other, bike-related activities...
end
Solution: Extract shared behavior into a Drivable
module AND customize particular behavior through the template pattern
The solution is very similar to the previous solution. However, now all Drivable
classes have the responsibility to define their top speed and acceleration through methods.
Note that in the following code the Drivable
module enforces all implementers to define their own #top_speed
and #acceleration
methods to be able to #accelerate
. Drivable
however, helps a bit by raising informative exceptions in case a developer forgets to implement any of these.
# lib/drivable.rb
module Drivable
extend ActiveSupport::Concern
included do
validates :direction, presence: true
validates :speed, presence: true
end
def turn(new_direction) self.direction = new_direction end
def brake self.speed = 0 end
def accelerate self.speed = [speed + acceleration, top_speed].min end
def top_speed
raise TemplateError, "The Drivable module requires the " +
"included class to define a " +
"top_speed method"
end
def acceleration
raise TemplateError, "The Drivable module requires the " +
"included class to define an " +
"acceleration method"
end
end
class Car << ActiveRecord::Base
include Drivable
# Implementation of template methods
def top_speed 100 end
def acceleration 10 end
# Other, car-related activities...
end
class Bicycle << ActiveRecord::Base
include Drivable
# Implementation of template methods
def top_speed 20 end
def acceleration 1 end
# Other, bike-related activities...
end
Why use methods instead of constants?
- Methods provide flexibility: for a case of an amphibian vehicle, the top_speed and acceleration may change if the vehicle is currently on water or land. Methods provide the flexibility for such implementation.
- Easiness of testing: it is more natural to test methods than constants and it is easier to stub methods than to stub constants.
- We can raise helpful error messages through methods.
Imagine Drivable
is something you would like to share among multiple applications. Well... copying and pasting the module is clearly a bad solution, so what should you do?
Do this ONLY if you really need to share the behavior on multiple apps. Otherwise, keep the code inside the app.
It is commonly known as a good practice to EXTRACT the gem from an app. This means that first you re-factor and abstract everything inside the app and make sure it works before moving it out to a gem.
Rails used to have a thing called plugins. This is now deprecated and gems (and gemified plugins) are now the way to go.
Explanations on how to test and create a gem are outside the scope of this summary.
1.4.4 Problem: Your models contain some sort of "status" column and the code for dealing with that is very repetitive
Consider the following code:
class Purchase < ApplicationRecord
validates_presence_of :status
validates_inclusion_of :status, in: %w(in_progress submitted approved shipped received canceled)
# Status Finders
scope :all_in_progress, -> { where(status: 'in_progress') }
scope :all_submitted, -> { where(status: 'submitted') }
scope :all_approved, -> { where(status: 'approved') }
# ... all other finders
# Status Accessors
def in_progress? status == 'in_progress' end
def submitted? status == 'submitted' end
def approved? status == 'approved' end
# ... all other accessors
end
The problem with this code is that there is no single authoritative place to hold the statuses of a Purchase
. For example, if we were asked to split the shipped status to partially_shipped and fully_shipped, we would need to do a change on 3 different places: Validations, Finders and Accessors.
Solution 1 (non ideal): use in-class meta-programming to dynamically define validations, finders and accessors
This solution is included for educational purposes only as Rails already provides a feature to solve this very frequent problem.
class Purchase < ApplicationRecord
# Single authoritative source of statuses for this class
STATUSES = %w(in_progress submitted approved shipped received)
validates_presence_of :status
validates_inclusion_of :status, :in => STATUSES
# Dynamic definition of finders
class << self
STATUSES.each do |status_name|
define_method "all_#{status_name}"
where(:status => status_name)
end
end
end
# Dynamic definition of accessors
STATUSES.each do |status_name|
define_method "all_#{status_name}?"
where(:status => status_name)
end
end
end
The drawbacks of this approach are:
- The code gets a obscure.
- The whole idea of "statuses" could be abstracted for use on multiple classes. This approach just solves the problem for the
Purchase
class.
Solution 2 (still non ideal): abstract meta-programming for "status" handling into ApplicationRecord
This solution is shown to illustrate how
ApplicationRecord
can be used to give additional functionality to all your models or to extendActiveRecord
. The solution shown here is the equivalent of a hand-rolled version of Rails'enum
feature. However, the underlying knowledge of extension by usingApplicationRecord
may be useful on other circumstances.
# Step 1: Abstract the functionality to an extension module
# lib/extensions/statuses.rb
module Extensions
module Statuses
extend ActiveSupport::Concern
class_methods do
def has_statuses(*status_names)
validates :status, presence: true, inclusion: { in: status_names }
# Dynamic definition of finders
status_names.each do |status_name|
scope "all_#{status_name}", -> { where(status: status_name) }
end
# Dynamic definition of accessors
status_names.each do |status_name|
define_method "#{status_name}?" do
status == status_name
end
end
end
end
end
end
# Step 2: Include the extension into ApplicationRecord
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include Extensions::Statuses
# ... inclusion of other extension modules
end
# Step 3: Use the has_statuses method on models that have statutes and let
# the meta-programming magic do the work.
class Purchase < ApplicationRecord
has_statuses :in_progress, :submitted, :approved, :shipped,
end
# Now you can do things like Purchase.all_in_progress OR Purchase.last.in_progress?
Rails has a feature called Enums that was made for this exact reason.
class Purchase < ApplicationRecord
enum status: [:in_progress, :submitted, :approved, :shipped]
end
# The enum defines many convenience methods for you
purchase.in_progress!
purchase.in_progress? # => true
purchase.status # => "in_progress"
# It also defines finders/scopes
Purchase.in_progress # => Finds all purchases with status in_progress
1.4.bonus Problem: My models have statuses/states and modeling how the states transition is driving me crazy
Having a pre-defined set of "pathways" for states/statutes is a very common feature associated with states. For example, an order can only be returned if it was delivered (i.e it does not make sense to return an order that hasn't been shipped).
Hand, rolling this code often gets out of control easily.
Ruby has multiple libraries for state machines that are very useful to determine lawful (and hence unlawful) transitions of states. Here is a list of some of the most popular ones: