What is an implementation detail anyway?

I think knowing what implementation details are and how to properly isolate them is one of the important components of writing maintainable code. However, if you google for a definition of what an implementation detail actually is, it seems to be mostly subjective. Although, most people seem to agree that it has to do with knowledge of how something is done. You may have heard that you shouldn’t depend on implementation details in client code; you should follow the idea of encapsulating those things. I thought I’d share my thoughts on why this is such an important idea.

It’s all about how we organize our knowledge about the problem

When we start off trying to solve some particular problem, we usually have an idea of what our goal is and what steps are required in order to reach it. The knowledge of how the problem is solved is directly reflected in our code. Our code represents knowledge of a particular concept in the problem space and how it is implemented. This piece of information should be isolated from everything else that isn’t related to that particular part of the problem that is solved. Since this is getting quite abstract, so let me follow up with an example.

How the concept of a car may appear in some problem space

The picture above illustrates a car in some arbitrary problem space. It contains the concept of a seat, a steering wheel, a gas pedal and an engine. Knowledge of how each of those concepts are represented should be isolated from each other. Code representing the gas pedal should not know the inner workings of the engine, for example. The only thing it should care about is what is done, not how it’s done. Why is that?

If the interface through which the gas pedal communicates with the engine leaks knowledge of the engine’s inner workings, the gas pedal may become subject to change whenever the representation of the engine changes. Think about what this would mean if we added several more components which communicate with the engine through the same interface, all those components would have to change as well whenever we decide to change the engine in order to fulfill new requirements. Will you know where and what needs to be changed? Who keeps track of those places? Will the compiler tell you? When you change those components in this case, you add an additional risk of introducing bugs; the code becomes more fragile and rigid.

Examples of leaky abstractions

Let’s look at some possible leaky abstractions in some software that is used for, say, cooking food, since I am more familiar with this domain than cars (relatively speaking; you’re better off not asking me to cook for you).

Let’s assume we have a chef with the following interface (pseudoish C++ code).

class Chef {
public:
    Spaghetti MakeSpaghetti();
    Knife AskForCutter();
private:
    /* possible methods for making spaghetti goes here */
    Knife cutter_;
    /* some other tools needed for making spaghetti */
};

Let’s assume the chef uses the knife for cutting onions. With other words, he uses it when making spaghetti. His responsibility is to cook food. Then, we might have some client code like the following:

void Customer::EatCarrotSoup() {
   Chef chef;
   Carrot carrot;
   Knife knife = chef.AskForCutter();
   std::vector<CarrotSlice> slices = knife.cut(carrot);
   Soup carrot_soup = Soup(slices);
   Eat(carrot_soup);
}

We want to eat carrot soup, and we’re interested in the chef’s way of cutting things, so we ask for the chef’s knife in order to do this.

Is this good? It might be, it might not be. What happens if the chef decides a knife is not enough, and gets a battle axe instead? What happens if he decides that he doesn’t need a knife at all, and chooses to smash the onions instead? If you’re cutting carrots in the kitchen and you’ve been asking for his knife for years, what will you do when he suddenly hands you a battle axe? Or a blender? You’ll have to change the way you go about cutting carrots. Maybe the battle axe doesn’t support cutting carrots (for some weird reason). Imagine if we had 20 different people in the kitchen wanting his service for cutting different kinds of vegetables for cooking food. Lots of new learning to do.

What would be better, though, is if the client code could do this instead:

void Customer::EatCarrotSoup() {
   Chef chef;
   Soup carrot_soup = chef.CookCarrotSoup();
   Eat(carrot_soup);
}

This also separates the activities of cooking the soup and eating it, increasing the reusability of our code.

So with this final piece of code, the client no longer needs to know about how the chef cuts vegetables for his meals. We only care about what happens. This time you’d ask the chef to cook your carrot soup, and he’ll use his knife or his battle axe in order to get the job done. We don’t care what tool he uses, we just want him to make us some real hip carrot soup.

You could also argue that we could just instantiate a knife and not ask for the chef’s, and that may be true in some context, but in this context we’re interested only in the services of the chef. We want to use his way of making soup. Also, it doesn’t make sense for the responsibility of cooking to be on the client (customer), since this is the responsibility of the chef.

Summary

Interfaces should not leak implementation details. This causes client code to change if the implementation details changes. This may lead to an increased number of bugs and more time spent on making changes to the code, since you have to change every place where code is relying on those implementation details. With other words, the code becomes more rigid.

Interfaces should instead fulfill an immediate goal of the client (Vladimir Khorikov discussed this in a blog post as well, you should consider reading it). When designing an interface, you need to understand what service you’re going to provide for your client code and how the client code will make use of your interface. Test-driven development is a software development process that supports this idea very well, for example.