Command

Problem

There is a multitude of possible operations on an object, and a possible order in which one might want to invoke a sequence of such operations. The executing entity is different from the originator of the operations, and thus does not want to know about the particular nature of those - it just wants to execute() them.

Commands are often composed - one command actually holds/executes an entire set of lower level commands.

Solution

../../../../../_images/command.png

Exercise

Motivation

There is a database implementation, SocialDB, with the operations

  • insert()

  • find()

  • drop()

(See here for the definition of the SocialDB class.)

Implement a SocialDBCommand hierarchy that provides a type for each of the database operations.

Step 1: Basic insert()

Start with the insert command - it is the simplest because is has no return value, and naively we do not currently expect errors.

#include <gtest/gtest.h>

#include <socialdb.h>
#include <social-db-commands.h>


TEST(command_suite, insert)
{
    SocialDB db{{"1037190666", {"Joerg", "Faschingbauer"}},
                {"1234250497", {"Caro", "Faschingbauer"}},
                {"2345110695", {"Johanna", "Faschingbauer"}},
    };

    SocialDBInsertCommand ic("3456060486", "Philipp", "Lichtenberger");
    ic.execute(db);

    auto [firstname, lastname] = db.find("3456060486");
    ASSERT_EQ(firstname, "Philipp");
    ASSERT_EQ(lastname, "Lichtenberger");
}

Ensure that the command implements the SocialDBCommand interface. (That is the entire point behind Command.)

#include <gtest/gtest.h>

#include <socialdb.h>
#include <social-db-commands.h>


TEST(command_suite, insert_by_base)
{
    SocialDB db{{"1037190666", {"Joerg", "Faschingbauer"}},
                {"1234250497", {"Caro", "Faschingbauer"}},
                {"2345110695", {"Johanna", "Faschingbauer"}},
    };

    SocialDBInsertCommand ic("3456060486", "Philipp", "Lichtenberger");
    SocialDBCommand* c = &ic;           // <--- Insert is-a Command
    c->execute(db);                     // <--- Insert used-as-a Command

    auto [firstname, lastname] = db.find("3456060486");
    ASSERT_EQ(firstname, "Philipp");
    ASSERT_EQ(lastname, "Lichtenberger");
}

Step 2: Basic find()

Like with insert(), lets ignore the possiblity of errors for a moment. find() differs from insert(), though, in that it has a return value the issuer is sure interested in.

The SocialDBCommand interface has only a very anonymous void execute() method that does not have room for command-specific return values.

Solution: store the return value in the specific command object, and let the issuer ask for it once the command has run.

#include <gtest/gtest.h>

#include <socialdb.h>
#include <social-db-commands.h>


TEST(command_suite, find)
{
    SocialDB db{{"1037190666", {"Joerg", "Faschingbauer"}},
                {"1234250497", {"Caro", "Faschingbauer"}},
                {"2345110695", {"Johanna", "Faschingbauer"}},
    };

    SocialDBFindCommand fc("1037190666");

    SocialDBCommand* c = &fc;                  // <--- used as-a base
    c->execute(db);

    auto [firstname, lastname] = fc.result();  // <--- Hint: use struct SocialDB::Person as return value
    ASSERT_EQ(firstname, "Joerg");
    ASSERT_EQ(lastname, "Faschingbauer");
}

Step 3: drop()

Simplest!

#include <gtest/gtest.h>

#include <socialdb.h>
#include <social-db-commands.h>


TEST(command_suite, drop)
{
    SocialDB db{{"1037190666", {"Joerg", "Faschingbauer"}},
                {"1234250497", {"Caro", "Faschingbauer"}},
                {"2345110695", {"Johanna", "Faschingbauer"}},
    };

    SocialDBDropCommand dc;

    SocialDBCommand* c = &dc;           // <--- used as-a base
    c->execute(db);

    dc.result();                        // <--- no error expected

    ASSERT_EQ(db.size(), 0);
}

Step 4: Handle find() Errors

The way to quickly get to something that works is to ignore errors. Let’s not go in that directory for too long, and find a way to communicate errors to the command’s issuer.

The find command that we implemented above store the database operation’s return value, and make it available to the issuer via the result() method.

Now the find() database operation can throw an error (of type SocialDB::NotFound) that also needs to be made available. Store that in the object, and re-throw it in the result() method.

#include <gtest/gtest.h>

#include <socialdb.h>
#include <social-db-commands.h>


TEST(command_suite, notfound)
{
    SocialDB db;

    SocialDBFindCommand fc("1037190666");

    SocialDBCommand* c = &fc;           // <--- used as-a base
    c->execute(db);

    try {
        fc.result();
        FAIL();
    }
    catch (const SocialDB::NotFound&) {}

    // -Werror=restrict
    // ASSERT_THROW(fc.result(), SocialDB::NotFound);
}

Step 5: Handle insert() Errors

While insert() does not return a value, it can throw. Like in find(), store the exception in the insert command, and re-throw when the issuer requests the result().

#include <gtest/gtest.h>

#include <socialdb.h>
#include <social-db-commands.h>


TEST(command_suite, notinserted)
{
    SocialDB db{{"1037190666", {"Joerg", "Faschingbauer"}},
                {"1234250497", {"Caro", "Faschingbauer"}},
                {"2345110695", {"Johanna", "Faschingbauer"}},
    };

    SocialDBInsertCommand ic("1037190666", "Joerg", "Faschingbauer");

    SocialDBCommand *c = &ic;
    c->execute(db);

    try {
        ic.result();
        FAIL();
    }
    catch (const SocialDB::NotInserted&) {}

    // -Werror=restrict
    // ASSERT_THROW(ic.result(), SocialDB::NotInserted);
}

Step 6: Bulk Insert?

Now that we have an insert command that we can instantiate objects from, we could create a sequence of such objects and encapsulate those in a, say, BulkInsert command. Lets give it a try.

../../../../../_images/command-bulk-insert.png
#include <gtest/gtest.h>

#include <socialdb.h>
#include <social-db-commands.h>


TEST(command_suite, bulk_insert)
{
    SocialDB db;

    SocialDBBulkInsertCommand bic;
    bic.add(SocialDBInsertCommand("1037190666", "Joerg", "Faschingbauer"));
    bic.add(SocialDBInsertCommand("1234250497", "Caro", "Faschingbauer"));
    bic.add(SocialDBInsertCommand("2345110695", "Johanna", "Faschingbauer"));

    SocialDBCommand* c = &bic;          // <--- used as-a base
    c->execute(db);

    ASSERT_EQ(db.size(), 3);
}

Step 6a: Bulk Insert Using std::initializer_list?

To make matters less clumsy, they invented brace initialization. Lets try it out.

#include <gtest/gtest.h>

#include <socialdb.h>
#include <social-db-commands.h>


TEST(command_suite, bulk_insert__std_initializer_list)
{
    SocialDB db;

    SocialDBBulkInsertCommand bic{
        { "1037190666", "Joerg", "Faschingbauer" },
        { "1234250497", "Caro", "Faschingbauer" },
        { "2345110695", "Johanna", "Faschingbauer" },
    };
    
    SocialDBCommand* c = &bic;          // <--- used as-a base
    c->execute(db);

    ASSERT_EQ(db.size(), 3);
}