The Single and the RIGHT Level of Abstraction

The straightforward interpretation of the Single Level of Abstraction principle is to avoid mixing high and low level details in the same code.

Even at such a basic level it gives a lot of benefits. It helps keep the code focused, readable and easy to understand.

However, we can take this idea a step further.

First of all, we should keep our code not only at a single level of abstraction, but also at a right level of abstraction.

Second, this applies not only to the code, but also to the specs.

But let’s start with a straightforward example:

A Single Level of Abstraction

Consider the following, simple spec:

describe("battle mode switch", function() {
    it("activates shields");
    it("activates targeting system");
    it("activates plasma cannons");
});

It is very consistent and readable, to the point that it needs no explanation. All steps of the spec stay at exactly the same level of abstraction: the activation of various ship’s subsystems needed during a space battle. They even all start with the same word.

The same is mirrored in the code:

function battleModeSwitch() {
    activateShields();
    activateTargetingSystem();
    activatePlasmaCannons();
};

The above function is declarative and very easy to grasp, with the immediately obvious responsibility.

Now consider what happens when we mix two different levels of abstraction:

describe("battle mode switch", function() {
    it("activates shields");
    it("activates targeting system"); 
    it("opens hull's plasma cannon covers");
    it("makes generator set max power to plasma cannons");
    it("activates plasma cannons' cooling system");
});

The specification becomes a mess.

The neat list of which subsystems to activate gets muddled by the details of what is required to activate plasma cannons. The function becomes unreadable and its purpose unclear.

Even the specs’ names become over-complicated. Instead of short and simple action sentences like battle mode switch activates shields we get complex ones with two subjects: battle mode switch makes generator set max power to plasma cannons – are we specifying behaviour of the battle mode switch or the generator?

The same happens if we break the Single Level of Abstraction principle in the code (although in the specs it is more visible):

function battleModeSwitch() {
    activateShields();
    activateTargetingSystem();
    hull.openPlasmaCannonCovers();
    generator.setMaxPower(plasmaCannons);
    plasmaCannons.activateCoolingSystem();
};

Again, we have lost the readability and muddled the purpose of the function.

But what about the situation when all steps both in the code and specs ARE at a single level of abstraction? Do they automatically become clean? Is it that simple?

Unfortunately, not always. They can still be at a wrong level of abstraction. Let’s take a look at another, more subtle example:

The Right Level of Abstraction

When the monitoring system of our ship detects hull damage, it should immediately alert the repair crew and the captain by wailing the siren and flashing the lights in the mechanics room (at least one mechanic is always on duty in the room, so it is sufficient) and by calling the captain’s intercom and vibrating his watch (captain seldom stays in his room, so we need to catch him on the move).

We’re aware of the Single Level of Abstraction principle so we start with a specification like this:

describe('Hull Monitoring System', function() {
    describe('damage alert', function() {
        it('wails mechanics room siren');
        it('flashes mechanics room lights');
        it('vibrates captain watch');
        it('buzzes captain intercom');
    }); 
});

All four steps seem consistent and are at the identical level of abstraction. Is such a spec optimal, then?

As you can guess, it isn’t. Maybe you have already noticed a problem. If you haven’t, let’s take a look at the matching implementation, where the problem is more visible:

function HullMonitoringSystem() {       
    this.damageAlert = function() {
        mechnicsRoom.wailSiren();
        mechnicsRoom.flashLights();

        captain.vibrateWatch();
        captain.buzzIntercom();
    };
};

You can see that although all the steps are at the same level of abstraction, they tend to group around two different subjects: the mechanics room and the captain.

Such a grouping seems very natural and reasonable, so why not make it explicit?

function HullMonitoringSystem() {       
    this.damageAlert = function() {
        notifyMechanics();
        notifyCaptain();
    };

    function notifyMechanics() {
        mechnicsRoom.wailSiren();
        mechnicsRoom.flashLights();
    };

    function notifyCaptain() {
        captain.vibrateWatch();
        captain.buzzIntercom();
    };
};

Although the original implementation wasn’t particularly bad, the current one is much better. It makes the purpose of the damageAlert funcion clear.

Should we also modify the specs in the same way?

Before we answer this question, let’s stop for a moment to consider why the original function wasn’t good, despite obeying the Single Level of Abstraction principle?

The problem is that although all its steps were at the same level of abstraction, they were at the wrong level of abstraction!

