On the Value of Interfaces (and when you Need One)
·It is curious how people tend to bash DDD. I must admit - I never worked in a full-on DDD codebase or on a team that practices it, but looking at the mentioned articles like this one does make me shudder a little. There is little worse than a premature abstraction, and a there is a noticeable jump (or rather: a trough) which goes from abstraction to indirection. I’ve been programming for more than 20 years now - 12 of those professionally (with a little stint in-between) and I also went from obsessing over abstractions to a more, let’s say, “common sense” approach to them. Oddly enough, this is not about OOP for me - it is about modules. And, to an extent, types - but I do believe types and behavior are going to stay connected in meaningful way. Whether you do point.move()
or move(point)
is not of importance as long as it generalizes over some kind of Movable
.
It did take a while for a more digestible take on this to begin to crystallize, so I fugured it could be put on paper.
SOLID principles are not as much about objects as they are about modules
The key in “getting” the SOLID principles is that they allow modules to be swapped for one another, within reason. Abiding by those principles make modules easier to swap. Not more, not less. There could be a discussion about “X or Y makes things easier to reason about”, but I find the “reason about” take to get wheeled out when one wants to bash another’s programming paradigm, and we’re not here for that. What this is about applies in equal measure to both the super-strict languages like Idris and to the wildest dynlangs like Ruby.
The principles allow us to make software that composes better.
Don’t overdo it
As in the example provided by the lone architect - let’s quote:
It’s fine for most simple use cases. It’s very readable, barely testable and it’s all in one place. It just does the job. But there’s many caveats :
Are there though? Is there anything else that needs to do things to the users
table in the database? Why is this code “barely testable”? Is making a DBConnection
available in the scope the function runs impossible? And of course it is perfectly testable - feed it a request and a response, make sure the request contains the two params, make sure the response got filled with a JS-object-ish thing having the properties id
and emailAddress
, make sure there is a row in the database. It just doesn’t need the modulization and the encapsulation and the interfacement smeared all over it in thick gobs. When it does become interesting to change it for the better though, is when (and only when!) additional requirements arrive - which are also preferably present not only within this function. For example:
- What if we need to encrypt the password? (if there is anything wrong with this example function - this is it, fixing this would make it near-perfect)
- What if we need to be able to set the password from a commandline tool, not only from an API web request?
- What if we need to store our users someplace else (if we have decided to take a large contract with an auth-as-a-service-SAAS-company, because those never have data breaches - and are thus more secure than our DB
Now thest two would be much better examples of what can change in this function. But if we need none of the above: “all in one place” is priceless. All in one place is easy to find, easy to read in one sitting, easy to scan for bugs, and you don’t have to jump between files or classes or what-have-you. All in one place is good. Do have things in one place.
Modules compose over interfaces
Your modules are like an AC power outlet and your appliances, and they compose (integrate) over the interface of a power plug. It is that simple. The presence of an interface makes sense if, and only if, there are multiple modules on either end of the interface and those modules can be swapped for one another. No multiple modules = no interface, YAGNI, KISS, don’t do it.
Defining interfaces
But what if you do need to swap implementations? Well, then your modules will integrate (compose, plug, connect…) over some kind of interface. Defining good interfaces is key. They should be small, so that they are easy to understand. An AC power plug is very easy to understand. They should not change without a big need to do so, or change within reason and allowing existing use cases. An AC power outlet with a ground socket is compatible with an AC power plug without a ground pin. Interfaces should be well-tested and well-communicated - a power outlet and a plug are well-specced items.
Coming from dynamic languages, I find that “enforcing” interfaces is very rarely justified. Every use case which is often excercised is going to reveal corner cases which are not covered by the existing interface. Instead of enforcing it (be it with language constructs like final
, or with calling conventions like not allowing additional fields in a proto message or additional properties in a JSON object) define the minimum viable interface the caller needs to conform to. The smaller the interface, the easier it will be to conform to and to understand how it works. Even smallest interfaces will have corner cases - the Read()
call in the io.Reader
interface in Go has ambiguous behavior (is EOF an error or a read of zero-size?). A big interface makes a perfect breeding ground for corner cases big and small (did anyone mention ActiveRecord?)
Plugs and sockets, all the way down
An interface is just that - it is a set of rules by which two modules can interact with each other. The most obvious and familiar interface is the connection of the mains outlet in your office or apartment. The power outlet on the wall is a module. The plug on the end of the mains cord of your fridge is another module. The way by which these two combine to make a connection is the interface. The interface specifies a whole lot of interesting details:
- Whether the connection must pass earth or not (is there a pin that sticks out and disallows you to use non-grounded appliances?)
- That this is a single-phase connection
- That this connection is AC - if you can rotate your plug by 180 degrees and it will still work
- and many other small details
Whether to have those interfaces (or contracts) in your system is the decision you need to make.
My beef however is this: DDD creates interfaces between the internal components of your system. A lot, a lot of them. Most of them will never be used to their potential, because you will never swap a UsersRepository
for an Auth0Repository
- and if you do, you will likely find that your interface is sorely lacking. The problem with interfaces is the same as with any other kind of process - you can’t design them in a vacuum. Creating the power outlet and plug took multiple iterations over multiple decades - and most importantly, the need for swapping modules at both ends of the interface was always present. Creating an interface for just this one login page to talk to just that one UsersRepository
is nothing of the sort.
Do I actually need these layers and modules and interfaces and oh my?
So, hereby: rules of thumb for deciding whether you need an interface. I’ve followed this for quite a while, and they rarely failed me. At least when the climate was right.
Simple! Think about the answers to the following questions. You don’t have to tell me, because there might be a couple of spicy ones there. Just think for yourself. If most of the answers are “yes” - you might. If most of the answers are “no” - likely you won’t, or at least not in this stage of the system’ development (or - not in this stage of the development of your organization).
- Will the modules/layers of the system have to be substituted for other implementations, right now? Mains analogy: yes, both the plug and the power outlet may be replaced. Any appliance might get plugged into the power outlet that is being worked on. An appliance being worked on may need to be plugged into a different power outlet.
- Is it so that right now different teams (especially teams that don’t see eye to eye, or have different incentives) will be working on different layers? Mains analogy: it is the manufacturer of the appliance who produces the appliance. And it is your local electrician who is responsible for adequately mounting the power outlet. They are different entities, they have little in common, and they are likely to have different incentives.
- Is it so that the modules/layers are going to be using different technology, or be developed in different programming languages? Mains analogy: well, this is a bit silly of course, but an appliance can be produced at a factory and sealed in epoxy, while a power outlet might be connected to 50-year old copper wire in your basement.
- Are there any strict standards a module needs to conform to, while other modules in the system would not? Mains analogy: a power outlet needs to conform to building code. And an appliance must conform to earth shielding, magnetic emission regulations, and any other legalities that apply.
- Will using an interface make using at least one of the collaborating modules vastly more convenient, right now? Mains analogy: you can eschew a power outlet and a plug, and just twist the naked copper wires together. It would totally work, but it would make things very inconvenient even for slightest changes - like moving an appliance or vacuuming the space around it.
- Is there a sizeable body of people (contractors, teammates, customers) who just cannot in their right mind use the system and not break it, unless the usage is guided by extremely strict, narrow constraints - and should therefore be “frozen” and documented? Mains analogy: A power outlet is designed in a certain way so that toddlers would be less likely to stick their fingers in it, and it is helpful that all power outlets are the same.
The more “yes”-s you have collected, the more reason you have to do layering/interfaces/modules in this particular scenario. If this made you doubt - the answer is likely a “no”. Don’t do modules, don’t do interfaces, don’t do architecture astronautics. Hexagonal, pentagrammatic or tetrahedric - just don’t.
Note that I am liberally sprinkling this with the words “right now”. If you do not need it now - you might very well need it in the future, but you will be designing your module and interface then, not now. By then, your system will have gestated and it is likely to scream at you where to introduce seams.
Seriously: if you do not need it now - you do not need the hexagonal architecture, and you don’t need the Repository pattern, and you don’t need the bounded context, and you might not need the Clean Architecture.
Applying the questionnaire to avoid premature layering
It is hard to slice a sandwich in thin slices - especially so if the sandwich is already thin. This article shows us a layering of a hypothetical Ordering
microservice in an “Application layer”, “Domain model layer” and an “Infrastructure layer”. There is a number of alarms here suggesting this to be a questionable proposition. First: if your microservice needs 3 layers, is it really that “micro” to begin with? Second, let’s apply our questionnaire to that microservice.
- ❌ Will the modules/layers of the system have to be substituted for other implementations, right now? Clearly not - there will be just one implementation of persistence, only one implementation of domain logic and just one web API endpoint.
- ❌ Are there any strict standards a module needs to conform to, while other modules in the system would not? Clearly not - this is a self-contained microservice
- ❌ Is it so that right now different teams will be working on different layers? No because it is not specified - we will assume the Ordering Microservice is not getting massively hired for this quarter
- ❌ Is it so that the modules/layers are going to be using different technology, or be developed in different programming languages? No, it is .NET all the way down
- ❓ Will using an interface make using at least one of the collaborating modules vastly more convenient, right now? No as they all live inside this microservice and won’t be exported/exposed in any other way. One could say that it is probably good to separate the web request/response handling part and have some form of interfacting to the rest of the system, but no extreme need to do so.
- ❓ Is there a sizeable body of people (contractors, teammates, customers) who just cannot in their right mind use the system and not break it? Not quite - this is not specified, and we will assume people editing the Ordering Microservice at least know .NET The consistency of web calls can be maintained with a simple schema validation of the input and well-tested output. That is the only interface this system deserves.
Therefore: should the Ordering Microservice be layered? Judging from the article: hell no.
Interfaces and layering are cool, but once you actually have use for them. In other situations there is likely an entity trying to sell you a bridge.