On Structuring Rails Applications, Part II

Pascal Zumkehr

When models and controllers are supposed to be skinny, Rails clearly lacks a place to put all the extracted code. While there is a variety of different approaches, we like to keep it simple, stupid™.

In part one, we discussed the scope of models and controllers. Most applications contain logic that exceeds the responsibility of these classes. So where should all this code be put?

Controller actions as the main entry point into Rails applications can be separated in two different categories: Those that only read, and those that perform changes. Hence, basically two constructs are required when further organizing our code: One for fetching and displaying data, another for manipulating data. Today’s discussion about structuring Rails applications includes terms like Service Objects, Decorators, Form Objects, Presenters or Commands. They all fit into one of the above categories.

Go fire up Bundler… Not

Various gems implement such constructs. Some by just establishing a simple method convention, others with complete frameworks and by hooking into low-level Rails interfaces.

We like to follow Rails as much as possible. We even use helpers now and then and admit it in public blog posts. During the last ten years we used Rails, the framework provided an elaborate, stable and highly productive foundation for our applications. The menu is omakase. Many gem hype-cycles rolled up and down, the Rails API evolved steadily.

If an application has to be maintained for several years with minimal effort and budget, it is essential to confine the amount of used gems. Every now and then a gem is abandoned, and even though it is a free gem, new owners are not always easy to find. As soon as incompatibilities with new versions of Rails or other gems start to appear, you are forced to replace it. Such a replacement is especially costly if the gem affects the structure of your code. When you rip it off your application, all code building on it will have to be refactored.

Because of that, we do not use any gems for structuring our code beyond Rails, and not even an own method pattern. The benefit of a gemified pattern is usually not worth the issues that may appear.

Plain Old Ruby Objects™ have all the power we need.

So we do not use gems for that, but we still like to have some patterns in our code. We found service objects and presenters to be a great foundation for an advanced structure. They have a clear responsibility and their designated folder in  app/. How do they look like?

One for the moneymanipulation

A major lack in the Rails philosophy is the use of namespaces. For each domain entity, there is one model class, one controller, one serializer and one decorator, all at the top level. Nobody forbids anything, but all teach the same: one class is enough, namespaces are not needed.

This is wrong. In order to represent complex domain logic conforming to the single responsibility principle, code often has to be separated in multiple classes. This will usually be a main entry point acting as the service object and various supportive classes.

Namespaces provide the means to group a set of associated classes into an own place to reside.

Therefore, we avoid replicating the app/foos/*_foo.rb pattern. First, we create appropriate namespace folders. In there, the service and its supportive classes are free to occupy as many files as they need, one per class.

As mentioned, services are just POROs. They do not have a common public method to be called. In some cases, they may even have more than one. Some keep track of error messages, others simply return a boolean or another result. Whatever fits best.

There is no boilerplate involved when calling a service from a controller:

Such service objects are easy to test and also easy to use in a different context, like a background job. With the use of namespaces, the big question of where to put the code that does not belong into a certain class is answered in most cases.

And when a simple @post.update(permitted_params) does the job in the controller, there is absolutely no need for an own service/operation/command object.

Two for the show

We have been down the road with decorators. They are a great substitute for helpers. If your model needs a couple of methods to format special values, apply various CSS classes and so on, decorators are the way to go.

Unfortunately, they are not the holy grail for all kind of display logic. When a model needs different decorator methods on different pages, the decorator soon gets cluttered and violates the single responsibility principle. For pages with information from different models, elaborate components and crunched numbers, decorators definitively fall short. This is were presenters come into play.

Like our service objects, presenters are POROs, responsible for fetching and displaying data. They only read and never write data. Another name that is often used is view models/objects. Generally, a presenter is created for one specific view. Complex queries handling filtering parameters go in there as well as the aggregation of data as required by the view.

Presenters do not have access to the view context, as they do not render any kind of HTML. This is great, because we may use the same presenter for rendering CSVs, PDFs or no matter what. They do prepare the data in a way that the conventional view helpers may directly use it. This keeps the logic out and the HTML in the templates.

If the page just displays the information of a single model object, there is usually no need for an own presenter. Either a decorator or some plain old view helper methods will do.

Three for the rest

There are certainly a few classes in most applications that still do not fit into the existing scheme. There may be non-persisted, model-like classes like a period of time or generic logic classes like a SQL condition builder. For these, an own support subfolder in app/ usually makes sense.

If you find yourself having a lot of code with the same kind of responsibility, additional subfolders are the (inofficial?) Rails-way to go, as do serializers, jobs, policies and so on. See this excellent post by Code Climate for further ideas.

And four to go

The key insight is that all these additional constructs do not primarily need a fixed form, potentially provided by a gem, but the following aspects:

  • Plain old ruby objects
  • Clear responsibilities
  • Namespace modules to unite associated code split up in several classes
  • An own, designated subfolder in app/

With these four points, you have the power you need to create a clear structure for a maintainable Rails application. Stop looking around, start coding :).

Kommentare sind geschlossen.