Dealing with Rails Application Complexity — A Report from MWRC
One of the major themes coming out of the 2012 Mountain West Ruby Conference (MWRC) was the rising complexity of Ruby applications, in particular with Rails. The focus of many of the talks was directed at the pain many of us are feeling with our bloated Rails models. When I first started developing with Rails back in 2007, much of the focus was moving the application logic from the view to the controller. Then, a few years ago, the “thin controller, fat model” mantra had us all moving our code from the controller to the model. While each of these steps were an improvement, Rails developers are now dealing with what is affectionately referred to as a “stinking pile of poo” in our models.
Having seen my share of fat models of late, this topic really grabbed my interest. I started thinking about this problem a couple of months ago while working on a rather large Rails application. Thanks to some good folks in the #urug (Utah Ruby Users Group) channel on Freenode I was pointed to the article Rails is Not Your Application which got me thinking about better ways to handle unmaintainable and unapproachable models in Rails. I was happy to see that I’m not the only one thinking about this. In fact, many very smart developers in the Ruby community are also dealing with this and coming up with very interesting ways of cracking the nut.
What follows is a summary of 5 talks given at MWRC that touched on this subject and some of the solutions they presented. Plenty of links have been provided to give you a chance to read up more on each approach so I won’t go into all the nitty-gritty details here.
Mike talked about using classic object oriented design patterns as a way to simplify your back-end business logic. This approach comes from some great work done by Avdi Grimm in his book Objects on Rails. Rather than putting all your logic in Rails models, Mike talked about using classes that aren’t directly tied to the database and are more based on behaviour than a table schema. When this is done, we get code that can handle change and can be tested in isolation. Much can be said about this approach and indeed much has! Take a look at the book, which can be read online for free.
Vagrant is the new darling of many developers and for good reason. Mitchell’s work on Vagrant is nothing short of game-changing for development environments. While developing Vagrant, Mitchell also ran up against some complexity in handling the provisioning of Virtual Box virtual machines. Taking some inspiration from Rack (see “What is Rack middleware?”), he implemented a middleware pattern in his code. Middleware embraces the concept of passing a state bag to a series of classes that all implement a similar API. Each class can then process the state bag (which is a Ruby Hash object) according to its needs and then pass it on to the next middleware. Those familiar with Rack will be right at home with this pattern. I could easily see this pattern as a replacement or used in conjunction with the state machine gem that is widely used for similar processing. At the end of Mitchell’s talk, many of us in the audience wondered if the middleware builder could be extracted from Vagrant and put in its own gem. At the MWRC Hackfest, Mitchell did just that and made it available on GitHub. Middleware solves complexity by explicitly showing the order that code has to run, makes it easy to see code dependencies, simplifies tests, and provides extensibility (via subclass).
Stephen Hageman (“Migrating from a Single Rails App to a Suite of Rails Engines”, Software Engineer at Pivotal Labs)
In his lightning talk, Stephen explained how complex Rails applications can be broken down into Rails Engines. His talk was a summary of what he wrote in a post from his Pivotal Labs blog. Anyone who’s built a Rails application using Spree will be familiar with this method. Breaking an application into functional engines, which can now be mounted with routes in Rails 3, provides the ability to simplify your test suite and even isolate certain functionality to individual web servers.
Rails application complexity comes in many forms and Jack Danger Canty presented the problem of not only having large models, but many models that interact with each other. Jack measures complexity by the number of possible endpoints that each class has. Each node in the graph represents a class in your rails app. Then he shows the relationship between each class by drawing vertices between each class. This results in the formula v =((n-1)^2 +n)/2 where n represents the number of nodes and v represents the number of vertices in your graph. So for 15 nodes, there’s a possibility of over 11,000 vertices! That complexity is often what makes big Rails apps difficult to adapt and change.
The solution that Jack presents is to reduce complexity in three ways: delete code, use a library or gem, and spin off a service. Deleting code can start with any functionality that doesn’t bring any value to the users of your app. As developers, we often try to plan for the unknown by introducing extra code that’s there “just in case”. Deleting code requires a YAGNI attitude. If you don’t need it, get rid of it. Many times code that is reused throughout the application can separated and isolated into a library. The advantage of doing this is that libraries can be maintained outside the application and if properly done, can contain their own test suite. Libraries can also be packaged in Ruby gems so that they are easier to keep isolated and avoid the temptation of constant tweaking. The last suggestion to spin off a service was also talked about by the next speaker so I’ll cover the details there.
Let’s be honest. Throwing the phrase “Service Oriented Architecture” at a bunch of Ruby and Rails developers is courageous. SOA is a title that brings with it a ton of baggage that rubyists just don’t like to deal with (SOAP, WSDL, ESB). In fact, this tweet pretty much sums up the community’s thoughts on the subject. Indeed, BJ Clark spent the good majority of his talk convincing us that we really don’t want to do this (#realtalk). Once we were all fairly warned, BJ then talked about how they use SOA at Goldstar with success and gives some very insightful tips for those willing to brave this kind of architecture.
To be clear, BJ isn’t advocating using an enterprise class SOA with an Enterprise Service Bus and Object Brokering, or even using SOAP for that matter. What he’s talking about is breaking your large Rails application into smaller applications that have a specific purpose. These applications then communicate with each other via HTTP behind your firewall. To keep things simple, the message format can be (should be) JSON. The thinking behind this is that smaller applications run faster than big applications and in a lot of cases it makes a lot of sense to do this. For example, your main application should not be concerned with sending email to your customers. So you could spin off a service that receives a simple JSON message from your main application to send an email and then let it do the processing for you.
To be successful doing SOA, BJ states that you need to have a small team where everyone knows about and works on all the services. This avoids potentially constraining ideas on what a service should and shouldn’t do. He also recommends that you read “Service-Oriented Design with Ruby and Rails” by Paul Dix to get you started but to adapt the concepts for your needs. For example, Dix states one advantage to having services is that they can automatically serve as an external API, but BJ believes that it’s better to have an app serve as your API that talks to your services.
It’s evident that the next big mountain to climb in Rails application development is going to be solving the problem of increased complexity. While Rails 3 introduced greater modularity to limit the framework’s footprint, application developers still need to put some thought into how they are building their applications on that framework. While any of the ideas above can help with that effort, it is probably going to be a combination of several of them that develop into practices that we can implement to help cut down on complex and unmanageable code.