Hexagonal Architecture Guidelines for Rails
Good application design is hard and there's no one "right" way to do it.
I often get asked the how I design decoupled applications and while there's no easy answer I'm going to lay down the rules that have worked for me.
Rails gives you a very minimal architecture of three parts (MVC) all three of which have proved inadequate to contain any real amount of complexity. Remember "fat models, skinny controllers"?
This is in stark contrast to 'enterprise' frameworks which are often criticised for providing too much complexity up front.
The key is to start with just a couple of extra layers and keep those layers decoupled so that more can be added, allowing you application to scale in complexity with ease.
- Controllers actions are allowed a single line of code
- The application doesn't return any values to controllers
- The application has no knowledge of the framework
- No inheritance or mixins (with two exceptions)
- Domain objects have no knowledge of persistence
- Separate your "wiring"
The skinniest of controllers
Your controllers should look like this
class ThingsController def show app_of_things.show_thing(rails_adapter) end def create app_of_things.create_thing(rails_adapter) end end
Nothing more, just that single line. This is an example of where ["Tell don't ask"](http://martinfowler.com/bliki/TellDontAsk.html) really shines.
A note on "Tell don't ask"
As soon as an object's method returns data back to its caller it relinquishes control of the program.
"Here's the data, you decide what to do with it."
In hexagon land the application is in charge until the bitter end, it never returns data back to its caller, it sends messages and calls all the shots.
Don't return anything to controllers
So how does the view or data get rendered? It's your app's responsibility to send a message back to the web layer instructing it to render.
rails_adapter should be implemented as a [proxy](https://en.wikipedia.org/wiki/Proxy_pattern) / [facade](https://en.wikipedia.org/wiki/Facade_pattern) wrapper around the Rails controller defining
a narrow interface that your app will depend on.
I recommend you implement methods such as
and map these to redirects or appropriate HTTP status codes in your adapter.
The adapter is not part of your application, it's the mediator that translates results of events from your application into something Rails can understand. It can and will have knowledge of the web or framework but must not leak it into the application.
The application has no knowledge of the framework
Define your own APIs for everything your application touches, do this by
wrapping foreign objects in scar tissue. Use
to make proxy object quickly and easily.
No inheritance or mixins
Predictably I make exceptions for
The Gang of Four said
["Prefer composition over inheritance"](http://en.wikipedia.org/wiki/Compositionoverinheritance)
so just prefer it, all the time!
These two standard library tools will help you build composed objects easily.
Domain objects have no knowledge of persistence
Use a combination of the
with a simple data mapper to get your data into some PORO or
Writing a general purpose data mappers is hard, Ruby doesn't have one yet and the enterprise solutions like Hibernate are not exactly loved. My advice is to write your own data mapper without making it general purpose or feature rich. If it only works just enough with your data it shouldn't be too complex. Add the features as you need them, optimise as lazily as possible.
Separate your wiring (or inject dependencies)
Have an object whose responsibility is to wire up all the others, it will make your code simpler and more flexible. I've [written](http://theaudaciouscodeexperiment.com/blog/2013/10/18/ruby-dependency-injection) and [spoke](https://skillsmatter.com/skillscasts/4437-improve-your-code-with-dependency-injection) about this before so won't re-iterate here.
That's my hexagonal prescription. If you want to get in touch to nerd out and discuss these ideas I'd be more than happy to chat.
Where's the sample app?
In the works :)