jeudi 28 septembre 2017

Mediator pattern with objects that self-manage

I have an object called UpdateManager that is used to collectively provide update ticks to objects it knows about. Think of this in a game context, where objects need regular update invocations to perform behavior within a scene based on time.

An object that wishes to be attached to UpdateManager has to derive from a class named UpdateReceiver. This object also maintains attaching/detaching to the UpdateManager, to eliminate some boilerplate code required by clients that are using this system.

Example code is below (live sample here):

class UpdateManager;

class UpdateReceiver
{
public:

    UpdateReceiver(std::shared_ptr<UpdateManager>& update_manager);

    virtual ~UpdateReceiver();

    virtual void Update(float dt) = 0;

private:

    std::weak_ptr<UpdateManager> m_updateManager;
};

class UpdateManager
{
public:

    void AddReceiver(UpdateReceiver& receiver)
    {
        std::cout << "Adding Receiver\n";
        m_updateReceivers.push_back(&receiver);
    }

    void RemoveReceiver(UpdateReceiver& receiver)
    {
        auto it = std::find(m_updateReceivers.begin(), m_updateReceivers.end(), &receiver);
        if (it != m_updateReceivers.end())
        {
            std::cout << "Removing Receiver\n";
            m_updateReceivers.erase(it);
        }
    }

    void Update()
    {
        for (auto& receiver : m_updateReceivers)
        {
            receiver->Update(1.f);
        }
    }

private:

    std::list<UpdateReceiver*> m_updateReceivers;
};


UpdateReceiver::UpdateReceiver(std::shared_ptr<UpdateManager>& update_manager)
    : m_updateManager(update_manager)
{
    update_manager->AddReceiver(*this);
}

UpdateReceiver::~UpdateReceiver()
{
    auto update_manager = m_updateManager.lock();
    if (update_manager)
    {
        update_manager->RemoveReceiver(*this);
    }
}

class Timer : public UpdateReceiver
{
public:

    Timer(std::shared_ptr<UpdateManager>& update_manager)
        : UpdateReceiver(update_manager)
    {}

    void Update(float dt) override
    {
        std::cout << "Updating Timer\n";
    }
};

And an example using the code above:

int main()
{
    auto update_manager = std::make_shared<UpdateManager>();

    {
        Timer t{update_manager};

        update_manager->Update();
    }
}

The idea is, the user creates a Timer (which implements UpdateReceiver) and provides it the required shared pointer to UpdateManager. The base class takes care of invoking AddReceiver and RemoveReceiver for clients of Timer, since this is considered boilerplate and this is less error-prone.

The code smell here that I want to be double-checked on is the circular dependency. I know that with the Mediator pattern, it's typical that the objects know about the mediator, and the mediator knows about the objects. But is it justified in this case? Is this design acceptable for the goals it is trying to solve?


Disclaimer: I originally posted this question on Code Review SE, but was told that SO might be a better fit. Originally I felt this question was a bit too open-ended, but I decided to post here anyway in hopes that it is mostly a good fit and doesn't get closed.

Aucun commentaire:

Enregistrer un commentaire