Half-Baked Thoughts on Ruby Web Architecture
July 21, 2013
Some half-baked ideas I’ve been playing around with on my Ruby projects recently.
Informal DCI
I got really into DCI a few months ago. I picked up Clean Ruby and acquired a copy of Lean Architecture (didn’t make it past page 10), but I need to explore most ideas in actual code instead of books.
The idea of DCI — as I have cystallized it — is to extract behavior into groups of roles or actions and then inject this new behavior into objects.
The canonical example, instead of this:
class User < ActiveRecord::Base
attr_accessible :email, :name
... bunch of validators ...
... tons of other crap ...
def approve(request)
request.state = :approved
ApprovalMailer.send_approval_mail(request)
end
end
user.approve(request)
Do something like this:
class User < ActiveRecord::Base
... only user model stuff ...
end
class Approver
def approve(request)
request.state = :approved
ApprovalMailer.send_approval_mail(request)
end
end
user.extend(Approver)
user.approve(request)
It is now easier to test the Approver
behavior in isolation, User
becomes less of a junk
drawer, it sort of makes more sense from a real-world sense since a user will be acting as
an approver in some contexts (and maybe acting as a Moderator
or something in another context).
I’ve seen some people arguing for a convention were all of the behavior roles have a method
called call
. This seems really silly to me and I can’t think of a good reason for it (and
the reason against it is that it hurts readability). I’ve also read arguments that using extend
really screws up the “method cache” and is apparently bad.
So for a while I was in this weird state: I liked the idea of DCI but none of the implementations felt right. And if it didn’t feel right, I knew I wasn’t going to stick with it.
I tried to take some of the ideas about roles and behavior extraction in a slightly different way. I looked at some of the Command/Query stuff that seems to be more popular in .NET-land and tried building an app using Commands (or Use Cases, there is so much overloaded terminology it is maddening).
This style felt right to me. I was writing a bunch of small, super focused classes to do some work. My controllers were pretty simple and I stopped testing them for the most part.
Some examples from my RSS reader (small Sinatra app):
class MarkAsRead
def initialize(story_id, repository = StoryRepository)
@story_id = story_id
@repo = repository
end
def mark_as_read
@repo.fetch(@story_id).update_attributes(is_read: true)
end
end
post "/stories/mark_all_as_read" do
MarkAllAsRead.new(params[:story_ids]).mark_as_read
redirect to("/news")
end
class ImportFromOpml
ONE_DAY = 24 * 60 * 60
def self.import(opml_contents)
feeds = OpmlParser.new.parse_feeds(opml_contents)
feeds.each do |feed|
Feed.create(name: feed[:name],
url: feed[:url],
last_fetched: Time.now - ONE_DAY)
end
end
end
post "/feeds/import" do
ImportFromOpml.import(params["opml_file"][:tempfile].read)
redirect to("/setup/tutorial")
end
I combined these command/use-case things with Repositories and wrote most of the test as isolated unit tests — super fast to run because I mock out the database…well, and I’m not using Rails so they are pretty fast already.
Persistence Layer Separation
Repositories seem like a natural fit given the recent change of heart about Fat Models from the Rails community. Again, my experience with this pattern comes from .NET, but the basic idea is use a class to get a group of domain objects out of a database. I think about a Repository as a group of Query objects with a common theme (usually the underlying model).
The code looks like:
class StoryRepository
def self.read(page = 1)
Story.where(is_read: true).includes(:feed)
.order("published desc").page(page).per_page(20)
end
end
Instead of using a scope
or putting more methods on Story
, we just do the querying behind
a clean StoryRepository#read
interface. This is definitely not common in the Rails apps I’ve
seen. I really like using this pattern: my controller isn’t cluttered with sort order or pagination
stuff, my model doesn’t need to know every possible way a caller wants to query it, I can
stub out that nasty method chain in a test easily.
This feels kind of strange for ActiveRecord
based applications — since the domain objects
and the data mapping are the same thing, ActiveRecord::Base
subclasses. In my experience
with other tools like NHibernate
you have dumber domain objects and explicit mapping objects
that link up database columns to properties.
This separation comes with trade-offs: the Rails Way is quicker to code up (with just one class) but you end up with hard coupling to the database whenever you create domain objects (not good for tests). Maybe the Ruby Object Mapper project will bring more popularity to splitting out domain models and mapping objects.
I haven’t really found a good solution for this yet. My latest exploration was just stubbing the
Repository methods to return OpenStruct
-like objects built in test factories.
Dependency Injection
DI is so easy in Ruby and really helps with testing. I don’t think I would ever go without it anymore. I also like how glaringly obvious your dependencies become when you use injection.
class FeedDiscovery
def discover(url, finder = Feedbag, parser = Feedzirra::Feed)
...
end
end
I could lie and say that this pattern is handy if I ever need to swap out gems, but who am I kidding? That never actually happens. Since Ruby allows for default arguments there is really no downside to this style of coding — the calling interface is the same but I can test much easier. Win, win.
Controller Callbacks
The biggest problem I had with my use-case/command style was that handling more than the happy path flow in the controller got clunky. I see two possible solutions to look into: returning result objects or some kind of callbacks on the controller.
Result objects seem like the more tame path. Define some convention for status, probably a hash
with keys like :status
, :errors
, and :model
and then handle that in the controller.
def create
result = AddFeedSubscription.subscribe(params[:feed])
if result[:status] == :success
flash[:success] = "Subscribed!"
redirect_to result[:model]
else
flash[:errors] = result[:errors]
render "new"
end
end
I don’t think this is a bad approach and probably what I would do with a team larger than 2.
The other approach, which I first saw in Hexagonal Rails, is to pass the controller as an argument and call methods on it.
def create
result = AddFeedSubscription.new(self, params[:feed]).subscribe
end
private
def subscription_succeeded(subscription)
flash[:success] = "Subscribed!"
redirect_to subscription
end
def subscription_failed(subscription)
flash[:errors] = subscription.errors
render "new"
end
end
class AddFeedSubscription
def initialize(callback, params)
@callback = callback
@params = params
end
def subscribe
... some work ...
@callback.subscription_succeeded(subscription)
end
end
This seems kind of inside-out, but something feels right about it to me. If you are going to have more than a if/else branch in the controller action, then the callbacks seem like they might be a win.
And controller testing can mostly go out the window. Throw in a double for callback and keep the actual callbacks simple and mostly framework plumbing and I think you have a recipe for good design.
This idea seems to be the one advocated by the DCI in Ruby sample application, which is the closest resource I’ve found that mirrors my own preferences and findings.
What’s still stewing?
Decorators/Presenters/View Models - I think if you are going all-in on view models then you should use something logicless, but that means Liquid right now for your templating. I can’t imagine building a whole app using Liquid and not wanting to pull my hair out.
Client-side JS - Can this co-exist with a typically server side app? Or are you in for a world of hurt if you don’t put clients-ide MVC as the first class citizen and just build a JSON API backend? My limited experience trying to use Backbone with Sinatra was painful, but workable.