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.
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 testsI 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 encapsulationRuby 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-organisedI 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!