Class design: How to implement RAII ressource management for Log-Listener class

79 Views Asked by At

EDIT: Alright, the question does not seem to be suitable for the platform as there is no real technical background here. No problem (really, no sarcasm), I will look somewhere else for advice. Thanks anyway.

I have a question purely about class design: Assume, we want to attach LogListener objects to a central managing host (Host class in the minimal example. We cannot change this implementation as it comes from a foreign codebase).

One crucial thing to ensure is to detach from the Host as soon as our listening task is done. For this to accomplish I want to call the detach method of the host from the destructor of some class in our codebase (current options for this task are the simplified classes Manager{A,B,C}).

My question now is concerned on how to design this. I have listed three options of which I currently opt for the first one as it is the most convenient to use for the moment but it violates the single responsibility principle (managing the Host-connection and listen to the logs). Further Listener derivations do not benefit from the connection management.

Do you have any advice for which way to go? I am pretty sure, I missed Option 4, 5, ... If you have some further ideas, those are very welcome!

#include <iostream>
#include <memory>
#include <set>
#include <utility>

// Simplified base class for the Listener interface
// ================================================
class Listener {
public:
    virtual void listen(const std::string& msg) = 0;
};

// Simplified implementation of a Host, storing logging listeners
// ==============================================================
class Host {
public:
    void attach(Listener* listener) { m_coll.insert(listener); };
    void detach(Listener* listener) { m_coll.erase(listener); };
    void call(const std::string& msg) { for(auto* L : m_coll) { L->listen(msg); }}
private:
    std::set<Listener*> m_coll;
};

static Host globalHost{};


class DerivedListener : public Listener {
public:
    void listen (const std::string& msg) override { std::cout << "Two!\n"; }
};

// Solution 1. Violates single responsibility, but has no "lifetime issues"
// Manager + Listener in one class
// ========================================================================
class ManagerA : public Listener {
public:
    ManagerA() { globalHost.attach(this); }
    ~ManagerA() { globalHost.detach(this); }
    void listen (const std::string& msg) override { std::cout << "One!\n"; }
};

// Solution 2. Has lifetime issues. Takes a raw-pointer to the listener but
// what if the managed pointer is deallocated during lifetime of the Manager?
// Moreover it has Optional semantics - the pointer may be NULL but then it
// cannot be attached as Host does not tolerate nullptrs.
// ==========================================================================
class ManagerB {
public:
    explicit ManagerB (Listener* listener) : m_ptr{listener} { if (m_ptr) globalHost.attach(m_ptr); }
    ~ManagerB() { globalHost.detach(m_ptr); }
private:
    Listener* m_ptr;
};

// Solution 3: Takes ownership of the managed ressource -> No lifetime issues
// but must implement Optional-Semantics because the internal ressource is
// empty after the claim. The claim again is neccessary as the caller needs
// access to the managed object and make independent of the manager.
class ManagerC {
public:
    explicit ManagerC (std::unique_ptr<Listener> listener) : m_ptr{std::move(listener)}
        { if (m_ptr) globalHost.attach(m_ptr.get()); }
    ~ManagerC() { globalHost.detach(m_ptr.get()); }
    std::unique_ptr<Listener> claim() { globalHost.detach(m_ptr.get()); return std::exchange(m_ptr, nullptr); }
private:
    std::unique_ptr<Listener> m_ptr;
};

int main() {
    ManagerA mA{};

    DerivedListener listenerB;
    ManagerB mB{&listenerB};

    ManagerC mC{std::make_unique<DerivedListener>()};
    auto listenerC = mC.claim();

    return 0;
}
0

There are 0 best solutions below