Expand Your Interfaces
Most people think of a software interface as a specification of the functionality that an object provides (a so-called provider interface). This is true, but they’re so much more. If that’s all you use them for, you’re missing out. In this article I’m going to show you some more fundamental interface concepts that will allow you to design better systems and not be beholden to trendy architectures.
NOTE: I use the term object in this article, but you can substitute any encapsulated/bounded artifact: module, package, program, daemon, server, etc.
You can also specify what an object needs from the rest of the system as an interface. Such an interface expresses a dependency of the object (a so-called dependency interface). Interfaces of this kind are usually written up as “I need an object that does X,Y and Z.”
Doing so abstracts and decouples the dependency from the object. You can freely substitute different dependency-interface implementing objects, selectable at run-time. You can also provide mock dependencies for testing.
Dependency interfaces are also know by the dependency inversion principle (DIP). The phrase was popularized by Robert C. Martin in an article published
for the C++ report in 1996. Martin talks in terms of higher-level modules versus lower-level modules. He is correct, but the principle is much more widely applicable. Interfaces are the mechanism for decoupling anything from anything.
But there is another benefit of specifying dependency interfaces. It explicitly
defines the scope of the knowledge that is inside of the object. It shows the
terms in which the object does its work.
Take for example an object that has the following two interfaces (Example given in golang
Without looking at any implementation, it’s obvious that this object executes
queries on a database to construct and return patient objects. This gives you an idea of the boundaries of the object, where its responsibilities lie, and how it interacts with the rest of the system. It provides a very gestalt, right-brain view that is easy to reason about its place in the system.
You can also infer a bit of the knowledge the object must have. For example:
- Particular SQL dialect used to query the database
- Database schema details
- Fields of a patient object
Oddly, the main push for explicitly defining your dependency interfaces isn’t for good system organization. It’s so that it’s easy to test your object in vitro. For this one, there are three obvious ways:
- use a real database
- something in memory that emulates a real database
- test the SQL strings that the object is generating (mocking)
The concept of a dependency interface is closely related to the concept
of duck typing, which gained popular usage with the Python language.
As the official docs say:
A programming style which does not look at an object’s type to determine if it has the right interface; instead, the method or attribute is simply called or used (“If it looks like a duck and quacks like a duck, it must be a duck.”) By emphasizing interfaces rather than specific types, well-designed code improves its flexibility by allowing polymorphic substitution.
Dependency interfaces let you explicitly codify exactly what kind of duck you’re looking for. Dependency interfaces also allows you to use duck-typing in less dynamic languages like Java. Go’s interface type system was explicitly designed to allow modeling duck-typing-like behavior in a static type system.
Another use of interfaces is for inspection, instrumentation, logging,
measuring, benchmarking, probes and debugging. I call these maintenance interfaces.
All systems at a certain level of complexity have to grow such facilities. Once you’re able to flip a switch and see what an object is doing at run-time in production, you’ll wonder how you ever lived without it.
In a way, these interfaces are explicitly anti-encapsulation. The whole point is to see what the object is doing under the hood. But this is fine, because the users of these interfaces are debugging consoles and log files, not application logic.
In practice these interfaces usually look like visitor patterns, something
“injected” to be called in specific places. Objects should provide default null interfaces that are effectively no-ops, or allow nil values that are conditionally checked internally.
If you use dependency interfaces, you get the ability to monitor the interactions with the dependency by wrapping the dependency with a monitoring pass-thru. The monitoring pass-thru provides the exact same interface as the dependency it wraps, only it performs instrumentation as functions are invoked.
From the previous ExecuteSQL() example, you can create a SQLLogger object that wraps a real SQL executor.
The beauty of this is that neither the repository or the database has to know anything about the logger. True maintenance interfaces have to be provided when dependency debugging isn’t good enough. What needs to be examined is the real logic inside the object.
Humpty Dumpty Had a Great Fall
But after all of the dependencies have been decoupled, how do you put it back together into a working application? In the mainline of your program of course. Examples of mainlines include:
- main() in C/C++
- User Interface event handler
- WSGI application running in a WSGI server
- Java Servlet
The important thing is that this mainline is close to the process or API boundary of your system. It should be as close to the user as you can get. It’s important not to encapsulate small dependency structures deep inside your program. Doing so nullifies most of the benefits.
This mainline goes by many names, each with their own twists. Here’s a few
- Controller (Model View Controller)
- Service (Service-oriented Architecture)
- Application layer (Clean Architecture and its many variations)
- Dispatcher (Flux)
- Mediator (Gang of Four)
- Context (Data Context Interaction)
The one that gets close enough to the core idea without too much cruft is
controller. But really there is no need to over fit the concept. You can coordinate any sets of objects, regardless of their responsibilities. It even works well for batch programs that have no “views” or “user interface” at all.
At the end of the road, when all of the modules are completely decoupled from each other, you have a big pile objects that you need to put together to make your application. As you implement the user interactions and use cases in your program, you will start to see patterns of assembly. These patterns can become libraries of assembly and coordination. These assembler objects are commonly called Factories.
By going through this process, the assembly logic will start to clump together organizationally. By removing all of the explicit dependencies from your objects, you are able to centralize where all of the concrete dependencies are defined.
Diagrams of the concrete run-time dependencies gives a high-level view of the organization of your system, like a road map.
Depending on the problem domain you are working in, these dependencies and application objects could end up getting very high levels of reuse. This will mainly depend on if your system is designed top-down or bottom-up.
Bottom-up design promotes more reuse at the expense of combinatorial complexity. Top-down design promotes more single-use objects fitted to exact end-user use cases. Ultimately, all systems are both. It just depends on the granularity of the concepts that are modeled.
With highly reusable systems, some write the assemblers in higher-level languages. End user requirements can be met more quickly by assembling existing objects in an easier to use language. If a new object is needed, it is written in the lower-level language and then used in the higher-level language. This is the ultimate end road of any system that gets enough reuse and users under its belt. And it’s been happening over and over since the inception of computers.
Old Gray Beard
UNIX programmers and administrators learned this decades ago. The defacto assembler/controller in UNIX land is the humble shell script.
These scripts contain a list of commands, each one handling input text files (dependency interfaces) and providing output text files (provider interfaces).
The programs are highly configurable by means of arguments, switches and environmental variables. They have maintenance interfaces like verbosity flags and debug files.
A grand maintenance example is a feature of the bash shell (The default shell on virtually all Linux distributions). If a script runs the set -o xtrace command, bash will print out all of the commands it executes.
Shell scripts also have the time command, which can be used to record how long a particular commands takes.
But really that’s just the tip of the iceberg. You could say that learning to be a UNIX system administrator is learning how to deploy, measure, inspect, debug and trace objects(programs) written in lower-level languages.
For other such Unixy insights, I highly recommend Eric S. Raymond’s book The Art of Unix Programming.
I hope you’ve seen some of the more fundamental uses of interfaces in modern programming languages, and how they can help tackle complexity and monolithic design. Many of the popular architectures in use these days rely on these underlying concepts to create their models. Maybe you can push the rock forward by coming up with some new ones yourself.