In this multi-series blog post, we're going hands-on through the steps that can help transform the design of a default Rails application into one with clearer responsibilities and that is easier to test.
Have a look at the other parts, too!
You can follow along with a repository available on GitHub that contains all the code discussed here.
Rails controllers, and controllers of web frameworks in general, tend to attract behaviour. Unfortunately, often times it's behaviour that does not necessarily belong in a controller. If we think of Rails purely in the way of being a facility to expose our application to the web, a first step toward a better design is done already!
In the movie example, that means the core domain of the application is organising movies. It's not storing or viewing movies on a website. That's a nice “byproduct” but not the main intent, even when we can be certain the application will always be running in the web. Separating the core domain logic and behaviour from the so-called delivery mechanism is a good design approach.
There are several names for this kind of application architecture. Uncle Bob calls it Clean Architecture and Alistair Cockburn used Hexagonal Architecture or Ports & Adapters for it. The main idea behind it is that the application's core logic shouldn't worry or even know whether users interact with it through a web interface, a GUI or a terminal, etc.
A similar concept has also been around the Rails community for some time now under the name of skinny controllers. Keeping controllers skinny means there shouldn't be much code in them. Ideally, the only responsibility of a controller is to translate a web request for our application.
Translating can be as simple as just triggering an action because a user clicked on a button, or converting data of a form submission that contains a lot of parameters into a data structure the application can use.
But where to put all the code that currently lives in the controller then?
Into something we call service classes. A service class encapsulates a certain action or behaviour specific to a use case. It can also be seen as a command object, to use a design pattern name for it.
How can this look in our movie example? This is the implementation of the
MoviesController (shortened for readability purposes).
Here, the responsibilities of
create are a) creating a movie; b) showing a success message, redirecting when the movie has been created; and c) showing an error message depending on whether the movie couldn't be created while re-rendering the form. And not to forget, actually persisting the movie if sufficient data has been provided.
Having all of these responsibilities is not necessarily bad, but having the controller know about the specifics of how to create and validate a movie is. This “obsession” about details is reflected in the tests for the controller, too.
Most of the tests verify web-related behaviour, which is what we want the controller tests to focus on. But that makes a test like
"creates a new movie" stand out, as it is too concerned about details. We do need to verify that a movie gets created, but the controller's tests shouldn't verify the details of that action.
Adding a Create Service
The controller should not be responsible for actually creating a movie.
Well… that is not 100% true. It is and will be responsible for creating a movie, but hopefully only by delegating the details of how to do this to something else. This will also help clean up the tests for the controller, as the details of verifying that the correct movie has been created can be moved to a more suitable place.
We're going to extract those details to a
The tests for the
MovieCreator will then make sure that a movie with valid data has been created in the correct way.
MovieCreator returns a result object that can be asked whether or not the movie could be created. With that in place, the controller's
create method can be simplified into this:
Now the controller doesn't need to know the details of how to create (and validate) a movie anymore. It delegates to the
MovieCreator and decides based on its result what to do.
Which leaves us with another question: What do we do with the previously mentioned test
"creates a new movie"?
One option could be to to leave it as it is, since it is still passing and verifying needed behaviour. An alternative is to refactor it in a way that expresses the intent behind
create on a higher level:
Now the controller's tests focus on the essence of what the controller should be doing and nothing else. All web-related behaviour is still in place and covered by the tests. But we did change how the movie gets created. Since on this level we're not interested in the details of how to create a movie, we only verify that a movie has been created.
Now that we introduced several movie-related classes, it's time to revisit the directory structure of our application. We created a
MovieCreator, and a
MovieUpdater. They all seem to have something to do with a movie. So instead of prefixing all classes with “
Movie”, Ruby provides modules as a better way to group cohesive classes like that.
This will also help tidying up the
lib directory, as the class files will then reside in (for example)
lib/movies. The classes inside that directory will be namespaced with a
Movies module (e.g.
Another way to name theses service classes is using verbs instead of nouns, e.g.
Movies::Update. Which way we choose is not as important as being consistent with it.
By extracting service classes, we improved the responsibilities of our application's classes.
We will end up with more classes overall in the end, but all classes focus on a more clearly defined task—having one responsibility at best. This not only helps with a better adherence to the Single Responsibility Principle. It makes testing easier too, as the tests can focus on that one particular task as well.
This was a common theme visible in all parts of this blog post series so far: identifying and defining responsibilities.
Just because we use a framework does not mean we're bound to all its defaults. Rails (or any framework, for that matter) is not always to blame if our application code becomes messy or the tests become slow. Using a framework should increase our productivity and not slow us down. Sometimes, though, we need to work a little bit harder to reap the benefits.