Take a look at the requirements: monitoring system [...] should [...] alert repair crew and the captain by [...]. Monitoring system should alert the repair crew and the captain. This is the purpose of the system, not wailing sirens and buzzing intercoms. These are the implementation details. The by part of the requirements. These are the how not the what.

The problem with the original function was that it tried to skip one level of abstraction, going two levels down instead of one.

This, in turn, answers our question if we should also change the specs:

Definitely!

The goal of a specification is to describe what the system should do, not how. Therefore, by jumping over one level of abstraction, we’ve missed the primary purpose of the specs. Let’s fix it:

describe('Hull Monitoring System', function() {
    describe('damage alert', function() {
        it('notifies mechanics');
        it('notifies captain');
    }); 
});

Now it’s much better. The purpose of the alert is clear.

However, there is another problem lurking in the implementation details of the specs.

A Single Level of Abstraction at a class level

Up till now we have focused only at the specs’ descriptions. This was OK, as it allowed us to have a clearer view of their levels of abstraction. But we can’t avoid the implementation forever. Let’s take a look at it now:

describe('Hull Monitoring System', function() {
    describe('damage alert', function() {
        it('notifies mechanics', function() {
            expect(mechanicsRoom.siren).toWail();
            expect(mechanicsRoom.ligts).toFlash();
        });

        it('notifies captain', function() {
            expect(captain.watch).toVibrate();
            expect(captain.intercom).toBuzz();
        });
    }); 
});

There is a faint smell here: two expectations in each of the specs.

This isn’t wrong by default. Although we should strive to describe only a single example in each spec, that doesn’t necessarily map to exactly one expectation. A few related expectations working together to describe a single, coherent example are acceptable.

However, we need to consider if in our case both expectations really cooperate to specify a single concern? My answer is no. Wailing sirens and flashing lights are two separate, unrelated actions. We should separate them.

We may try to do it only at the description level:

describe('Hull Monitoring System', function() {
    describe('damage alert', function() {
        describe('it notifies mechanics', function() {
            it('wails mechanics room siren');
            it('flashes mechanics room lights');
        });

        describe('it notifies captain', function() {
            it('vibrates captain watch');
            it('buzzes captain intercom');
        });
    }); 
});

But it feels artificial and won’t scale well if complexity of the implementation increases – too many levels of nesting make specification unreadable.

Is there another way to decompose it?

Fortunately, there is. We just need to introduce a new class:

describe('Hull Monitoring System', function() {
    describe('damage alert', function() {
        it('notifies mechanics', function() {
            expect(notificationCenter).toNotifyMechanics();
        });

        it('notifies captain', function() {
            expect(notificationCenter).toNotifyCaptain();
        });
    }); 
});

describe('Notification Center', function() {
    describe('notify mechanics', function() {
        it('wails mechanics room siren');
        it('flashes mechanics room lights');
    });

    describe('notify captain', function() {
        it('vibrates captain watch');
        it('buzzes captain intercom');
    });
});

Such a spec forces us to also change the implementation:

function HullMonitoringSystem() {       
    this.damageAlert = function() {
        notificationCenter.notifyMechanics();
        notificationCenter.notifyCaptain();
    };
};

function NotificationCenter() {
    this.notifyMechanics = function() {
        mechnicsRoom.wailSiren();
        mechnicsRoom.flashLights();
    };

    this.notifyCaptain = function() {
        captain.vibrateWatch();
        captain.buzzIntercom();
    };
};

And it turns out that it was a good move. The code is now even clearer, with strictly separated responsibilities. Also, we have gained the ability to mock the Notification Center and thus postpone its implementation, what gives us greater flexibility.

But what exactly has happened?

The hull monitoring system spanned to many levels of abstraction. It contained both the steps describing the implementation of damageAlert and all the sub-steps describing those steps.

We’ve split these two levels of abstraction into two separate classes: hull monitoring system and notification center, each operating only at a single level of abstraction. This made both classes more focused, with a better separation of responsibilities.

Should we always adhere to the Single Level of Abstraction principle so strictly? At the function level, I’d say yes. At the class level, probably not. But we should at least take a look at each class and spec through this lens – often, it may lead us to a better design.

What are your experiences with the Single Level of Abstraction principle? When does it improve the design and when does it go too far? Share your opinion in the comments below!

Advertisements

What do you think?

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s