Jonathan Boccara's blog

It all comes down to respecting levels of abstraction

Published December 15, 2016 - 6 Comments

Daily C++

As software developers, we get to learn many good practices and strive to apply them in our code.

For instance we learn the importance of good naming of variables and functions, encapsulation, class cohesion, the usage of polymorphism, conciseness, readability, code clarity and expressiveness, and many others.

What if there was only one principle to know instead of plenty of best practices ?

I believe this principle exists: it consists of Respecting levels of abstraction.

This is the one principle to rule them all, because applying it automatically applies all the above best practices, and even more of them. When you follow it, your code writes itself out well naturally.

It’s based on simple notions, but it took me years of practice and study to formalize it. Anyway, enough talk, let’s dive right into it.

The What and the How

What are levels of abstraction in the first place ? This notion is easy to grasp when you look at a call stack. Let’s take the example of a software dealing with financial products, where the user has a portfolio of assets that he wants to evaluate:

This call stack can be read from the bottom up the following way:

  • To evaluate a portfolio, every asset has to be evaluated.
  • To evaluate a particular asset, say that some type of probability has to be computed.
  • To compute this probability there is a model that does mathematical operations like +, -, etc.
  • And these elementary mathematical operations are ultimately binary operations sent to the CPU’s arithmetic and logic unit.

It is quite natural to conceive that the code at the top of this stack is low-level code, and the code towards the bottom of the stack is rather high-level code. But level of what ? They are levels of abstraction.

Respecting levels of abstraction means that all the code in a given piece of code (a given function, an interface, an object, an implementation) must be at the same abstraction level. Said differently, at a given abstraction level there mustn’t be any code coming from another level of abstraction.

A given level of abstraction is characterized by what is done in it. For example at the bottom level of the stack, what is done is evaluating a portfolio. Then one level above in the stack, what is done is evaluating an asset. And so on.

And to go from a given level of abstraction to the next lower one, the less abstract one is how the more abstract one is implemented. In our example, how to evaluate an asset is by computing a probability. How to compute a probability is with elementary mathematical operations, and so on.

So the crucial question to constantly ask yourself when you design or write code is: “In terms of what am I coding here ?”, to determine which level of abstraction you are coding at, and to make sure you write all surrounding code with a consistent level of abstraction.

One principle to rule them all

I deem the Respect of levels of abstraction to be the most important principle in programming, because it automatically implies many other best practices. Let’s see how several well-known best practices are just various forms of respecting levels of abstractions.

Polymorphism

Maybe the first thing you thought of when reading about abstraction is polymorphism.

Polymorphism consists of segregating levels of abstraction.

Indeed, for a given interface (or abstract class) and a concrete implementation, the base class is abstract, while the derived implementation is less abstract.

Note that the derived class is still somewhat abstract though, since it is not expressed in terms of 0s and 1s, but it is at an inferior level of abstraction than the base class. The base class represents what the interface offers, and the derived class represents how it is implemented:

Good naming

Let’s take the example of a class in charge of maintaining a caching of values. This class lets its clients add or retrieve values of type V, with keys of type K.

It can be implemented with a map<K,V>:

Imagine now that we want the interface to be able to provide the whole set of results for all stored keys at once. Then we add a method to the interface. How should we name this method ? A first try may be “getMap”.

....
const std::map<K,V>& getMap() const { return data_; }
....

But as you might feel, “getMap” is not a good name. And the reason why it isn’t is because at the abstraction level of the caching interface, “Map” is a term of how (observe that it appears in the bottom part of the diagram), and not of what, so not at the same abstraction level. Calling it “getMap” would mix several abstraction levels together.

A simple fix would be to call it “getAllValues” for instance. “Values” is a consistent term with the level of abstraction of the caching interface, and is therefore a name that is more adapted than “Map”.

Good naming is in fact giving names that are consistent with the abtraction level they are used in. This works for variable names too. And because naming defines levels of abstraction and is therefore such an important topic, we will have a dedicated post about it. You can follow me on Twitter (or subscribe to the Rss feed) at the bottom of this post if you want to be notified when this comes out.

Encapsulation

But isn’t it a violation of encapsulation to provide the map of results to the outside of the class in the first place? Actually the answer depends on whether the concept of a results container is logically part of the abstraction of the class interface.

So breaking encapsulation is providing information that go beyond the abstraction level of the interface.

Cohesion

Now imagine that we added a new method in the caching class to do some formatting on values:

....
static void formatValue(V&);
....

This is obviously a bad idea because this class is about caching values, not about formatting them. Doing this would break the cohesion of the class. In terms of abstraction, even though caching and formatting don’t have a what-how relationship, they are two different abstractions because they are in terms of different things.

So cohesion consists of having only one abstraction at a given place.

Conciseness, readability

Let’s go down to the function (or method) level.

To continue on the financial example, let’s consider financial indices such as the Dow Jones or the S&P, that contain a collection of equities like Apple, Boeing or Caterpillar.

Say that we want to write a function that triggers the save of an index in database after having done some checks on it. Specifically, we want to save an index only if it is valid, which means say, having an ID, being quoted on a market and being liquid.

A first try for the function implementation could be the following:

void saveIndex(Index const& index)
{
    if (index.hasID() && index.isQuoted() && index.isLiquid())
    {
        ...

We could object to this implementation that it has a relatively complex boolean condition. A natural fix for this would be to group it and take it out of the function, for code conciseness and readability:

void saveIndex(const Index& index)
{
    if (isValid(index))
    {
        ...

When we think about this fix, it consists in fact pushing out the implementation of how an index is considered valid (having an ID, quoted, liquid) and replacing it with what the save depends on (being valid), which is more consistent with the level of abstraction of the save function.

An interesting thing to note at this point is that respecting levels of abstraction goes beyond the simple conciseness of code. Indeed, we would still have done this fix even if being valid only meant having an ID. This wouldn’t have reduced the number of characters typed in the code (it would even have slighlty increased it), but this would have improved code clarity by respecting levels of abstraction.

Expressiveness

Last but not least, expressiveness, which is the focus of Fluent C++.

Say that we want to remove some components from the index if they are not themselves valid.

The best solution here is to use the remove_if algorithm of the STL. STL algorithms say what they do, as opposed to hand-made for loops that just show how they are implemented. By doing this, STL algorithms are a way to rise the level of abstraction of the code, to match the one of your calling site.

We’ll explore the STL in depth in future posts (again – follow me to stay updated) because they are such a great tool to improve code expressiveness.

Conclusion

Following the principle of Respecting levels of abstraction helps make choices when designing code, on many aspects. If you think about this principle when designing your code, if you constantly ask yourself the question “In terms of what am I coding here ?”, your code will write itself well, naturally.

Many guidelines can be derived from this principle. I intend to write several posts exploiting it to improve code in various ways. If you want to be notified so you don’t miss out on this, you can just follow with one of the buttons below :).

 

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin

Comments are closed