3
\$\begingroup\$

I'm interested in creating a simple game but what I realized was that half-way through development I get lost in the details with the design. In particular, I find dependency injection, unit testing/mocking and proper abstractions like interfaces to be overwhelming. I figured if I wanted to avoid having problems, I should practice those concepts in a toy project. Here's the code:

task.hpp

#ifndef TASK_HPP
#define TASK_HPP

#include <functional>

class itask {
public:
    virtual ~itask() = default;
    virtual void operator()() const = 0;
    virtual void bind(std::function<void()> f) = 0;
};

class task : public itask {
    std::function<void()> action_;
public:
    task() { }

    void bind(std::function<void()> f) override {
        action_ = f;
    }

    void operator()() const override {
        action_();
    }
};

#endif

actor.hpp

#ifndef ACTOR_HPP
#define ACTOR_HPP

#include <memory>
#include <task.hpp>
#include <vector>

class iactor {
public:
    virtual ~iactor() = default;
    virtual void add_task(std::shared_ptr<itask>) = 0;
    virtual void do_task() = 0;
};

class actor : public iactor {
    std::vector<std::shared_ptr<itask>> tasks_;
public:
    actor() { }

    void add_task(std::shared_ptr<itask> t) override {
        tasks_.emplace_back(t);
    }

    void do_task() override {
        (*tasks_.back().get())();
        tasks_.pop_back();
    }
};

#endif

main.cpp

#include <actor.hpp>
#include <iostream>
#include <cassert>
#include <sstream>
#include <functional>
#include <task.hpp>
#include <vector>

struct bark : public itask {
public:
    bark() { }

    void bind(std::function<void()> f) override {
        assert("Cannot bind to this task.");
    }

    void operator()() const override {
        std::cout << "Woof, woof!\n";
    }
};

struct dog : public iactor {
    std::vector<std::shared_ptr<itask>> tasks_;
public:
    dog() { }

    void add_task(std::shared_ptr<itask> t) override {
        auto try_cast = dynamic_cast<bark*>(t.get());
        if (!try_cast) {
            assert("Pupper can't do what you asked.");
        } else {
            tasks_.emplace_back(t);
        }
    }

    void do_task() override {
        (*tasks_.back().get())();
        tasks_.pop_back();
    }
};

int main() {
    std::unique_ptr<iactor> good_boy = std::make_unique<dog>();
    std::unique_ptr<itask> t = std::make_unique<bark>();

    good_boy->add_task(std::move(t));

    good_boy->do_task();
}

test-actor.cpp

#include "catch.hpp"
#include "fakeit.hpp"
#include <actor.hpp>
#include <memory>

TEST_CASE("wow he really can act!", "[actor]") {
    using namespace fakeit;

    Mock<iactor> actor_mock;

    auto arg = std::make_shared<task>();
    arg->bind([] () {
        std::cout << "Welcome to the test.\n";
    });

    When(Method(actor_mock, add_task).Using(arg)).AlwaysReturn();
    When(Method(actor_mock, do_task)).AlwaysReturn();

    auto &do_stuff = actor_mock.get();
    do_stuff.add_task(arg);
    do_stuff.do_task();

    Verify(Method(actor_mock, add_task).Using(arg));
    Verify(Method(actor_mock, do_task));
}

test-task.cpp

#include "catch.hpp"
#include "fakeit.hpp"
#include <task.hpp>
#include <memory>

TEST_CASE("reporting for duty, sir", "[task]") {
    using namespace fakeit;

    Mock<itask> task_mock;

    auto arg = [] () {
        std::cout << "Welcome to the test.\n";
    };

    When(Method(task_mock, bind)).AlwaysReturn();
    When(Method(task_mock, operator())).AlwaysReturn();

    auto &do_stuff = task_mock.get();
    do_stuff.bind(arg);
    do_stuff();

    Verify(Method(task_mock, bind));
    Verify(Method(task_mock, operator()));
}

Ultimately I'd like to have a lot of different objects with injectable dependencies like a world class that can have a lot of actors. And actors can either be generic (just takes some tasks and executes them) or have specialized behavior like the dog class (only accepts certain types of tasks). My questions are:

  • Is my approach scalable? Does it make sense?

  • Am I testing the interface correctly? Do I need to change/remove the bind function?

  • Am I using smart pointers correctly?

\$\endgroup\$
1
\$\begingroup\$

Does your code make sense? Not really.

bark violates the Liskov substitution principle. And very ineptly, using assert(), which will disappear in a release-build compile with -DNDEBUG.

Either remove bind from itask and create a subclass accepting a callable, or just use std::function directly and accept the possible extra-allocation.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.