This page describes a new way to think about web application development, specifically for Ruby on Rails applications. To do this properly, I’ll need to provide a little bit of motivation, and present more than a trivially small code snippet. Bear with me, I think you’ll find that it’s worth it. The pattern I’m introducing here has been used in production on the WegoWise codebase for the past 5 years, with an existing team of many members.
When we talk about writing “good” code, or reference code “quality”, what exactly do we mean? A thorough discussion of this topic would need to delve into the idea of software development as an art form, where quality and aesthetics go hand-in-hand, and where the interplay between these notions guides our cognitive processes. For now, though, I’ll put forward something more practical and suggest that code quality is measured by the following criteria:
Note that while readability and understandability are related, they are not the same. A code may be structured in a manner that makes it easy to understand but challenging to read, or vice versa.
So how do we know if we are meeting our goals for code quality? The better your code meets the five criteria above, the more strongly you’ll be able to answer the following three questions in the affirmative:
Below, I outline an example application written using common Rails idioms, describe common anti-patterns, and work through how to refactor the code to improve its quality. I’ll first apply some well-known approaches, then show how the context pattern improves things further.
Suppose we’ve built a web app to help people adopt rabbits in need of homes.
The relevant models for us in this application are User
, Rabbit
, Adoption
,
AdoptionCenter
, Coupon
, and Referral
. An Adoption
associates a User
and Rabbit
together, and every Rabbit
belongs to an AdoptionCenter
. We’re going to look
specifically at the RabbitAdoptionController#create
action. Here’s what happens
when a request hits this endpoint:
User
from the params. This is done in an ApplicationController
because it’s relevant across endpoints.Rabbit
being adopted from the paramsAdoption
record in the databaseReferral
with the
Adoption
AdoptionCenter
class RabbitAdoptionController < ApplicationController
def create
@rabbit = Rabbit.find(id: params[:id])
@adoption_center = @rabbit.adoption_center
if !@rabbit.spayed_or_neutered?
redirect_to new_spay_neuter_path(@rabbit) and return
end
@fee = @rabbit.adoption_fee
if params[:coupon_code]
@coupon = Coupon.find(params[:coupon_code])
@fee -= @coupon.amount
end
if params[:referral_hash]
@referral = Referral.find_by(hash: params[:referral_hash])
end
@adoption = Adoption.new(rabbit: @rabbit, user: @current_user, fee: @fee)
Adoption.transaction do
@adoption.save!
@referral.update!(adoption_id: @adoption.id) if @referral
end
rescue ActiveRecord::RecordInvalid
flash[:error] = "Something went wrong"
render action: :edit
end
end
class ApplicationController < ActionController::Base
before_action :get_current_user
before_action :redirect_unless_logged_in
def get_current_user
@current_user = User.find_by(id: session[:user_id])
end
def redirect_unless_logged_in
redirect_to new_session_path unless @current_user
end
end
class Adoption < ActiveRecord::Base
belongs_to :rabbit
belongs_to :user
after_create :adopt_rabbit
def adopt_rabbit
rabbit.adopt!
end
end
class AdoptionCenter < ActiveRecord::Base
def display_name
# `name` and `is_volunteer_foster_home` are both attributes of this model
return name unless is_volunteer_foster_home?
"Foster volunteer: #{name}"
end
end
class Rabbit < ActiveRecord::Base
belongs_to :adoption_center
def adopt!
update!(adopted: true)
adoption_center.increment!(:adoption_count)
end
end
module RabbitAdoptionHelper
def adoption_center_address
# `verified` is an attribute of the User model
if @adoption_center.is_volunteer_foster_home? && !@current_user.verified?
"#{@adoption_center.display_name}<br>"\
"#{@adoption_center.city}, #{@adoption_center.state}"
else
"#{@adoption_center.display_name}<br>"\
"#{@adoption_center.street_address}<br>"\
"#{@adoption_center.city}, #{@adoption_center.state}"
end
end
end
<p>Congratulations, <%= @current_user.name %>! You've adopted a cute little
bunny in need, named <%= @rabbit.name %>. The bunny is located at:</p>
<p><%= adoption_center_address %></p>
<p>
The adoption fee is $<%= fee %>.
<% if @coupon %>
(This includes a discount of $<%= @coupon.amount %> from your coupon)
<% end %>
You'll need to pay this when you go to pick up the rabbit.
</p>
<p>You will get an email with more information. Thanks!</p>
The code in the previous section doesn’t do anything particularly out of the ordinary as far as Rails conventions go, but it is full of anti-patterns. These code smells, when applied to a much larger and more complex codebase, become very challenging to deal with.
Controllers are meant to direct requests with relatively minimal logic. In our example, the controller is a full 18 lines of code for a single action, and is instantiating numerous different objects. How easy was it to follow all the different things that the controller action did? Controllers with this much logic are neither understandable nor readable.
Rails helpers are modules that are automatically made available to views based on naming conventions. This means the functions they contain do not have explicit receivers, making them annoying to test. Relying on the helpers for view logic commonly leads to taking shortcuts where object models are discarded, and procedural convenience methods are written instead. This is what is happening in our example.
We all know that modular abstractions with a clear separation of concerns makes
code easier to test and maintain. In our example, we are clearly violating this
goal in both our Adoption
and Rabbit
models.
Well-written code reflects clear thought, and packing our domain models with
superfluous methods results in “junk-drawer” code that pollutes our
abstractions. The more we cram into our notion of a particular object, the
harder it is to understand what actually defines that object. This leads to a
snowball effect of increasing complexity over time. In our example, the
AdoptionCenter#display_name
method exemplifies this code smell.
This is so common and uncontroversial that you’re probably surprised to see it listed. But instance variables in views make your code hard to test, hard to refactor, and prone to bugs. How do you know where among your controller stack and helper modules an instance variable is defined? More importantly, instance variables can be referenced even if they’re never set. This can lead to silent failures, challenges when debugging nil values, and difficulties understanding intent when refactoring code.
There are a number of well known approaches we have available to us. In
particular, we can make use of service objects and presenters to resolve a few
of our code smells. Adding an AdoptionCreationService
object can help us
enforce separation of concerns, and an AdoptionCenterPresenter
can help us take
view-specific logic out of the helper and models.
Let’s look at the relevant code after we’ve made these changes. The methods
previously in the Adoption
and AdoptionCenter
models have been removed, so
those aren’t shown. The view is also unchanged from earlier so it isn’t
repeated.
class RabbitAdoptionController < ApplicationController
def create
@rabbit = Rabbit.find(id: params[:id])
@adoption_center = @rabbit.adoption_center
if !@rabbit.spayed_or_neutered?
redirect_to new_spay_neuter_path(@rabbit) and return
end
if params[:coupon_code]
@coupon = Coupon.find(params[:coupon_code])
end
if params[:referral_hash]
@referral = Referral.find_by(hash: params[:referral_hash])
end
creator = AdoptionCreationService.new(
rabbit: @rabbit,
user: @current_user,
coupon: @coupon,
referral: @referral
)
created = creator.execute
@fee = creator.fee
if !created
flash[:error] = "Something went wrong"
render action: :edit
end
end
class ApplicationController < ActionController::Base
before_action :get_current_user
before_action :redirect_unless_logged_in
def get_current_user
@user = User.find_by(id: session[:user_id])
end
def redirect_unless_logged_in
redirect_to new_session_path unless @user
end
end
class AdoptionCreationService
def initialize(rabbit:, user:, coupon:, referral:)
@rabbit = rabbit
@user = user
@coupon = coupon
@referral = referral
end
def adoption
@adoption ||= Adoption.new(rabbit: @rabbit, user: @user, fee: fee)
end
def execute
Adoption.transaction do
adoption.save!
@rabbit.adopt!
@rabbit.adoption_center.increment!(:adoption_count)
@referral.update!(adoption_id: adoption.id) if @referral
end
true
rescue ActiveRecord::RecordInvalid
false
end
def fee
@fee ||= @rabbit.adoption_fee - @coupon.try(:amount)
end
end
class AdoptionCenterPresenter
def initialize(adoption_center:, user:)
@adoption_center = adoption_center
@user = user
end
def address
if hide_street_address?
"#{name}<br>#{@adoption_center.city}, #{@adoption_center.state}"
else
"#{name}<br>"\
"#{@adoption_center.street_address}<br>"\
"#{@adoption_center.city}, #{@adoption_center.state}"
end
end
private
def name
return @adoption_center.name unless is_volunteer_foster_home?
"Foster volunteer: #{@adoption_center.name}"
end
def hide_street_address?
@adoption_center.is_volunteer_foster_home? && !@user.verified?
end
end
class Rabbit < ActiveRecord::Base
def adopt!
update!(adopted: true)
touch(:adopted_at)
end
end
module RabbitAdoptionHelper
def adoption_center_address
AdoptionCenterPresenter.new(
adoption_center: @adoption_center,
user: @user
).address
end
end
Our “after” code is certainly an improvement from where we started. But the controller doesn’t seem any less complex than before. In fact, it’s even harder to understand and read now. Our helper module is simpler, but it’s still doing non-trivial work by instantiating the presenter and calling a method on it. And we’re still relying on instance variables to communicate among controllers, helpers and views.
Fundamentally, something is missing from the way we’re organizing our code. Let’s consider what we would ideally want from our object categories:
Here’s what’s missing from the above that leads to our code smells: Where do we do the work of figuring out what to do from our params? Where do we instantiate our service objects and presenters? Interpret results of methods called on those objects? How do we communicate information to our views in robust ways?
Context objects provide the missing piece. Specifically:
A Context Object is responsible for interpreting the current state of the request, providing the context for a controller to do its work, and defining an interface that may be referenced by views. Every request has exactly one context object associated with it. This context is built up throughout the life cycle of a request.
To illustrate how this works, I’ll start by showing what our code example looks like using the context pattern. This code requires adding the the context-pattern gem to our gemfile:
gem 'context-pattern'
In our new code, the models, service object and presenter are the same as before and our helper module is now empty. The controllers and view have changed, and now we have two context object classes. Here is the relevant new/ modified code:
class RabbitAdoptionController < ApplicationController
def create
extend_context :RabbitAdoptionCreate
if !rabbit.spayed_or_neutered?
redirect_to new_spay_neuter_path(rabbit) and return
end
create_adoption
if !successful_adoption?
flash[:error] = "Something went wrong"
render action: :edit
end
end
end
class ApplicationController < ActionController::Base
include Context::Controller
helper Context::BaseContextHelper
before_action :set_application_context
before_action :redirect_unless_logged_in
def redirect_unless_logged_in
redirect_to new_session_path unless logged_in?
end
def set_application_context
extend_context :Application, params: params, session: session
end
end
class ApplicationContext < Context::BaseContext
view_helpers :current_user
attr_accessor :session, :params
def current_user
User.find_by(id: session[:user_id])
end
memoize :user
def logged_in?
current_user.present?
end
end
class RabbitAdoptionCreateContext < Context::BaseContext
view_helpers :adoption_center_address,
:coupon_amount,
:fee,
:rabbit
delegate :fee, to: :adoption_creator
def adoption_center_address
adoption_center.address
end
def create_adoption
@success = adoption_creator.execute
end
def coupon_amount
coupon.amount if coupon
end
def rabbit
Rabbit.find(id: params[:id])
end
memoize :rabbit
def successful_adoption?
@success == true
end
private
def adoption_center
AdoptionCenterPresenter.new(rabbit.adoption_center)
end
memoize :adoption_center
def adoption_creator
AdoptionCreator.new(
rabbit: rabbit,
user: user,
fee: fee,
referral: referral
)
end
memoize :adoption_creator
def coupon
Coupon.find(params[:coupon_code]) if params[:coupon_code]
end
memoize :coupon
def referral
Referral.find_by(hash: params[:referral_hash]) if params[:referral_hash]
end
memoize :referral
end
<p>Congratulations, <%= current_user.name %>! You've adopted a cute little
bunny in need, named <%= rabbit.name %>. The bunny is located at:</p>
<p><%= adoption_center_address %></p>
<p>
The adoption fee is $<%= fee %>.
<% if coupon_amount %>
(This includes a discount of $<%= coupon_amount %> from your coupon)
<% end %>
You'll need to pay this when you go to pick up the rabbit.
</p>
<p>You will get an email with more information. Thanks!</p>
First, you’ll see our primary controller action is much simpler to understand. It’s doing exactly what we want – directing what the request should do. It’s so simple that it almost reads like pseudocode.
The ApplicationController has two notable changes to allow us to use the
context pattern gem: it now includes Context::Controller and we have added a
helper Context::BaseContextHelper
declaration.
You might also notice that our view doesn’t reference any instance variables anymore. Those are all methods in our ERB syntax.
Then there’s the new stuff: the extend_context
calls, and the context classes
themselves where we have all the logic that was previously in the controllers
and where we have view_helpers
declarations.
Let’s go step by step through the life cycle of the request (skipping the
irrelevant Rails internals). First the Rails router determines that the request
is for the RabbitAdoptionController#create
action and we go to the
ApplicationController
.
This Context::Controller
module provides our ApplicationController
with three
important pieces of functionality:
extend_context
method for us.@__context
, which is an instance of the Context::BaseContext
class. It will
become clear why I’m calling it a “context stack” later. To start, this
object merely provides the scaffolding for us to define our own contexts.method_missing
logic that I’ll get back to later.The request then goes to the ApplicationController
. Our first filter in
ApplicationController
does this:
extend_context :Application, params: params, session: session
This should be read as: “Add a new instance of the ApplicationContext
class to
our context stack, and initialize it with these attributes: { params: params,
session: session }
.” This means that the @__context
variable will now reference
this new instance of ApplicationContext
. That instance will have a reference to
the instance of Context::BaseContext
that the @__context
variable used to
define. Think of it somewhat like a linked list, where the most recent context
we’ve extended is at the top of the list.
Here’s how the structure of the ApplicationContext
works:
attr_accessor
declarations.view_helpers
declaration specifies which methods are available to views
latercontext-pattern
gem includes the
memoizer
gem).Our second filter in the ApplicationController
does this:
redirect_to new_session_path unless logged_in?
Here we can see that the logged_in?
method is coming from the context.
Our request then goes to RabbitAdoptionController
where the create
action
starts with:
extend_context :RabbitAdoptionCreate
This should be read as: “Add a new instance of the RabbitAdoptionCreateContext
class to our context stack”.
The next line of code is:
if !rabbit.spayed_or_neutered?
Here, the rabbit
variable is coming from the RabbitAdoptionCreateContext
instance at the top of our context stack.
Our context stack now includes instances of RabbitAdoptionCreateContext
,
ApplicationContext
, and Context::BaseContext
, in that order. The order is
very important here. Suppose that we tried calling logged_in?
at this point in
the code. Here’s what would happen:
Context::Controller
provides some method_missing logic? This
is where it comes into play.method_missing
code in the gem causes us to look for the logged_in?
method among the public methods provided by the RabbitAdoptionCreateContext
instance at the top of our context stack.method_missing
logic that causes it to request the method from the next
context in the stack. In this case, that will be the instance of
ApplicationContext
, where the method will be found.After we go through the controller action code, we go to the view, where we
find <%= current_user.name %>
.
In this code, current_user
is a method being provided by ApplicationContext
.
We are able to call this method from the view because of logic contained in
Context::BaseContextHelper
. (which is declared to be a helper module in
ApplicationController
). That helper module uses a similar approach as
Context::Controller
to access methods from the context stack via
method_missing
. The primary difference is that the logic in BaseContextHelper
only gives us access to context methods that are declared to be view_helpers
in
our contexts. In other words, controllers are able to access all public methods
defined in our contexts, and views are able to access all methods in our
contexts that are public and also declared to be view_helpers
.
This linked-list approach used to define the context stack is fundamentally
different from either inheritance or modules. Both of those are methods of
defining behaviors for and relationships among classes. One instance of a class
will have the same interface as another interface of that same class. The
context stack defines relationships between instances, not classes. In one part
of our codebase, an instance of FooContext
may have an instance of BarContext
next in the stack whereas another place in the codebase may have an instance of
BazContext
next in the stack.
The context stack enforces directionality. Suppose you had the following code in a controller:
extend_context :Foo
extend_context :Bar
A method defined in BarContext
could make reference to a method defined in
FooContext
, but not vice versa.
A context object can, and in practice often will, reference methods that it expects to be defined somewhere in the context stack already. For example, you might have a UserEditContext that references the logged in user via a method named current_user, which is defined earlier in the context stack. That’s a sensible thing to do, because it’s a way of saying that the context for editing the logged in user doesn’t make sense unless you’ve already established a context in which there is a user logged in. Another way to think about this is that the UserEditContext expects the context stack to provide it with an interface that includes a current_user.
Context objects can never overwrite public methods already available in the context stack, with the exception of support for object decoration, described next. The fundamental idea here is that good code shouldn’t change its mind. For example, this is an antipattern:
foo = 1
foo = 2 if blah?
The better code would be:
foo = blah? ? 2 : 1
If you try to reason about code as though you were the request, going
step-by-step as we did earlier, you want to be able to understand things in a
clear, linear fashion. This means that in our rabbit adoption agency example,
we can’t (for example) have a method named current_user
in the
RabbitAdoptionCreateContext
. If we tried to define such a method, the gem would
raise an exception at runtime. This exception would be raised when the code
reaches the extend_context :RabbitAdoptionCreate
code in our controller.
While we don’t want to be able to change the definition of an object entirely
as we move through a request, we certainly do want to be able to refine our
understanding of what that object is via decoration. This distinction is
important. You could imagine, for example, that ApplicationContext
might
define current_user
for us, but in some other specific controller action, we’d
later want to do the equivalent of current_user =
UserWithAdminRights.new(current_user)
.
Contexts give us an explicit way to do this as follows:
class FooContext < Context::BaseContext
decorate :current_user, decorator: UserWithAdminRights
An example showing all capabilities of the decorate declaration would be something like the following:
class FooContext < Context::BaseContext
decorate :current_user,
decorator: UserWithAdminRights,
args: [:bar, :baz],
memoize: true
def bar; end
def baz; end
end
This allows us to do the equivalent of the following in our context (to be clear, the code below would raise an exception):
class FooContext < Context::BaseContext
def current_user
UserWithAdminRights.new(@parent_context.current_user, bar: bar, baz: baz)
end
memoize :current_user
def bar; end
def baz; end
end
If you were using the standard decorator pattern with something like SimpleDelegator, the args would not be relevant. If you’re rolling your own code for object decoration (as we do at WegoWise) you might find it useful.
context-pattern
gemIf you’d like to try using the context pattern in an app, you can get started easily via the context-pattern gem. It includes a README with usage details, and questions/ feedback are welcome via Github issues and pull requests!
Thanks to Joseph Method, Nathan Fixler, and Marc Tibbitts for providing feedback and suggestions re: this writeup.