A few months ago, development of http://relishapp.comÂ reached what is a familiar but rather unpleasant plateau for me. Up until that point, it had been developed using conventional Ruby on Rails idioms, with controllers that talk directly to ActiveRecord class methods to make queries, then set instance variables to pass the results of those queries to view templates for rendering. It’s all very procedural, and very tightly coupled.
This post is an introduction to a forthcoming series which will explain how I’ve been tackling this.
Let me start by saying that the coupling in my Rails applicationÂ is not necessarily a bad thing. In fact, to get the product off the ground and give RSpec’s amazing living documentation a place to call home, we needed to be able to put Relish together very quickly. This is where Rails absolutely dazzles: the early stages of a project are so easy that adding features is a delight.
Unfortunately, the very forces that make adding features to a new Rails application so easy are the same ones that start to hold you back as the number of features grows. That conventional Rails style is what Kent Beck has called a connected design style, Quoting from that article:
In a connected system, elements are highly available to each other (via global state, for example). Adding the first feature to a connected system is cheap. All the resources you need are available. However, the cost of all those connections is that subsequent features are very likely to interact with previous features, driving up the cost of development over time.
The alternative to a connected design style is what Kent calls a modular design style.Â He contrasts these using this chart:
For the first few features, a connected design style is cheaper than a modular design style. In fact, using what might be referred to by software design snobs as ‘poor design’ could give you a competitive advantage in the early stages of your product’s development. The trick is to recognise when it’s time to switch styles.
So, while I love Rails for what it is, I also recognise its weaknesses. To make the Relish codebase habitable in the long term, I need to move beyond its connected design style and establish a more modular design. But how? It’s a question that many different people are asking at the moment, and I’ve been diving deep into what they are saying. Here’s a sample:
- Gary Bernhardt and Corey Haines have been agitating with screencasts and talks that highlight the advantages of decoupling from Rails.
- Bob Martin has been reminding us that Rails should be an implementation detail.
- Jim Gay is writing a book about using DCI (Data, Context, Interaction) to organise Ruby codebases.
- Avdi Grimm has written a book about building object-oriented Rails applications.
- Steve Klabnik has written about the delights of POROs.
- Nicolas Henry is thinking about pushing his application code out into gems.
- Mark Burns has been trying out DCI in a new app.
- The Ruby Rogues have been discussing Object-Oriented Programming in Rails with Jim Weirich.
- Piotr Solnica has been writing about making his ActiveRecord models skinny too.
For my part, I re-read the outstanding Growing Object-Oriented Software Guided by TestsÂ (GOOS) by Steve Freeman and Nat Price. In that book, Steve and Nat advocate a ports-and-adapters or hexagonal architecture, which keeps your core application logic separate from any technical details needed to connect the application to the real world; technical details like databases, or web user interfaces.
I think it’s easier to use an architecture like this in Java or C#, where packages and interfaces make it easy to see and enforce separation between chunks of the application, than in Ruby where it’s harder to make clear boundaries around areas of the system. But that doesn’t mean it isn’t possible. For the past couple of months I have been experimenting with refactorings in the Relish codebase to move it towards a more hexagonal architectural style. The overall goal is to make the Relish codebase habitable, which for me breaks down into:
1. Fast tests
I never add features without tests, and I want those tests to run fast. The goal of the hexagonal architecture is to have the most interesting (and frequently changing) logic implemented in plain old Ruby objects that have no dependency on any other libraries so they are easy to understand, and can be quickly spun up in a test.
2. Modularity and encapsulation
Ruby is an object-oriented language with great support for functional programming, and I want to make the most of that to keep Relish’s code easy to change.
3. Clean and well-organised
I want a structure that communicates what each part of the system is doing, and makes it easy for new team members to jump in and start hacking on the code.
I’ll start the series by explaining a couple of key concepts from Steve and Nat’s GOOS book, and about hexagonal architectures in general. Then we’ll get down to some practical work refactoring a typical Rails controller. Stay tuned!