Intentional Code
[1:57] Viral tweet of a directory folder in an objectively shitty codebase
the thing that really jumped out to me of this particularly shitty codebase was how thoughtless it all was. Like, completely, mundanely thoughtless.
Organizing code by architectural feature is way up there on the list of shitty things you can do to your software with no real meaning
As the joke goes, organizing your code by architectural concepts is like getting a classic car collection, smashing it to pieces and organizing it by wheels, windscreen wipers, and seats. It ceases to look like anything, certainly not cars, when you do that.
[3:35] This isn’t a talk about Design Patterns
The notion originated via Christopher Alexandra’s “Notes on the Synthesis of Form”, a work which descriptively classified architectures in real-life buildings into patterns.
In the late ’80s Kemp Beckham Ward Cunningham started to write about pattern languages
The gang-of-four book came out in 1994.
One of the core mistakes people make is they thought this was these people telling you how to structure your applications, and all they were really saying is here are some things we have seen a couple of times.
Between the GoF book and many related efforts (from Kent Beck, Martin Fowler), the usual goal of these books is to provide concrete guidance and descriptions of common solutions to common problems that have been seen in the market.
Giving these things names gives people a way to talk about them, which is great since our industry is still in its teenage years.
Patterns try and make categories of problems look the same. This [talk] is about all the things that don’t look the same.
[6:15]
I suppose I just kind of lament the fact that most of the talk of design and software turns into talk about design patterns, because they’re kind of the least interesting part.
If design patterns are all about the parts of the system that are familiar. Familiar answers have familiar questions. But with the acceptance of that, we kind of pushed programming languages to innovate and to make some of those patterns obsolete over time anyway.
Software as Literature
[7:45]
Back in 1984, Donald Knuth wrote Literate Programming. He tried to get traction on the idea of combining documentation with source code.
This makes a ton of sense, the languages of the ’70s looked more like assembly language, ALGOL, stuff like that. Modern programming languages are far more literate and expressive, so in some sense things moved in that direction anyway.
So over the years I come back to this really really precise definition of what software really is. Like when you strip everything away, like what on Earth is software.
Software is a constrained form of communication between programmer and programmer that describes concepts or problems that happens to run on a machine.
The more you think about it, the more it starts to make sense. You spend a much larger portion of your time, of your working day, reading code than you do writing it. And code that exists in a context often has subtext, it has authorial intent, much like any other written language.
And actually, often, like, if you think about how defects happen in software, most of it is a horrible dissonance between what the author intended the software to do–what their intention really was–and how they managed to capture it. Just like it is in written language.
Actually, all the navel-gazing conversations about kind of linting and format is all actually really about flow and form and rhythm, like any other rhythm.
Burn all your copies of “Clean Code”, it’s aged poorly. It’s cast a long shadow over all software design conversations, where all meaningful, thoughtful discussion is replaced with discussion of SOLID principles.
I was having a conversation with someone the other night who made the casual observation, you know, the Liskov Substitution Principle only got into SOLID just so it could make a word.
Intentional > Clean
[13:19]
I want you to approach the work as if every single thing you type is important, it’s intrinsically part of the work.
Code written with thoughtfulness and intentionality is code that survives, that humans can comprehend and that’s what makes software fundamentally good.
[14:30]
Thinking of your software as literature lets you value different ways of communicating in code.
On the macro level, organizational patterns. e.g. MVC
MVC communicates “this is a website”, which is a decent first-order approximation. Of course, everything is a trade-off. This organizational pattern communicates nothing more than “this is a website”. So “what does this website do?” is actively obscured.
On the micro level, form and flow are tools to build cohesion
[15:52] Code example about form and flow
Block of code with a lot of whitespace between statements.
It forces you to comprehend everything statement by statement.
Now, if I was writing an English essay, I would use paragraphs in text to capture intent, to collage common Concepts. Now, code is really just the same. Now, lines mean something, and if you use them everywhere or nowhere, you’re removing a form of expression.
Examples that fit on a slide don’t really illustrate the aggregate effect of doing this to large swaths of code.
Code with great form is like poetry, it leads the eye of the reader. It telegraphs how you’re meant to read the program.
The complexity of your application should be at most as complex as the problem space that it inhabits, and no greater.
Every poorly designed piece of software I have ever seen exists where the technical solution dwarfs the scope of the problem. That’s bad, you’re making the world a worse place, you’re making things harder.
So many systems just straight out fail this test, you know, like, they lose themselves under infrastructure code, under build systems, under deployment, and modules and pipelines and re-use and all these grand ideas about their eventual destination, without really focusing on what the correct form to express the solutions of the problem they have right now is.
[19:24] Designs exist to solve problems, to communicate intent using form and function.
Design is temporal in nature: designs only really make sense in their context, in their time. The legibility and recognizability of a design to its prospective users benefit from the fact that design is an iterative process. The convergence of phones and walkmans into a single device is a consequence of realizing that these two devices both transmit audio.
People seem to believe that design is something you do once upfront, and hope you get it right. Whereas processes like TDD are meant to be added to iterative design processes.
It’s probably worth revising our notions of software design around the fact that we now rarely write software for mainframes, nor even for desktops anymore. And our languages have made some categories of design irrelevant too.
Structural Design is often too big
[24:00]
Most of the things you think of as best practice are probably just overblown structural design practice. e.g. most microservices are neither micro nor services, they are 80% distributed monoliths glued together with messaging, so that the flow control is completely opaque.
So you end up with is these big monstrous machines where the intent of the code–the thing that it does–is like 15% of the work, and how it orchestrates and does it is all the things people waste their days doing.
These prescriptive notions of structural best practices add a huge number of single-use components, which explodes the surface area of the software, and explodes the complexity and cognitive load the software places on anyone trying to reason about it.
Known-Good Designs
[28:22]
There are problem spaces where there are known-good designs
RPC-style systems (like HTTP APIs) map really well to Command/Query patterns.
In these, the form and the flow of the application matter even more, because you want the shape of the code itself overwhelms the shape of the application in order to achieve legibility.
When your macro level design is comfortable, you risk code blindness. So as a reader you start to skim and you stop seeing the differences. And that’s how bugs happen, right, when the programmer doesn’t comprehend the complexity of the thing they’re looking at.
You don’t want everything to look the same
[29:38]
Our job, when we’re designing software, is fundamentally a war with complexity.
If it’s not patterns, what makes good code?
Now, there’s this kind of distressing realization, when you reach these lines of thinking, and that’s that design patterns are precisely so popular because it gives people a way to reach towards something where the feel is good–they like the answer they seem to have reached–a definite conclusion, a place to go, without having to actually think about how you got there. Like that’s their magic trick almost, like here’s an easy answer, go with it.
Once you’ve seen enough codebases and enough languages, you kind of realize that most of the arbitrary stylistic qualities of code that people focus on, like tabs versus spaces and brace style are basically totally irrelevant, like, all that stuff is total noise. And instead there are a couple of non-negotiable qualities you look to in a codebase, that are far less regimented than your average, like javascript style guide.
- Code that is easy for someone with minimal ‘domain context’ to read
- Code that focuses on developer experience (debuggability and usability)
- Code where the intent takes up more visual space than the language syntax (easier to see what you’re doing instead of how you’re doing it)
At a meta level, all programming is abstraction: composing and encapsulating provided functionalities.
The file open API of any programming language is a great example. It abstracts away the file APIs of the operating system, which in turns abstracts away file systems, the entire file/folder analogies, the specific device drivers involved, which abstract away the specific hardware.
[37:00]
If you expose a module (package, class, binary, whatever), the interface represents the complexity it imposes on the program that consumes it. If you think of that module’s interface complexity as a function of how much complexity it abstracts for you, then you get a sense there are ‘deep modules’, where the interface is far simpler than what it abstracts away, and ‘shallow modules’ where the interface is not much more complex than what it abstracts away.
Shallow modules bleed their concerns out and increase the cognitive load of the system.
With the mental model of shallow modules, it follows that functions with many arguments are bleeding a substantial amount of complexity to their consumers.
Finding the Right Abstractions
It may seem like painfully obvious advice, but abstractions and generally any other module of code you find should contain the problems a single problem space entirely.
This means that code with lots of pass-through methods are bad: because you’ve introduced an abstraction without abstracting anything, the pass-through method is the smell.
[40:00]
You get to this certain point where you realize that all talk about design mostly dissolves into sort of discussions of dogma and absolutism. And it’s because here’s an absolute truth for you, ironically: any design, when stretched to its logical absolute extreme becomes nonsense.
Example: if your code needs to do something, should you implement it yourself or take on a dependency? If your own implementation clocks in at 30-1000 lines of code, maybe it’s better to implement yourself (and take on the maintenance burden of that NIH code) than to pull in a dependency (and take on the maintenance burden of an additional dependency).
Testing the Quality of Your Designs
Once you start viewing design in terms of trade-offs instead of absolutes, you can develop tests for the suitability of a given design.
Could this be done with less moving parts?
The golden rule here is to only break up systems into individual components when they need to ship, scale, or deploy independently.
Is this operable in a production environment?
Software systems that are hard to observe, deploy, manage, and automate slow the safety and pace of change.
Is it easy to change?
Most software libraries live only 5-9 years. The context inevitably changes and it will be replaced wholesale. Don’t waste the time designing for hypotheticals of the distant future, design for it to be adaptable to slight changes in the current context.
Putting the Design Back into Software
Reading recommendations
- “Your Code as a Crime Scene”, Adam Turnhill
- “A Philosophy of Software Design”, John Oosterhout
- “Code that Fits in Your Head: Heuristics for Software Engineering”, Mark Seemann