This is the second post in a series about my experience of applying a GOOS-style hexagonal architecture to a Ruby on Rails application. In the first post, I talked about why the idiomatic, connected, style of Rails architecture might have advantages in the short term, but that I want a more modular architecture to make it habitable for the long term.
In this post, I’ll explain a couple of the fundamental concepts that will be used in the rest of the series, which should give you some new ideas to think about as you’re working on your own Rails applications. We’ll talk about the tell, don’t ask style of object communication, and about the difference between objects and values. Let’s start by explaining what that funny term hexagonal architecture actually means.
So what is a hexagonal architecture?
The fundamental idea behind this style of architecture is to isolate the core domain of your program—the bit that defines its unique and interesting behaviour—from the technical infrastructure that enables it to talk to the outside world. Technical infrastructure means things like databases, file systems, user interfaces, web services and message queues.
We can visualise it as two hexagons, one inside the other, as in the diagram below. The inner hexagon contains our core business logic. It doesn’t know or care anything about the details of how it will communicate with the outside world, but it exposes ports where we can plug in adapters that fulfil that role. The adapters sit in the outer hexagon, and translate between our core domain and the technology-specific domain of a particular piece of infrastructure.
That’s it, nothing fancy. The word hexagon isn’t actually really very important but I like to think it as a metaphor for the modular, pluggable nature of this architecture. It’s catchier than ports and adapters, too.
Keeping this separation between the inner and outer hexagons, between the core behaviour and infrastructure details of our program, gives us a few advantages:
-
Our code is easier to understand. When we’re working in the inner hexagon, all the language and concepts we see are related to our core domain, without the distraction of the technical realities of connecting it to the real world. When we’re working in the outer hexagon, we can concentrate on the job of mapping core domain events and data onto the domain of the technical infrastructure that we’re writing an adapter for, without the distraction of business logic.
-
We can easily replace pieces of infrastructure, such as swapping out a relational database for a key-value store, or using an in-memory database for fast acceptance testing, without modifying the core domain at all.
-
Our tests can run fast. The code in the core domain does not depend on Rails or any other large external libraries, so its tests are lightning quick. Once we start to recognise core domain logic and push it inside the inner hexagon, more and more of our tests are running against plain old Ruby objects that are quick to instantiate. We have separate tests for our adapters that ensure they integrate with their respective technology, but we don’t need to run them as often as we used to. We only need to run those slow tests when the adapter needs to change, and because our business logic has moved into the core domain, that’s not very often.
There are apparent costs too. As I’ve moved the Relish codebase towards a more hexagonal style, I’ve noticed that there’s more code appearing. However, each piece of code is simpler to understand, and the pieces are much less coupled together, giving me flexibility for the future.
For me, probably the biggest factor in achieving this flexibility has been finally understanding an object-oriented design axiom I’ve been hearing for a long time: that objects should communicate by telling each other things, rather than asking each other questions.
Tell, don’t ask
Your Rails application most likely has a problem with procedural code. When I talk about a procedural style of programming, I am talking about this kind of thing:
class PublishersController < ApplicationController
def create
@publisher = Publisher.new(params[:publisher])
@publisher.creator_user = current_user
if @publisher.save
current_user.add_membership(@publisher)
redirect_to publisher_collaborators_path(@publisher),
:notice => 'Publisher created successfully. You can now add collaborators.'
else
render :index
end
end
end
This is a fairly typical Rails controller action method. We can read this code and quite easily understand what is going to happen when it runs. This is the advantage of a procedural style – it reads like a story. The problem with this style really hits us when we want to change the behaviour.
Imagine we want to add an activity feed to our application, so that as a side-effect of successfully creating a new publisher, we want to post a message to the user’s activity feed. The way things stand, we’d probably be inclined to squeeze another line into the top branch of that if
statement:
...
if @publisher.save
current_user.add_membership(@publisher)
current_user.feed_entries.create! content: "Created a new publisher #{publisher.name}."
redirect_to publisher_collaborators_path(@publisher),
:notice => 'Publisher created successfully. You can now add collaborators.'
else
...
Over time, as more and more behaviour is added to the application, this is only heading in the direction of chaos and ugliness. A more object-oriented design would allow us to plug in the extra activity feed behaviour without modifying this code at all, and it’s the tell-don’t-ask principle that will guide us into producing that kind of code.
We don’t have space for an example here, but we’ll see plenty of them as the series unfolds.
Your domain is in the protocols
Let’s examine that controller action again, looking in detail at the messages passing between the different objects in the scenario where the publisher is saved successfully. All in all, there are six different actors in this little scene: the Controller, the Publisher class, the new Publisher instance, the current User instance, the user’s Feed Entries collection, and Rails itself. Let’s imagine this method as a conversation between them:
Controller: Dear Publisher class, please may I have a new instance using these parameters? [Query]
Publisher Class: Certainly, here you are. [Query Response]
Controller: Publisher instance, this User instance is your creator_user
! [Command]
Controller: Publisher instance, attempt to persist yourself! [Command]
Controller: Hey Publisher instance, did the attempt to persist yourself succeed? [Query]
Publisher instance: Thanks for asking! Yes, it went perfectly. [Query response]
Controller: User instance, grant yourself membership of this publisher instance! [Command]
Controller: User instance, can I see your feed entries collection please? [Query]
User instance: Oh alright then, here it is. [Query response]
Controller: Feed entries collection, create and persist a new entry with these parameters! [Command]
Controller: Rails, redirect to the following path, and show this message to the user! [Command]
In total, there are eleven different messages passing between the six different objects in play as this method runs. Of these, five messages are commands, and the other six are queries or their responses. We’re not even counting the potential for the call to feed_entries.create!
to raise exceptions – we’ll talk about that some other time.
A revelation for me as I re-read GOOS recently was this: Your domain model is not in the classes you create in your source code, but in the messages that the objects pass to one another when they communicate at runtime.
In other words, the structure of these conversations—the protocols—is what’s most interesting about your application. The simpler you can keep these protocols, the easier your code will be to change in the future. Code that involves queries is inevitably more complex, because the code asking the question needs to then act upon the answer. Using a tell, don’t ask style encourages us to push that action off into other objects. Smaller, more re-usable objects.
So is there ever a valid case for using queries? There is, and that’s when the object is a value.
Objects and values
The authors of the GOOS book distinguish between just two different categories of class in an object-oriented program: objects, and values. As Rails programmers, we can find these two categories confusing because many of the objects we’re used to dealing with, particularly ActiveRecord::Base subclasses, fit into both categories. Nevertheless, I’ve found these two labels extremely useful in helping me think about how I structure my own core domain code.
Objects are the things that get work done. They may carry state, but they don’t expose it. They receive messages from collaborating objects, and make decisions about what messages to send to their collaborators.
Values carry state. They’re immutable: once created, they can’t be changed. Values have query methods that either return component parts of their state, or the results of calculations on that state.
So I’ve explained that a hexagonal architecture involves separating the code that allows your application to talk to the outside world, from the code that is the application itself. I’ve talked about how, when designing that code, it’s important to think about the protocols between the objects, and I’ve talked about the difference between objects and values.
I realise that these first two posts have been fairly abstract and theoretical, but I really felt I needed to put down some terminology before we could continue with the story. In the next post we’ll get much more practical and look at a pattern for applying tell-don’t-ask in my Rails controllers which I’m calling passive controller.