Rails application development is becoming more complex every day. Its common to see Rails apps with tensmaybe hundredsof 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 givenup 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 wed 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, Id like to propose a middle ground.
Lets look at the standard Rails app directory layout:
app/ models/ controllers/ views/ ...
\ Its a global namespace. You see these directories grow, and grow, and grow, and suddenly youre not sure what some classes are used for. Everything can be used everywhere, so youre 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.md account_management/ assets/ models/ controllers/ views/ ... README.md ...
\ There really isnt much to it. But I think that *this* is much clearer than what we had before. We can now understand what our apps 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?
Ill take each of these problems in turn.
Lets 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, its just easier to add behavior to it.
But lets pause for a bit. Are we *sure* we really need a User model in our apps? Arent 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 isnt a User. Theres 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 wont require you to use :if or :unless in validations and callbacks, or scoped attr_accessible declarations (using :as), because theyre being used in a much narrower context, so they have a smaller scope. You probably only have to worry about validating a Users email in AccountManagement::Registration. And maybe you can load your Store::Browser models as read-only. Youll find that different models based on the same tables dont depend on each other that much.
Things that change together, belong together. Smaller, more focused classes allow you to keep together that which youd probably keep separate. At the risk of committing heresy: if youre 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 dont 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 dont ripple through the entire test suite, because you arent using the same models everywhere. Your factories (or fixtures) are smaller.
Ok, so weve 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?
Theres 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.rb controllers/ application_controller.rb models/ email.rb ...
Use concerns where appropriate. Use inheritance where appropriate.
Communication between subsystems
I dont have much to say here. Sometimes all it takes is a method call into another subsystems object (dont 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 youll pay in clarity, and Im not sure about you, but Ill take clarity over sophistication any day.
Where to draw the boundaries
Lets be honest, theres no clear cut way to do this, and you probably wont get it right the first time. Some subsystems are very well definedyou just need to pay attention to the terms people use to describe certain sectionsothers arent. Ill have to leave this to your judgement.
Think about cohesion and coupling. Your subsystems shouldnt have to talk to the outside much.
Pay attention to qualified references (Module::Class).
Tell, dont ask.
For things that need to be shared (lets 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!