Rails application development is becoming more complex every day. It’s common to see Rails apps with tens—maybe hundreds—of models, not counting controllers, helpers, decorators, interactors, presenters…
We seem to be at a turning point. Whatever Rails had that made us abandon other frameworks (and languages) in the hope of finding greener (and more productive) pastures, seems to be fading in our collective memory. Many have given up already, declaring the Rails “golden path” to be hopeless.
There have been a number of architectural approaches aimed at solving the problem, the most commonly known being DCI and the Hexagonal Architecture. But I think many of our problems could be solved if we’d simply stick to writing small applications.
We have been taught to write small methods, and to put them inside small classes. Small is easier to understand, easier to change, and easier to reuse. So why do we forget this advice when it comes to structuring our apps? Perhaps because the alternative that comes to mind is SOA.
SOA comes with its own set of problems. It is often hard to understand where a service should end and another should begin. We create services that are not-quite-right, and then try to use them in not-so-appropriate ways. We cut corners. Chaos ensues.
So, I’d like to propose a middle ground.
has_many :modules
Let’s look at the standard Rails app directory layout:
app/models/controllers/views/...
It’s a global namespace. You see these directories grow, and grow, and grow, and suddenly you’re not sure what some classes are used for. Everything can be used everywhere, so you’re not quite sure which changes will affect what. And given the rampant tight coupling, making a change often means entering a world of pain.
But what if we did something like this instead:
app/authentication/assets/models/controllers/views/...README.mdaccount_management/assets/models/controllers/views/...README.md...
There really isn’t much to it. But I think that this is much clearer than what we had before. We can now understand what our app’s subsystems are, at a glance. A README.md strategically placed inside each subsystem could give a clear overview of what that subsystem is about, what are its most important classes, etc.
This is not the same as using many Rails Engines, which are usually maintained as separate projects and included in the main project as gems. They still have the maintenance overhead as a disadvantage, and are better used when you are building reusable modules.
The problem of shared code
“Ok”, you say, “this is all well and good, but what if these subsystems use common functionality? And what happens when each subsystem needs to talk to others?”
I’ll take each of these problems in turn.
Let’s use the quintessential example: the User model. Usually, the User model is one of the fattest models in any app. Because so many things are dependent on the user, it’s just easier to add behavior to it.
But let’s pause for a bit. Are we sure we really need a User model in our apps? Aren’t we just accidentally coupling our database structure (in all likelihood, we are using a “users” table) with our code structure? The truth is, a user exhibits different behavior depending on the role they are assuming at a given moment. Consider:
- Someone looking at our app from the outside is a Visitor.
- A user that is editing their profile may be an AccountOwner. Or maybe you don’t really care about who holds the account, but simply that you indeed have an Account, or a Registration, or even a Profile.
- A user looking looking for products to order may be a Browser.
- A user placing an Order becomes a Customer.
So there isn’t a User. There’s AccountManagement::Account, Store::Customer or Store::Browser. They do different things, therefore the behavior lives in different places.
To me, the biggest gain from this approach is that your classes become much more focused. They become easier to reason about. Often, they won’t require you to use :if or :unless in validations and callbacks, or scoped attr_accessible declarations (using :as), because they’re being used in a much narrower context, so they have a smaller scope. You probably only have to worry about validating a User’s email in AccountManagement::Registration. And maybe you can load your Store::Browser models as read-only. You’ll find that different models based on the same tables don’t depend on each other that much.
Things that change together, belong together. Smaller, more focused classes allow you to keep together that which you’d probably keep separate. At the risk of committing heresy: if you’re working on an API, you could even implement #to_json inside your model classes. Remember, all the principles we are taught are guidelines that are meant to be applied in certain contexts. If we change the context, we may find that some guidelines don’t fit as nicely, or in the way we think we should apply them (In this case, I daresay each class still has a single responsibility, but that responsibility spans multiple layers).
Our tests (unit/integration/acceptance) enjoy the same benefits. Changes to a model don’t ripple through the entire test suite, because you aren’t using the same models everywhere. Your factories (or fixtures) are smaller.
Shared concerns
Ok, so we’ve seen that you may not have to share as much code as you think you would, but what about code that you do need to share? What if multiple models require email validation, for example?
There’s a semi-convention in Rails to use app/views/shared for shared partials. I think we can extend that concept to the entire app:
app/application/ (or shared/)concerns/email_holder.rbcontrollers/application_controller.rbmodels/email.rb...
Use concerns where appropriate. Use inheritance where appropriate.
Communication between subsystems
I don’t have much to say here. Sometimes all it takes is a method call into another subsystem’s object (don’t be alarmed, references to, say, AccountManagement::Account outside the AccountManagement module will make you pause due to the fully qualified path). If you really want to keep subsystems decoupled you have a wide range of solutions all the way up to using queues, but really… start small before you bring in the big guns. The higher the level of sophistication, the higher the price you’ll pay in clarity, and I’m not sure about you, but I’ll take clarity over sophistication any day.
Where to draw the boundaries
Let’s be honest, there’s no clear cut way to do this, and you probably won’t get it right the first time. Some subsystems are very well defined—you just need to pay attention to the terms people use to describe certain sections—others aren’t. I’ll have to leave this to your judgement.
Think about cohesion and coupling. Your subsystems shouldn’t have to talk to the outside much.
Pay attention to qualified references (Module::Class).
Tell, don’t ask.
For things that need to be shared (let’s say you have a dashboard where you are showing widgets coming from different subsystems) experiment with a lightweight plugin architecture, with a well-defined interface.
Above all, start small, with a limited number of sub-systems. One advantage of this system is that it should make it really easy to move classes around. Use it!
That’s it, for now
I hope this post has got you thinking. On my next post, I plan to translate this into actual code. We’ll see how far Rails will allow us to go!
—David Leal