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;
}