Imagine this scenario:
Two people are sitting at a conference, side by side.
One of them says, “interfaces aren’t everything”.
The other person says, “I would never work in a language without interfaces”.
This actually happened. The first person is me, and the second person is someone I happened to sit next to for a session. I don’t know them and was actually mumbling to myself.
Every once in a while something comes around which reminds me of this interchange. Me, talking to myself about interfaces, the other person interrupting my conversation with myself.
So, here’s the deal: not every programming language has interfaces. Even object oriented languages don’t have interfaces.
WAT
If you work in one such interfaceless language, here’s the gist:
1 | public interface IDBConnector { |
This jumble of words is defining what a class would look like that behaves like IDBConnector
. If it doesn’t all mean something to you right now, that’s alright. All it really means is, anything that uses this interface MUST adhere to the definition.
I can think of two object-oriented languages offhand which do not have the notion of interfaces at all: JavaScript and Io.
The rest of this post will use JavaScript to demonstrate:
- Why we should care
- How we can gain some of the goodness that comes from having an interface
But First, Dependency Injection
In case you aren’t familiar with dependency injection, and specifically constructor injection, I’m going to give a very fast crash course in the topic.
Dependencies, Dependencies
With any sufficiently complicated program, you will eventually need to break things into separate files. Ideally those files will represent some contextually meaningful part of the program. Moreover, things may get reused.
All of these smaller parts of your program are dependencies. Something, somewhere will depend on some code written in another file, module, class, whatever. It’s a fact of life.
Object Oriented Dependencies
In object oriented languages it is common to see dependencies introduced in the constructor of the object they will be used. For those of you who are familiar with dependency management, I apologize for what you are about to see.
You may have something like this:
1 | const DBConnector = require('./DBConnector'); |
At first blush, this might seem fine, until you want to test things, or… you know, change your DBConnector
. All of a sudden you find yourself in dependency management hell, or worse, testing hell.
If your dependencies are created where they are going to be used, you can’t break them out to get tests in. By the same token, if everything, everywhere, is depending on that EXACT DBConnector
file, changing to a NEW connector will require going around and touching every file that used the old one.
This is sometimes referred to as shotgun surgery.
Ew.
Dependency Injection
We can take a different approach to handling object instances and dependencies. We can declare a constructor which takes dependencies as arguments, eliminating the need to instantiate (new up) every object in our constructor, and decoupling our code.
1 | class TodoDataService{ |
All at once, if you change your DBConnector
, as long as it behaves like the old one, you can swap the file in one place, and be done. Everything will get the new one for free.
…and testing. Testing becomes magical!
Since you can jam anything you want into that constructor, your test can provide a fake DBConnector
and your code will be none-the-wiser.
SO COOL!
(or so it would seem…)
Back To The Interface
Dependency injection in JavaScript breaks some very important stuff. If you aren’t requiring (or importing) your files, you don’t have a direct line of sight to the contract for your dependencies. Since you don’t have a link to the file, you also lose all of your code hinting in your editor.
BOOOOO!!
HOWEVER, you can pull your fat out of the fire by emulating interfaces!
I contend, what you want most from interfaces are:
- insight into a code contract
- communication when something that implements the interface missed a required method
Fortunately, you can do all of that in JavaScript with classes (or prototypal objects), and default parameters.
YAY!
Creating the Interface
Let’s dig into how we can create an interface using a simple class structure. What we will end up with is going to be more akin to an abstract class, but that’s a discussion for a different day. We work with what we have.
Let’s suppose I am creating a DBConnector
class which will provide an API to send queries to a database. I want something to reflect what the API will look like, while keeping people from directly using the interface for anything. The approach I prefer is, any method attached to our interface-like structure would throw an error on call.
Let’s list some of the expectations:
- There should be no constructor. An interface has no logic, so there should be nothing to construct.
- All interface methods must represent the real contract as closely as possible to the way methods will be defined in the classes which implement our interface.
- All methods attached to our interface should throw if they are called directly.
Here’s what I landed on for for a first pass:
1 | class DBConnectorInterface{ |
Using Our Interface
It’s not a very exciting bit of code, but I believe it gives us the insight we are looking for. Now, if we construct a concrete class inheriting from this interface, it would probably look something like this:
1 | const DBConnectorInterface = require('./DBConnectorInterface'); |
Dependency Injection and the Interface
Now that we have our interface, and we know roughly what dependency injection looks like. Let’s have a look at how our interface can help us in our day to day lives.
Our interface, as we’ve been calling it, is actually instantiable – i.e. you can do something like new InterfaceThing()
– so we can use it like a real value. JavaScript is a dynamically typed language, so we can’t get any feedback until our program is run. This means that an interface that is a value is likely just what we need!
If we use the magic of ES-Next default parameters, we can guarantee we always have a real, usable value. This might seem like a strange thing to want in our code, but we get a really nice side-effect. Since we can have an interface instance, that is linked to real code, our editor will actually be able to give us all of the code hints we know and love!
Let’s look at what our final TodoDataService would look like with dependency injection, using our new, fancy interface value:
1 | const DBConnectorInterface = require('./DBConnectorInterface'); |
Voilà!
Our data service hasn’t undergone much of a change, however, the DBConnectorInterface
instance as a default parameter for our constructor gives us a close enough approximation to a static interface that we get a bunch of benefits:
- Our API surface is now clearly defined in a standalone, presumably easy to find source document.
- We get IDE support! The editor can actually read our interface definition and use it to provide code hinting while we work.
- The interface has no dependencies, so it is safe to require in anywhere, including tests, and documents with potentially unsafe dependencies – like those that might interact with the outside world.
- It’s a totally inert change to the code, so if we have a class we want an interface for, we can introduce one without changing the outward behavior of the code.
So What?
If you are using TypeScript, this might be a big “so what”, you have interfaces. If you are not using dependency injection, this might also be a big “so what.”
HOWEVER, even if you aren’t using dependency injection, you may find it helpful to create an interface, especially if you rely on common patterns, like the factory pattern.
It can also be quite useful to have an interface to work from when writing test code. By providing a contract that must be followed, test doubles can be created more easily, if by no other means than simply inheriting from the original interface, and overriding what you need for your test.
In the end, the use of this pattern in JavaScript, or other language lacking interfaces, can provide significant help when creating software that needs to be easily understood and supported now, and into the future.