Problem Summary
There are two competing concerns in this question.
- Life-cycle management of
Subsystem
s, allowing their removal at the right time.
- Clients of
Subsystem
s need to know that the Subsystem
they are using is valid.
Handling #1
System
owns the Subsystem
s and should manage their life-cycle with it's own scope. Using shared_ptr
s for this is particularly useful as it simplifies destruction, but you should not be handing them out because then you loose the determinism you are seeking with regard to their deallocation.
Handling #2
This is the more intersting concern to address. Describing the problem in more detail, you need clients to receive an object which behaves like a Subsystem
while that Subsystem
(and it's parent System
) exists, but behaves appropriately after a Subsystem
is destroyed.
This is easily solved by a combination of the Proxy Pattern, the State Pattern and the Null Object Pattern. While this may seem to be a bit complex of a solution, 'There is a simplicity only to be had on the other side of complexity.' As Library/API developers, we must go the extra mile to make our systems robust. Further, we want our systems to behave intuitively as a user expects, and to decay gracefully when they attempt to misuse them. There are many solutions to this problem, however, this one should get you to that all important point where, as you and Scott Meyers say, it is "easy to use correctly and hard to use incorrectly.'
Now, I am assuming that in reality, System
deals in some base class of Subsystem
s, from which you derive various different Subsystem
s. I've introduced it below as SubsystemBase
. You need to introduce a Proxy object, SubsystemProxy
below, which implements the interface of SubsystemBase
by forwarding requests to the object it is proxying. (In this sense, it is very much like a special purpose application of the Decorator Pattern.) Each Subsystem
creates one of these objects, which it holds via a shared_ptr
, and returns when requested via GetProxy()
, which is called by the parent System
object when GetSubsystem()
is called.
When a System
goes out of scope, each of it's Subsystem
objects gets destructed. In their destructor, they call mProxy->Nullify()
, which causes their Proxy objects to change their State. They do this by changing to point to a Null Object, which implements the SubsystemBase
interface, but does so by doing nothing.
Using the State Pattern here has allowed the client application to be completely oblivious to whether or not a particular Subsystem
exists. Moreover, it does not need to check pointers or keep around instances that should have been destroyed.
The Proxy Pattern allows the client to be dependent on a light weight object that completely wraps up the details of the API's inner workings, and maintains a constant, uniform interface.
The Null Object Pattern allows the Proxy to function after the original Subsystem
has been removed.
Sample Code
I had placed a rough pseudo-code quality example here, but I wasn't satisfied with it. I've rewritten it to be a precise, compiling (I used g++) example of what I have described above. To get it to work, I had to introduce a few other classes, but their uses should be clear from their names. I employed the Singleton Pattern for the NullSubsystem
class, as it makes sense that you wouldn't need more than one. ProxyableSubsystemBase
completely abstracts the Proxying behavior away from the Subsystem
, allowing it to be ignorant of this behavior. Here is the UML Diagram of the classes:
Example Code:
#include <iostream>
#include <string>
#include <vector>
#include <boost/shared_ptr.hpp>
// Forward Declarations to allow friending
class System;
class ProxyableSubsystemBase;
// Base defining the interface for Subsystems
class SubsystemBase
{
public:
// pure virtual functions
virtual void DoSomething(void) = 0;
virtual int GetSize(void) = 0;
virtual ~SubsystemBase() {} // virtual destructor for base class
};
// Null Object Pattern: an object which implements the interface to do nothing.
class NullSubsystem : public SubsystemBase
{
public:
// implements pure virtual functions from SubsystemBase to do nothing.
void DoSomething(void) { }
int GetSize(void) { return -1; }
// Singleton Pattern: We only ever need one NullSubsystem, so we'll enforce that
static NullSubsystem *instance()
{
static NullSubsystem singletonInstance;
return &singletonInstance;
}
private:
NullSubsystem() {} // private constructor to inforce Singleton Pattern
};
// Proxy Pattern: An object that takes the place of another to provide better
// control over the uses of that object
class SubsystemProxy : public SubsystemBase
{
friend class ProxyableSubsystemBase;
public:
SubsystemProxy(SubsystemBase *ProxiedSubsystem)
: mProxied(ProxiedSubsystem)
{
}
// implements pure virtual functions from SubsystemBase to forward to mProxied
void DoSomething(void) { mProxied->DoSomething(); }
int GetSize(void) { return mProxied->GetSize(); }
protected:
// State Pattern: the initial state of the SubsystemProxy is to point to a
// valid SubsytemBase, which is passed into the constructor. Calling Nullify()
// causes a change in the internal state to point to a NullSubsystem, which allows
// the proxy to still perform correctly, despite the Subsystem going out of scope.
void Nullify()
{
mProxied=NullSubsystem::instance();
}
private:
SubsystemBase *mProxied;
};
// A Base for real Subsystems to add the Proxying behavior
class ProxyableSubsystemBase : public SubsystemBase
{
friend class System; // Allow system to call our GetProxy() method.
public:
ProxyableSubsystemBase()
: mProxy(new SubsystemProxy(this)) // create our proxy object
{
}
~ProxyableSubsystemBase()
{
mProxy->Nullify(); // inform our proxy object we are going away
}
protected:
boost::shared_ptr<SubsystemProxy> GetProxy() { return mProxy; }
private:
boost::shared_ptr<SubsystemProxy> mProxy;
};
// the managing system
class System
{
public:
typedef boost::shared_ptr< SubsystemProxy > SubsystemHandle;
typedef boost::shared_ptr< ProxyableSubsystemBase > SubsystemPtr;
SubsystemHandle GetSubsystem( unsigned int index )
{
assert( index < mSubsystems.size() );
return mSubsystems[ index ]->GetProxy();
}
void LogMessage( const std::string& message )
{
std::cout << " <System>: " << message << std::endl;
}
int AddSubsystem( ProxyableSubsystemBase *pSubsystem )
{
LogMessage("Adding Subsystem:");
mSubsystems.push_back(SubsystemPtr(pSubsystem));
return mSubsystems.size()-1;
}
System()
{
LogMessage("System is constructing.");
}
~System()
{
LogMessage("System is going out of scope.");
}
private:
// have to hold base pointers
typedef std::vector< boost::shared_ptr<ProxyableSubsystemBase> > SubsystemList;
SubsystemList mSubsystems;
};
// the actual Subsystem
class Subsystem : public ProxyableSubsystemBase
{
public:
Subsystem( System* pParentSystem, const std::string ID )
: mParentSystem( pParentSystem )
, mID(ID)
{
mParentSystem->LogMessage( "Creating... "+mID );
}
~Subsystem()
{
mParentSystem->LogMessage( "Destroying... "+mID );
}
// implements pure virtual functions from SubsystemBase
void DoSomething(void) { mParentSystem->LogMessage( mID + " is DoingSomething (tm)."); }
int GetSize(void) { return sizeof(Subsystem); }
private:
System * mParentSystem; // raw pointer to avoid cycles - can also use weak_ptrs
std::string mID;
};
//////////////////////////////////////////////////////////////////
// Actual Use Example
int main(int argc, char* argv[])
{
std::cout << "main(): Creating Handles H1 and H2 for Subsystems. " << std::endl;
System::SubsystemHandle H1;
System::SubsystemHandle H2;
std::cout << "-------------------------------------------" << std::endl;
{
std::cout << " main(): Begin scope for System." << std::endl;
System mySystem;
int FrankIndex = mySystem.AddSubsystem(new Subsystem(&mySystem, "Frank"));
int ErnestIndex = mySystem.AddSubsystem(new Subsystem(&mySystem, "Ernest"));
std::cout << " main(): Assigning Subsystems to H1 and H2." << std::endl;
H1=mySystem.GetSubsystem(FrankIndex);
H2=mySystem.GetSubsystem(ErnestIndex);
std::cout << " main(): Doing something on H1 and H2." << std::endl;
H1->DoSomething();
H2->DoSomething();
std::cout << " main(): Leaving scope for System." << std::endl;
}
std::cout << "-------------------------------------------" << std::endl;
std::cout << "main(): Doing something on H1 and H2. (outside System Scope.) " << std::endl;
H1->DoSomething();
H2->DoSomething();
std::cout << "main(): No errors from using handles to out of scope Subsystems because of Proxy to Null Object." << std::endl;
return 0;
}
Output from the code:
main(): Creating Handles H1 and H2 for Subsystems.
-------------------------------------------
main(): Begin scope for System.
<System>: System is constructing.
<System>: Creating... Frank
<System>: Adding Subsystem:
<System>: Creating... Ernest
<System>: Adding Subsystem:
main(): Assigning Subsystems to H1 and H2.
main(): Doing something on H1 and H2.
<System>: Frank is DoingSomething (tm).
<System>: Ernest is DoingSomething (tm).
main(): Leaving scope for System.
<System>: System is going out of scope.
<System>: Destroying... Frank
<System>: Destroying... Ernest
-------------------------------------------
main(): Doing something on H1 and H2. (outside System Scope.)
main(): No errors from using handles to out of scope Subsystems because of Proxy to Null Object.
Other Thoughts:
An interesting article I read in one of the Game Programming Gems books talks about using Null Objects for debugging and development. They were specifically talking about using Null Graphics Models and Textures, such as a checkerboard texture to make missing models really stand out. The same could be applied here by changing out the NullSubsystem
for a ReportingSubsystem
which would log the call and possibly the callstack whenever it is accessed. This would allow you or your library's clients to track down where they are depending on something that has gone out of scope, but without the need to cause a crash.
I mentioned in a comment @Arkadiy that the circular dependency he brought up between System
and Subsystem
is a bit unpleasant. It can easily be remedied by having System
derive from an interface on which Subsystem
depends, an application of Robert C Martin's Dependency Inversion Principle. Better still would be to isolate the functionality that Subsystem
s need from their parent, write an interface for that, then hold onto an implementor of that interface in System
and pass it to the Subsystem
s, which would hold it via a shared_ptr
. For example, you might have LoggerInterface
, which your Subsystem
uses to write to the log, then you could derive CoutLogger
or FileLogger
from it, and keep an instance of such in System
.