TL;DR
Don’t just throw all your code in app/models
; don’t just build a Rails app.
Have another directory that contains all of your domain-specific logic. Put some
thought into how you organize the files in that directory. And there’s a rule to
follow: files in this directory must not invoke any Rails-provided methods.
Write an application for your domain, not a Rails app
Don’t get me wrong. Rails is fantastic—I love it! But my past Rails projects all eventually suffer from poor architecture and slow tests. It’s not Rails’ fault, it’s mine. I’ve been leaning too heavily on Rails this whole time. Now I’m finally learning how to build great apps that I enjoy working on long beyond the prototype stage.
The key to building an application with Rails that stands the test of time is to not use Rails as the defining feature of the application. Rails is just a delivery mechanism and a persistence layer. The domain logic should be separate from Rails. (Thanks to Bob Martin for motivating me to confront this this issue.)
At the same time, Rails provides a lot of default goodness out of the box. I do not want a lot of extra code that re-invents anything already provided by Rails. The goal is to achieve an improved architecture without sacrificing the ease provided by Rails.
I’ve split this post into two parts (ignoring this intro). The first is a high-level explanation of the architecture that I’m proposing. In the second section, I’ll show some of the code I’m using to get this all to work.
The Architecture (from 20k ft)
The short version: app/models
contains my ActiveRecord models and app/lib
contains application / domain-specific logic.
app/models
The classes in app/models
should be ActiveRecord classes or classes that
extensively use the ActiveRecord API (e.g., query-helper classes).
Additionally, code in app/models
should know nothing of our domain logic.
The classes should be focused only on data retrieval and storage. One way to
think of app/models
classes is as facades to the ActiveRecord API.
I use ActiveRecord models throughout the application, but I don’t write any
code outside of app/models
that directly uses an ActiveRecord method. This
means that, in code outside of app/models
, I only invoke standard field
getters/setters and methods that I’ve defined on the models.
This approach is a balancing act. For example, I let scaffolded controllers invoke ActiveRecord methods—I’m not going to spend time updating the default controllers. This goes to the point of not creating extra work / reinventing the wheel. After all, one could have PORO classes to represent each ActiveRecord model to the rest of the application, but achieving this would require a decent amount of work for what may be an almost entirely academic benefit.
app/lib
The app/lib
directory contains the business/domain logic. The code in
app/lib
must have no knowledge of Rails—none at all. app/lib
classes
(all POROs) can still use the model classes, but they are not allowed to use any
ActiveRecord API methods (except the field getters/setters). This means direct
invocation of save
, create
, update_attributes
, all querying, etc. is off
limits.
By separating the application logic into a separate directory, I’ve found it
much easier to make good architectural decisions. A nice benefit of this
approach is that it makes it very easy to write
fast_specs since I
know that code in app/lib
does not have dependencies on Rails or the database.
All models in app/lib
have their specs located in a fast_spec
directory.
The classes within app/lib
are organized into modules. The modules you define
will vary depending on your domain, but I’ll share a few of mine from a project
I’m working on:
Integration – interaction with remote data sources
Integration classes are responsible for interacting with remote data sources. These classes have no dependencies on our other classes. The external sources may be remote APIs, uploaded data files, etc.
Render – renders documents in various formats
Classes in the render module are responsible for rendering non-HTML/JSON views (e.g., render a PDF, XLS, etc). It is often useful to be able to generate these files outside of the standard request/response cycle and it’s much easier to test the result when it’s just plain Ruby.
Service – encapsulates user story logic
Service classes encapsulate the logic of our user stories (or parts of stories).
The service classes decrease complexity and coupling in the system by their
organization and by providing interaction between the persistence layer
(app/models
) and the domain logic layer (app/lib
). Service classes may
invoke other service classes to build more complex behaviors.
Util – code not relevant to our domain
Any code unrelated to our specific domain gets placed here (e.g., I have a class that converts XLSB files to XLS and another that compares hashes). Anything you throw in here might be a good candidate for a gem!
This sounds awesome. How can I do it? (i.e., the code)
Setting up app/lib
Add the following to config/application.rb:
1
|
|
That’s all you need to do in order to begin adding code into app/lib
! Here’s
what my app/lib
looks like:
1 2 |
|
Each folder (module) has a simple file with the same name that creates the module. E.g.,:
1
|
|
Setting up fast_spec
For the corresponding fast_specs, add a fast_spec
directory at the top level
of your project. Then add a file fast_spec/spec_fast_helper.rb
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
I also added a rake task so that I can rake fast
:
1 2 3 4 5 6 7 8 9 |
|
Now in each fast_spec file just require 'spec_fast_helper'
. If you like guard,
check out guard-fast_spec. And if
you haven’t been using guard because your tests have been too slow, try it
again. When your tests are this fast you’ll love it.
When I need to include ActiveRecord models in the tests, I just use stubs. The danger here is that your specs become out of sync with your models, but a decent integration test suite should catch these issues.
Since all the complicated logic is contained in fast_spec
, the specs in spec
are all simple. Really simple. Which means that those specs run decently fast:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
And the fast_specs
?
1 2 3 4 5 6 7 8 9 10 |
|
It’s still a young app so there aren’t a ton of specs, but I think you get the idea.
Minimal changes, big results
This simple organizational change (along with the associated rules) makes it easier for me to employ good OOP design principles in my projects that use Rails, yet I’m still able to reap the benefit of Rails (which is plenty!).
Let me know if you like this idea. If there’s interest, I’ll write some
follow-up posts with code samples that demonstrate how I’m using this app/lib
organization to write cleaner, more modular code.