Great Design
Software has plenty of examples of bad design, but the well designed stuff becomes almost invisible and isn’t talked about very much. Today I chose a few of my favorite ideas and implementations that deserve the mantle of truly “Great” design.
The Unix File API
If you write a file in Unix, you just need a few system calls.
open, read, write,
seek, and close. That’s all you need. The
caller doesn’t need to know about the perils of writing a file, or where
the bytes go, they create a file, read and write data from it, and
that’s all they need to do.
None of the implementation details bleed out to the caller: you don’t have to worry about what kind of device you’re writing to, whether it be an SSD, HDD, tape, or even a piece of paper. The operating system figures that out for you. If you read or write, the operating system moves the needle on tape, the pen on paper, or talks to the SSD’s block controller.
The interface itself doesn’t care about its backing store, since it assumes the caller doesn’t. If the caller notices that operations are slow, they might start looking at the storage media, but not before admiring how nice the interface is.
Writing to persistent storage is a gargantuan task: Linux file system drivers are millions of lines of dense code that works behind the scenes, and the user doesn’t have to know a thing about it.
Other operating systems decided to create system calls that were tied to the physical media. That meant that programmers would have to change their code if their storage media changed, which meant the code was brittle and hard to port. The Unix file API sidesteps all that. The user doesn’t have to change a thing no matter the backing media.
The API is also easy to optimize – if your storage media supports
concurrent reading and writing, read and write
can fire off as many workers as needed to make your request as fast as
possible, or read out of order on disk as long as the
caller has their requirements met, which is the bytes arrive in the
order they are on disk.
A true paragon of good design.
UTF-8
In the 1990s, many people speaking many different languages started to use the internet. Many languages have more than 256 symbols, so they don’t fit in a byte, and many languages had created their own custom encodings that made browsing the web a struggle – you’d need to download dozens of encodings for all these different languages to just browse the internet.
That was until UTF-8 became dominant. Now, UTF-8 is the encoding of the web, but it wasn’t always that way – even 15 years ago the Japanese web had many pages using Shift-JIS.
The ingenuity of UTF-8 lies in its interoperability with ASCII with
no overhead, and the ability to encode either 1, 2, 3, or 4-byte
characters by only using 1, 5, 8, or 11 bits to signal the remaining
length of each character. With 21 bits still remaining for a 4-byte
character, UTF-8 can support 2^21 bytes, or a little over 2
million characters.
That should be enough for a while.
TCP
TCP was introduced 50 years ago, and it still holds up as good design: it provides one guarantee, like re-requesting lost packets and providing in-order transmission of packets between two programs. TCP also notifies the client of an error if packets couldn’t be received from the sender. These strengths, in exchange for less throughput and higher latency make it easy for people to implement higher level protocols on top of TCP, like HTTP. As well, its transmission control algorithms were invaluable in helping the internet scale to its current usage.
TCP shows the importance of abstraction in design – IP, which TCP is built on top of, may lose packets in transmission due to network congestion, load balancing, or other hardware problems in the physical or link layers. Packets could also be corrupted, or come in any order. TCP handles error handling and, with little overhead, turns those unwiedly packets into a set of ordered packets.
If you’re skeptical of the guarantees TCP provides, try writing an application without it, using just UDP – it’s hard. You have to build a robust in-order network stack at the system level so the application layer can consume bytes in-order, or write your application to tolerate out of order transmissions, dropped packets, and unbounded timeouts.
TCP takes care of that.
Message Passing
If Smalltalk won, this section would be called “Object-Oriented Languages”. Alas, Smalltalk did not win, and everyone thinks OOP means Java style interfaces and inheritance. Sigh. Erlang might be the only true Object Oriented language still around.
Message Passing is what it sounds like. You pass a message to an actor, who receives the message and acts accordingly based on the message. In Erlang, objects are processes that can only receive and send messages and cannot share any state except by message passing. This metaphor is very powerful up and down the stack.
A computer nowadays has many cores (my computer has 16), and they all have their own memory and have to read and write to both core-local and shared caches in order to receive and broadcast their state (the results of their computations). Rather than have each core have access to each other core (which would get messy quickly), cores can pass messages to each other, allowing them to coordinate (e.g. invalidate a value because it has become stale, or publish a new value since it’s been updated).
If I choose to read a webpage on my browser, I receive some HTML, CSS, and JS over the wire and my browser renders it. If I pass some extra parameters along with my request, like query parameters or a body, I can customize the response I get. I can also send some data to the computer, which it can respond to in kind by sending a POST request. I have no access to the internals of the server, and the server has no access to my computer’s internals. Message passing becomes the boundary. As long as both sides make backwards compatible changes (like still responding to the same messages the same way), either side can make any arbitrary changes.
When we converse with other people, we pass messages through spoken word to each other to understand each other’s internal state (emotions).We also use the written word to do the same thing. Without message passing, we would have to do brain surgery to understand each other’s thoughts, and even then, since we’re not translate physical signals from the brain into coherent thoughts, this wouldn’t work either.
When I try to design something, I always prefer stateless message passing. If two actors can get away with message passing, even if it makes things a little harder in the short-term, it’s almost always worth it in the long-term. The less I need to know, the better.