问题总结
这个问题有两个相互竞争的问题。
- s 的生命周期管理
Subsystem
,允许它们在正确的时间被移除。
- s 的客户
Subsystem
需要知道Subsystem
他们正在使用的 是有效的。
处理#1
System
拥有Subsystem
s 并且应该用它自己的范围来管理它们的生命周期。为此使用shared_ptr
s 特别有用,因为它简化了销毁,但您不应该将它们分发出去,因为这样您就失去了您正在寻求的关于它们的释放的确定性。
处理#2
这是需要解决的更有趣的问题。更详细地描述问题,您需要客户端接收一个对象,该对象的行为类似于Subsystem
while Subsystem
(它是 parent System
)存在,但在 aSubsystem
被销毁后行为适当。
这很容易通过代理模式、状态模式和空对象模式的组合来解决。虽然这似乎是一个有点复杂的解决方案,但“只有在复杂性的另一面才有简单性。” 作为库/API 开发人员,我们必须加倍努力以使我们的系统更加健壮。此外,我们希望我们的系统能够按照用户的期望直观地运行,并在他们试图滥用它们时优雅地衰减。这个问题有很多解决方案,但是,这个应该让您达到最重要的一点,正如您和Scott Meyers所说,它“易于正确使用,难以正确使用。 ”
现在,我假设在现实中System
处理一些Subsystem
s 的基类,您可以从中派生各种不同Subsystem
的 s。我在下面介绍了它SubsystemBase
。您需要在下面引入一个Proxy对象,它通过将请求转发到它所代理的对象来SubsystemProxy
实现接口。SubsystemBase
(从这个意义上说,它非常像装饰模式的特殊用途应用程序。)每个都Subsystem
创建这些对象中的一个,它通过 a 持有shared_ptr
,并在通过请求时返回GetProxy()
,在调用时由父System
对象GetSubsystem()
调用。
当 aSystem
超出范围时,它的每个Subsystem
对象都会被破坏。在他们的析构函数中,他们调用mProxy->Nullify()
,这会导致他们的Proxy对象改变他们的State。他们通过更改指向实现接口的Null ObjectSubsystemBase
来做到这一点,但什么都不做。
在这里使用状态模式允许客户端应用程序完全不知道某个特定对象是否Subsystem
存在。此外,它不需要检查指针或保留本应销毁的实例。
代理模式允许客户端依赖于一个轻量级的对象,该对象完全封装了 API 内部工作的细节,并保持了一个恒定的、统一的接口。
Null Object Pattern允许代理在删除原始对象后运行Subsystem
。
示例代码
我在这里放置了一个粗略的伪代码质量示例,但我对此并不满意。我已经将它重写为我上面描述的一个精确的编译(我使用了 g++)示例。为了让它工作,我不得不介绍一些其他的类,但它们的用途应该从它们的名称中清楚地看出。我在课堂上使用了单例模式NullSubsystem
,因为你不需要超过一个是有道理的。 ProxyableSubsystemBase
完全从 Proxying 中抽象出 Proxying 行为Subsystem
,使其不知道这种行为。这是类的UML图:
示例代码:
#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;
}
代码输出:
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.
其他想法:
我在一本 Game Programming Gems 书籍中读到的一篇有趣的文章谈到了使用 Null 对象进行调试和开发。他们专门讨论了使用 Null 图形模型和纹理,例如棋盘纹理,以使缺失的模型真正脱颖而出。通过更改 for a 可以在此处应用相同的方法,该NullSubsystem
for aReportingSubsystem
将记录调用,并可能在访问时记录调用堆栈。这将允许您或您的图书馆的客户根据超出范围的内容追踪他们所在的位置,但无需导致崩溃。
我在@Arkadiy 的评论中提到,他在两者之间提出的循环依赖System
有点Subsystem
令人不快。它可以很容易地通过System
从依赖的接口派生来解决,这Subsystem
是 Robert C Martin 的依赖倒置原则的应用。更好的办法是将Subsystem
s 需要的功能与其父级隔离,为此编写一个接口,然后保留该接口的实现者System
并将其传递给Subsystem
s,后者将通过 a 保存它shared_ptr
。例如,您可能有LoggerInterface
用于Subsystem
写入日志的 ,然后您可以从它派生CoutLogger
或FileLogger
从它派生,并在 中保留这样的实例System
。