Better code with an inversion of control container
Last week we walked through the steps required to set up dry-container as an inversion of control container to manage our application’s objects and to make dependency injection easy and practical with dry-auto_inject.
Let’s take a moment now to consider how this arrangement improves our app’s design. It makes it easy for us to following a couple of important object-oriented design principles, and also borrow a trick from functional programming!
Dependency inversion
The container helps us implement the dependency inversion principle, an approach to decoupling modules in object-oriented programming. The principle states that:
High-level modules should not depend on low-level modules. Both should depend on abstractions.
and:
Abstractions should not depend on details. Details should depend on abstractions.
What does this mean in practice? Let’s look at some code that doesn’t follow this principle, and clean it up. We can bring back an example from our look at functional command objects:
class CreateArticle
attr_reader :params
def initialize(params = {})
@params = params
end
def call
result = ValidateArticle.call(params)
if result.success?
PersistArticle.call(params)
end
end
end
Here we have CreateArticle
, a high-level module, signifying an important user-exposed function of our app, depending on lower-level modules, ValidateArticle
and PersistArticle
. It’s also tightly coupled to the concrete implementations of these lower-level modules: their names are hard-coded right there in the class definition. These lower-level modules cannot be changed without consequences in the higher-level one.
Now let’s bring back this same code after we converted it to use dry-container and dry-auto_inject:
class CreateArticle
include AutoInject["validate_article", "persist_article"]
def call(params)
result = validate_article.call(params)
if result.success?
persist_article.call(params)
end
end
end
CreateArticle
now has more abstract validate_article
and persist_article
dependencies, which behave in a certain way. It no longer depends on particular implementations of these dependencies. As long as they continue to resolve from the container, we can adjust them or replace them and everything will continue to work.
For example, we might decide that our original validator implementation is no longer what we need, and we now want to validate each article by submitting it to a web service for bad pun detection. We can create a BadPunValidator
that responds to #call
, register it with the container as validate_article
, and now we have a completely different validation approach that continues to work with the high-level code. Looks like we’ve just … validated … this idea.
What we also see in this process is how CreateArticle
is the place we set the interface expectations for these lower-level modules. This is a subtler thing to notice in Ruby, where duck-typing rules and there is no formal notion of an “interface,” but it’s still in effect here. Any implementation that wants to behave as low-level “article validator” or “article persister” module must now adhere to the interface expectations set here in this high-level module (e.g. both must respond to #call(attrs)
, and the validator’s call must return an object offering #success?
, etc.). This is the inversion of dependency in action, the detail depending on the abstraction, not vice versa.
When we design our apps according to the dependency inversion principle, we gain a number of advantages. We focus more on interface before implementation. If a high-level module can easily have its dependencies made available, it can work with as few or as many as it likes, and in the way that makes the most sense to serve its own design. The tail doesn’t wag the dog. In doing this, we also work more with objects than we do classes. Since the objects are injected (thanks, dry-container and auto_inject!), this reduces the degree of coupling between our app’s components.
Our higher-level modules are now more reusable because they’re no longer coupled to specific implementations of the lower-level modules. Those modules are free to be changed or replaced, and the higher-level modules will continue to work.
Single responsibility
A nice thing about our CreateArticle
command is that it’s readable in a single glance. We’ve achieved this by spreading responsibility around the system, with its validate_article
and persist_article
collaborators performing some of the more specific tasks that it requires. A poorer version of CreateArticle
would see it handle some combination of all these responsibilities in a single place. This muddled version of CreateArticle
would violate the single responsibility principle, which states that:
A class should have only one reason to change.
We’ve already looked at one example of a change being made to our system, when we introduced the pun-based validations. That we only had to change a single thing meant that we already had a collection of objects designed according to this principle.
Designing our system around single-responsibility objects has always been possible, but we’re interested in making it easy, which is why this line matters so much:
include AutoInject[“validate_article”, “persist_article”]
Auto-injection almost entirely eliminates the friction of reaching for a new dependency when we’re building something and a new responsibility reveals itself to us. It aligns what’s right with what’s easy, and this ensures we’re left with a system that’s easy to change over time.
Function composition
Now that we’re free to work with a wide range of single-responsibility objects, we’re also naturally led to design objects with narrower interfaces. If an object only has to do one thing, then just one or a very small handful of public methods is its need, nothing more (try calling #instance_methods
on an ActiveRecord subclass if you’re up for a fright). The narrower the interface, the easier an object is to understand and use, and the less work it will be if you ever have to change it.
In fact – and it should be no small secret by now – my favoured interface for many objects is #call
and nothing more. That’s as narrow as it gets. Objects like this represent a command or an action, and #call
allows us to invoke it. Designed to preserve immutability, these objects are effectively functions.
When we have many functional objects, and they’re all registered in a container, then an auto-inject statement acts more like a function import:
include AutoInject[“validate_article”, “persist_article”]
And when we bring functions together like this, we’re actually performing function composition, a key element of functional programming. A system made of small, stateless functions, composed to build larger and more complex functions is a system that is both easy to understand (data goes in, data goes out) and easy to change.
Change is easy
We’ve looked at how each of these principles has a positive impact on the changeability of our systems. Change is a constant in the world of web app development. Change can also be easy, but only if we make it easy. By adopting an inversion of control container and widespread dependency injection, we’ve made it easy to follow some of the most helpful software design principles, and we’ve set ourselves up for easy future change, and for a more pleasurable development experience through every stage of our application’s life.
Next week, we’ll look at one more important principle for better web app development: a focus on verbs, rather than nouns.
Read part 1 of this series: My past and future Ruby
Part 2: Inactive records: the value objects your app deserves