Краткое описание проблемы
В этом вопросе есть две конкурирующие проблемы.
- Управление жизненным циклом
Subsystem
с, что позволяет их удалить в нужное время.
- Клиенты
Subsystem
должны знать, что Subsystem
, который они используют, действителен.
Обработка # 1
System
владеет Subsystem
s и должен управлять их жизненным циклом со своими собственными возможностями. Использование shared_ptr
s для этого особенно полезно, поскольку это упрощает разрушение, но вы не должны раздавать их, потому что тогда вы теряете детерминизм, который вы ищете в отношении их освобождения.
Обработка # 2
Это более интересная проблема для решения. Для более подробного описания проблемы вам необходимо, чтобы клиенты получали объект, который ведет себя как Subsystem
, в то время как этот Subsystem
(и его родитель System
) существует, но ведет себя соответствующим образом после уничтожения Subsystem
.
Это легко решается с помощью комбинации Proxy Pattern , State State и Null Object Pattern . Хотя это может показаться немного сложным решением, « Существует простота, которую нужно иметь с другой стороны сложности ». Как разработчики библиотек / API, мы должны приложить дополнительные усилия, чтобы сделать наши системы надежными. Кроме того, мы хотим, чтобы наши системы вели себя интуитивно, как того ожидает пользователь, и изящно затухали, когда они пытались их неправильно использовать. Есть много решений этой проблемы, однако этот должен привести вас ко всему важному моменту, когда, как вы и Скотт Мейерс говорите, " легко использовать правильно и трудно использовать неправильно .
Теперь я предполагаю, что на самом деле System
имеет дело с некоторым базовым классом Subsystem
с, из которого вы получаете различные Subsystem
с. Я представил это ниже как SubsystemBase
. Вам необходимо представить объект Proxy , ниже SubsystemProxy
, который реализует интерфейс SubsystemBase
путем пересылки запросов к объекту, который он передает. (В этом смысле это очень похоже на специальное приложение Pattern Decorator .) Каждый Subsystem
создает один из этих объектов, который он удерживает с помощью shared_ptr
, и возвращает по запросу через GetProxy()
, который вызывается родительским объектом System
при вызове GetSubsystem()
.
Когда System
выходит из области видимости, каждый из его Subsystem
объектов разрушается. В своем деструкторе они вызывают mProxy->Nullify()
, что заставляет их Proxy объектов менять их State . Они делают это, изменяя указатель на Null Object , который реализует интерфейс SubsystemBase
, но делает это, ничего не делая.
Использование Pattern State здесь позволило клиентскому приложению полностью забыть о том, существует ли конкретный Subsystem
. Более того, ему не нужно проверять указатели или хранить экземпляры, которые должны были быть уничтожены.
Proxy Pattern позволяет клиенту зависеть от легковесного объекта, который полностью оборачивает детали внутренней работы API и поддерживает постоянный, унифицированный интерфейс.
Шаблон Null Object позволяет Proxy функционировать после удаления исходного Subsystem
.
Пример кода
Я разместил здесь примерный пример качества псевдокода, но меня это не удовлетворило. Я переписал это, чтобы быть точным, компилирующим (я использовал g ++) примером того, что я описал выше. Чтобы заставить его работать, мне пришлось представить несколько других классов, но их использование должно быть ясно из их названий. Я использовал Singleton Pattern для класса NullSubsystem
, так как имеет смысл, что вам не понадобится больше одного. ProxyableSubsystemBase
полностью абстрагирует поведение 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, рассказывает об использовании нулевых объектов для отладки и разработки. Они конкретно говорили об использовании нулевых графических моделей и текстур, таких как текстура шахматной доски, для того, чтобы недостающие модели действительно выделялись. То же самое можно применить здесь, заменив NullSubsystem
для ReportingSubsystem
, который бы регистрировал вызов и, возможно, стек вызовов при каждом обращении к нему. Это позволило бы вам или клиентам вашей библиотеки отследить, где они находятся, в зависимости от того, что вышло из области видимости, но без необходимости вызывать сбой.
Я упомянул в комментарии @Arkadiy, что круговая зависимость, которую он вывел между System
и Subsystem
, немного неприятна. Это можно легко исправить, если System
получить от интерфейса, от которого зависит Subsystem
, то есть от применения Принципа обращения зависимостей Роберта С. Мартина . Еще лучше было бы изолировать функциональность, которая нужна Subsystem
s, от их родителя, написать интерфейс для этого, затем удержать разработчик этого интерфейса в System
и передать его Subsystem
s, который бы удерживал его через shared_ptr
. Например, у вас может быть LoggerInterface
, который ваш Subsystem
использует для записи в журнал, затем вы можете извлечь CoutLogger
или FileLogger
из него и сохранить его экземпляр в System
.