Abstractions are Important Part 2 – Good Abstractions
Last time, I talked about the importance of abstractions in relation to a piece of service code that I had seen. This time, I’d like to expand on that concept a bit. I showed some examples of good versus bad abstractions and talked about why they were good or bad, and this time, I’d like to explore the idea of defining in general good versus bad abstractions.
What are Abstractions, Anyway?
If we’re going to talk about abstractions in general rather than simply by example, it probably makes sense to define the term a bit more formally. Wikipedia has a fairly good definition for it:
In computer science, abstraction is the process by which data and programs are defined with a representation similar in form to its meaning (semantics), while hiding away the implementation details.
In other words, an abstraction is a way to say to your clients, “give me the gist of what you want to do and let me worry about the details of how.” If you’re a client of the abstraction, you trust the provider to handle the details correctly.
An Example Abstraction
Abstraction, as a concept, is not limited to the problem domain of programming. Let’s consider an abstraction that has nothing to do with programming, on its face: the pizza shop. The pizza shop abstracts the process of making a pizza from its customers, allowing customers to specify basic properties of their desired pizza while the shop handles more granular ones.
If you call a pizza shop, you tell the shop that you want a large pizza with pepperoni on it. What you typically don’t do is specify how much pepperoni (aside from perhaps in vague terms like “extra” or “light”) nor do you specify the exact dimensions of “large”. These are some of the details that are simplified for you. Others are hidden entirely such as how much basil goes in the marinara sauce or the temperature at which the ovens are set for pizza cooking.
The procedure in general is a simple one. You specify a few rudimentary details about the pizza and whether you want delivery or not, and the shop responds with a time estimate and then, later, a pizza. However, this is an excellent abstraction (as one might surmise by the popularity and ubiquity of its implementation).
So, what makes an abstraction ‘good’? How do we make sure that the ones we’re creating are good?
Exposes Details That Make It Useful
Any abstraction has to expose some level of detail to clients or it would be useless. When calling the pizza place, you are aware, obviously, that you want a pizza. This is unavoidable. On top of that, the pizza place also allows you to specify size and a lot of the ingredients of the pizza. This ensures that you will get as much or as little food as you want and that dietary restrictions and considerations are met. In addition, the pizza place (usually) allows you to specify whether you want to eat there, carry the pizza home or have it delivered. This is another angle for the abstraction as it allows the pizza shop to accommodate your location preference for where you want to eat.
Making the abstraction useful is vital or else nobody would actually use it. In the world of pizza parlors, if one opened up that served only small, mushroom pizzas for carry out, it probably wouldn’t last very long. Even assuming there were no such thing as competition, people would probably opt to make their own pizzas most of the time rather than agree to such specific restrictions. No one would make much use of this abstraction.
Hides details that it needs to control
The flip side of exposing enough detail to make it useful is hiding details that need to be controlled. Imagine the opposite of our “you only get small, mushroom pizzas for carry out” situation, where a pizza parlor allowed specification of any detail, however minute. Customers could call and say that they wanted a pizza cooked in natural cave at 193 degrees Celcius, infused with rare spices from a remote island, and delivered at 4 AM.
The impact on a shop of catering (or attempting to cater) to this level of detail would be disastrous. Imagine the logistics of having to dispatch employees to whatever location customers demanded. Or, imagine the expense incurred in obtaining certain ingredients. And, imagine the absurdity of a pizza place staying open 24/7/365. These things would be the result of too much permissiveness with the abstraction. This abstraction hides no details from its users and, by relinquishing all control over operational details, it allows its users to put it into unprofitable, preposterous modes of operation.
Is Understandable and Intuitive to Clients
If usefulness and guarding against damaging levels of control by clients are table stakes for having an abstraction that can hope to survive, understand-ability and intuitiveness are necessary to thrive. One of the reasons the pizza place is so successful is that it’s a relatively universal, common-sense and simple abstraction. Whatever slight variations may exist, you will generally have no issues ordering pizza from a place even if you’ve never ordered from there before.
“Ask for food”, “add a few ingredients”, “specify where you want the food”, and “pay for food” are all very simple concepts. This simplicity is a big part of the reason that when you’ve checked into a hotel and are tired, you fall back to ordering a pizza instead of ordering, say, tapas or hibachi or going out and buying a bunch of groceries. “How do I get there?”, “Where can I cook this?”, “Do you have a way for me to take this home?”, etc are questions you don’t need to ask. This simplicity and universality makes the abstraction a wildly successful one.
Prevents (or limits) client mistakes
It’s crucial to limit mistakes that clients can force you to make, and it’s almost as crucial to prevent clients from making their own mistakes. The former might blow up your abstraction before it gets going, where the latter, like intuitiveness, is important for gaining adoption. One of the attractions of ordering a pizza is that it’s unlikely to end in disaster. Oh, it might not be cooked to perfection or it might generally be mediocre, but it won’t set your oven on fire or come bubble over into a gigantic mess during cooking.
The reason for this is that the pizza restaurant abstraction removes a lot of the potential problems of cooking a pizza by hiding the process from the customer and leaving it safely in the hands of a specialist. Nothing about specifying the size or the toppings of the pizza gives me the ability to make a decision that somehow causes the pizza to be overcooked or the meat toppings to be dangerously undercooked.
Back to the Code (and are you even making abstractions)?
So, what does all of this mean for coding? I would argue that since a pizza place is really just a process abstraction, we can translate these lessons directly to code. Exposing things to make the abstraction useful while hiding things that would cripple it is fairly straightforward to do, provided that you think in abstractions. I might have a database access abstraction that allows users to specify connection credentials but internally prevents things like multiple connections, dropped connections, etc. In this fashion, I can allow users to connect with different levels of privilege, but I can prevent them from inadvertently getting my class into some invalid state.
Likewise, I can create intuitive operations such as “create new record” or “delete record” that hide ugly details like SQL statements and transactions. This presents an intuitive and inviting interface that’s pleasant to use. And, in addition to providing a minimum guarantee of my abstraction’s own functionality, I can at least assist in saving them from their own lack of familiarity with the abstraction. Fail early goes a long way toward this — I can throw descriptive exceptions if they try to delete nonexistent records, rather than leaving them to decipher what SQL Error 9024B means. This is the equivalent of the pizza place operator saying, “I’m sorry sir, but ordering a negative six inch pizza makes no sense — we don’t offer that.” In real life, this “fail early” approach is much better than a delivery guy showing up empty handed and leaving it to you to figure out why no pizza arrived.
To pull back a bit, I think it’s important to consider the pizza shop or a similar metaphor when writing methods/classes/modules. Don’t simply write code that is technically functional, strewing it willy-nilly about various classes and locations. Don’t write code by coincidence while the debugger is running, setting flags and whatnot to get things working for your exact scenario. Don’t go with the philosophy “ship it if it works.”
Instead, when writing code, imagine that you’re creating a metaphorical pizza place. Who are your ‘customers’? Answer that question, and it becomes easy to answer “what do they want from me?” Answer this question well before “how do I get them what they want?” The “what” is your public API — the useful thing you’re going to provide. The “how” is the detail(s) that you hide from them, for their own good and the operational good of your code. The intuitiveness of your public API is going to be determined by answering questions like “am I logically book-ending operations — if I allow them to open something, do I allow them to close it?” or “if I read this method name and its parameters aloud, is it clear what this does” or “do all parts of my public API do what they say they’re going to do?” If you’re answering yes to these questions, your pizza shop is looking good. If not — if you’re sending sandwiches or sushi when the customers order a medium pepperoni pizza — your abstraction (code) is probably doomed.
And, I hate to bring unit testing into everything, but there’s really no avoiding this — if you write unit tests and especially if you practice TDD, you’re a lot more likely to have better abstractions. Why? Well, you’re your own first customer. This is like going “undercover boss” and ordering pizza from your own shop to see how the experience goes. When you write tests, you’re using your public API, and if you’re muttering things like “what are all these parameters” or “why do I have to pass that in?!?” you’re getting early feedback on your own abstraction. I’ve rarely seen an abstraction that inspired me to react with a “wat?!?” and gone on to find a nice set of unit tests covering it. Tests seem to function as insurance against boneheaded abstractions.
A Checklist For Abstractions
I’ll close out with a set of suggestions — a checklist for evaluating whether you’re creating good, usable abstractions or not.
- Does your API operate at a consistent level of abstraction — do you avoid having some methods that require users to pass you SQL statements and others that encapsulate this detail, for example?
- Do your methods generally have two or fewer parameters (more parameters making it increasingly hard on users to intuitively understand the method)?
- Do your methods have succinct but communicative names like “CreateEntry(Entry entryToCreate)” as opposed to needlessly verbose (“CreateRecordThatIsGoingToGoInTheDatabase(Entry entry)”) names that are hard to type and remember or weirdly succinct names (“CE(Entry e)”)?
- Do your methods lie? Does “CreateEntry(Entry entryToCreate)” actually delete an entry, or perhaps less egregiously, create an entry sometimes, unless the entry has a certain flag set true, in which case it quietly fails?
- Do you avoid forcing weird details on your clients, such as asking them to store boolean flags?
- Do you avoid multiple return values (i.e. out/ref parameters?)
- Do you somehow communicate what exceptions your methods might throw?
- Do you limit the number of methods per class so that reading through the documentation or IDE assistance is not painful?
- Do you avoid forcing your clients to violate the Law of Demeter? That is, in order to get a “D”, do you force your clients to call getA().getB().getC().getD(); ?
- Do you practice command query separation in your public API?
- Do you limit or eliminate exposing public state, and especially flags?
- Do you limit temporal couplings that force your clients to call your methods in a specific order?
- Do you avoid deep inheritance hierarchies that make it unclear where the members of the public API actually come from?
- Do your publicly exposed classes have a single, obvious responsibility — do you avoid exposing swiss-army-knife classes with a mish-mash of different functionalities?