Having waffled for a couple of articles I think it’s time for a code example. This one cropped up in my personal side coding project only a few days ago…
I’m currently building a wiki. The key feature of wikis is the links between pages, so in the wiki browser client I have this type:
type ResolvableLink = {
title: string,
url: string,
}
(The code in this example will all be in Typescript. If you don’t know Typescript, think C# and you won’t be far adrift.)
But, as in any wiki, there can also be links to pages that don’t yet exist:
type UnresolvableLink = {
title: string,
}
Now, throughout the client I occasionally need to create lists of links (for example the links visited in this client session), and each list can contain a mix of ResolvableLink
s and UnresolvableLink
s. What should the type of such a list be? Clearly something that is based on an Array:
type ListOfLinks = Array<T>
But what should T be, given that I want to include a mix of two object types?
One common starting point is to observe that the only difference between a ResolvableLink
and an UnresolvableLink
is the presence or absence of that URL. So I could write this:
type ListOfLinks = Array<{
title: string,
url?: string,
}>
By copying the internals of the link types, I have created coupling between them and the list that I can’t see just by reading the definition of ListOfLinks
. Nor can my IDE help me, because there’s no reference to either of the link types.
The type ListOfLinks
will always be coupled to the types ResolvableLink
and UnresolvableLink
, because those are the kinds of data I’ll be putting into — and getting out of — the array. But the type definition above doesn’t give me many clues about what that relationship actually is.
I’m calling this Implicit Coupling: if any of these three types changes in certain ways, only run-time checks will tell me that something has broken. And that means that this code contains surprises that could slow me down when I try to work on it, and could render any developer estimates useless. (I’ve represented it by dashed connections in the diagram.)
Instead of this, I want to have Explicit Coupling between these types. I want to be able to see their dependencies locally, without having to execute the code. So for this example, I want to couple the ListOfLinks
type explicitly to the types it could contain, ie. ResolvableLink
and UnresolvableLink
.
So I want to do this:
type ListOfLinks = Array<ResolvableLink | UnresolvableLink>
Or this:
type Link = Either<UnresolvableLink, ResolvableLink>
type ListOfLinks = Array<Link>
(using the wonderful fp-ts library by Giulio Canti). Both of these alternatives allow me to specify the types of the things I can put into the array. (When it comes to get them out again, well that’s a story of different coupling, for a future article.)
In both of these cases I can see the coupling locally, simply by reading the code here and now. I don’t need to run it, or go spelunking around — I can just see it here. This is what I mean by the term Explicit Coupling.
In diagrams I’ll represent Explicit Coupling with a solid connection:
In this example I will always have some form of coupling (although we’ll probably see examples later in which coupling can be removed). And given that the coupling is necessary, I want to choose Explicit over Implicit: I want to be able to see it locally — and maybe even have my IDE offer help. If I can do that I will have fewer surprises when I make changes. And surprises are bad, because surprises slow me down and add stress to my day.
There’s a lot more to say about the coupling in this little example — in later articles we’ll look at the code that uses these types too. But for now I simply wanted to introduce some vocabulary (as I intend to use it). I believe that Explicit vs Implicit Coupling is a simple idea that can transform the habitability of a codebase. Stay tuned and we’ll explore these ideas together.
I would love to hear about similar examples in your codebase, so if you can share, please:
Hello Kevin.
Do you think you could elaborate, e.g. with example(s), on what you mean by 'only run-time checks will tell me that something has broken' and 'I want to be able to see their dependencies locally, without having to execute the code.'
I am asking so that I can better understand what the problem is and also whether the reason why 'run-time' is being mentioned has anything to do with weak/strong typing and/or static/dynamic typing.
such union types is also something I really liked when learning elm (https://guide.elm-lang.org/types/custom_types.html). in java, we would model this with an interface and both ResolvableLink and UnresolvableLink implementing it?