In the previous post, we talked about the importance of functions doing one thing. We briefly touched on something called abstraction levels. One of the simplest ways to be sure that your functions only do one thing, is to make sure that they stay on one abstraction level.
Let’s try and explain this using a very simple example.
How to wash your hair
Every single day, millions of people around the world wash their hair in the shower. If you asked them the steps for washing their hair, they would likely give answers like the following:
“I just wash it”
“I first wash with shampoo and then rinse”
“When you’re in the shower and you want to wash your hair, first you have to make sure to wet your hair. Then you take the shampoo bottle and pour some shampoo onto your hands. Using your fingers make sure to lather the shampoo and distribute evenly across your hair. Wait a short time for the product to work, and then rinse your hair. Afterwards, take some conditioner and apply it to your hair. Finally, rinse for a second time and you have finished washing your hair.”
Which description is the correct one? They all are, the difference between the answers is the level of detail they go into. This is what we mean by abstraction levels, the higher level an abstraction is, the less details we show. Then, the more levels we go down, the more details we get.
If we tried to write code to represent this it would look like this:
public void WashHair();
This would be the highest abstraction level for the function that washes your hair. At this point we don’t know how hair actually gets washed, and maybe we don’t want to know. Maybe we’re responsible for writing a function for taking a shower. In that case, this is as far as we want to go. We don’t need to know how hair is washed, we only care that it happens.
Going deeper
Suppose we want to go down one level of abstraction and see how that happens:
public void WashHair(){
WetHair();
ApplyShampoo();
Wait();
Rinse();
ApplyConditioner();
Wait();
Rinse();
}
Now, we have more information about how the function works. We can understand the steps taken, even though there are no comments and we’re not looking at the code that actually does the work.
Go only as deep as you have to
Finally, let’s go down one more level of abstraction and see how shampoo is applied:
private void ApplyShampoo(){
Shampoo shampooDose = ShampooBottle.GetShampoo();
Hair.ApplyShampoo(shampooDose);
Hair.LatherShampoo();
}
Now, we finally get to see variables, classes and “lower level” code. We could go even deeper, but this level of detail is enough for us. We don’t need to know how the Shampoo class works (or maybe we can’t see the source code), we just trust that it does.
Dealing with changes
If at some point, you had to change the way that Hair is washed, then you would only have to go the abstraction level where that task is performed and modify a very small amount of code. For example, if someone wanted to wash their hair without conditioner you only need to write the following function:
public void WashHairWithoutConditioner(){
WetHair();
ApplyShampoo();
Wait();
Rinse();
}
There was no need to look at hundreds of lines of code. Since each function does only one thing, and the names are very clear about what each function does, we only have to call the functions that execute the steps we need. Any other programmer that later looks at the new code will understand what the new function does. There will be no need for commenting the new function or going into a deeper abstraction level since the name is very clear, and the functions called are already known.
Writing small functions, that deal with a single level of abstraction will help you write code that is cleaner, easier to understand and easier to modify. This can be a difficult change to make since most of use are used to writing longer (often much longer) functions. However, it will be worth it.