Skip to content

Latest commit

 

History

History
257 lines (206 loc) · 6.23 KB

inversion-of-control.asciidoc

File metadata and controls

257 lines (206 loc) · 6.23 KB

Inversion of Control

Say you have some sort of data source from which you receive data. There are multiple kinds of data, each of which might need to be dealt with differently. A few examples:

  • A GUI which produces events such as key presses and mouse movements.

  • A webserver which receives GET, POST, PUT, …​ requests.

  • A file system contains files and directories.

  • A MIDI file containing different kinds of events: note on, note off, …​

Take for example a GUI: at the core of a GUI system (this is true for both Windows and Linux --- MacOS probably too, I never bothered to check) resides a message queue in which the OS dumps notifications regarding user input and OS events. In Windows terms, whenever the user presses down a key, a WM_KEYDOWN is added to the queue. Other examples are

  • WM_MOUSEMOVE: the user has moved the mouse.

  • WM_SIZE: the user has resized the window.

  • WM_CLOSE: the user has closed the window.

  • And hundreds of others.

Each event is accompanied by extra data. For example, WM_KEYDOWN carries information with it about which key has been pressed.

Dealing with these messages is done using a long if-then-else structure:

while ( program_is_running )
{
    message = get_next_message();

    switch ( message.type )
    {
        case WM_KEYDOWN:
            char key = message.key;
            // process key
            break;

        case WM_MOUSEMOVE:
            int x = message.x;
            int y = message.y;
            // process mouse move
            break;

        case WM_SIZE:
            int new_width = message.width;
            int new_height = message.height;
            // process resize
            break;

        case WM_CLOSE:
            // process close
            break;

        // process other events
    }
}

For the sake of clarity, instead of having a GUI with hundreds of different messages, we receive input taking the form of a char that can be either A or B.

Our data processing code becomes:

while ( more_input_available() )
{
    char c = get_input();

    switch ( c )
    {
        case 'A':
            // Deal with A

        case 'B':
            // Deal with B
    }
}

Let’s consider some algorithms. Say we want to count the number of `A`s and `B`s:

unsigned a = 0, b = 0;

while ( more_input_available() )
{
    char c = get_input();

    switch ( c )
    {
        case 'A':
            a++;
            break;

        case 'B':
            b++;
            break;
    }
}

The code below swaps `A`s and `B`s around:

while ( more_input_available() )
{
    char c = get_input();

    switch ( c )
    {
        case 'A':
            output('B');
            break;

        case 'B':
            output('A');
            break;
    }
}

This algorithm checks if the input contains alternating `A`s and `B`s:

bool correct = true;
char last = 'B';

while ( more_input_available() )
{
    char c = get_input();

    switch ( c )
    {
        case 'A':
            if ( last != 'B' ) correct = false;
            last = 'A';
            break;

        case 'B':
            if ( last != 'A' ) correct = false;
            last = 'B';
            break;
    }
}

As you can see, the examples above all share the same structure. In this kind of situation, it is best to factor out the common code. It might seem a bit overkill for this case since the common code is not particularly complex. However, it would certainly be warranted if the input were more complex. For example, `A`s and `B`s could be accompanied by extra data which would need to be parsed. This code would be repeated for all three algorithms above.

So, let’s factor out the common code. Our approach is as follows:

class Receiver
{
public:
    virtual void process_a() = 0;
    virtual void process_b() = 0;
};

void process_input(Receiver& receiver)
{
    while ( more_input_available() )
    {
        char c = get_input();

        switch ( c )
        {
            case 'A':
                receiver.process_a();
                break;

            case 'B':
                receiver.process_b();
                break;
        }
    }
}

As you can see, we have declared an "interface" Receiver with two methods: process_a and process_b. process_input accepts a Receiver object. Whenever it encounters an A in the input, it calls process_a, and likewise for B.

The three algorithms above can then be rewritten as follows:

class Counter : public Receiver
{
    int a, b;

public:
    Counter() : a(a), b(b) { }

    void process_a() override { ++a; }
    void process_b() override { ++b; }
};

class Swapper : public Receiver
{
public:
    void process_a() override { std::cout << "B"; }
    void process_b() override { std::cout << "A"; }
};

class AlternateCheck : public Receiver
{
    bool result;
    char last;

public:
    Counter() : result(true), last('B') { }

    void process_a() override
    {
        if ( last != 'B' ) result = false;
        last = 'A';
    }

    void process_b() override
    {
        if ( last != 'A' ) result = false;
        last = 'B';
    }
};

This approach should not be unfamiliar to you. For example, GUI libraries use the same technique: you can specify on_click, on_key_down, …​ methods, which will be called internally by the library whenever the corresponding event occurs.

This technique, known as inversion of control, has multiple advantages:

  • It reduces code duplication.

  • The Receiver subclasses contain only code related to processing the different cases, which improves readability.

  • Receiver objects can be kept small and be combined so as to achieve more complex functionality.

  • Whenever new types of events are introduced, an extra method can be added to Receiver. The compiler will then tell you where all subclasses of Receiver need to be updated. This kind of compiler assistance would be lacking otherwise.