В поисках лучшей фабрики класса C ++ - PullRequest
6 голосов
/ 12 декабря 2008

У меня есть приложение, в котором есть несколько объектов (на данный момент около 50, но оно растет). В приложении имеется только один экземпляр каждого из этих объектов, и эти экземпляры становятся общими для компонентов.

Я извлек все объекты из базового класса BrokeredObject:

class BrokeredObject
{
  virtual int GetInterfaceId() = 0;
};

И каждый тип объекта возвращает уникальный идентификатор. Эти идентификаторы хранятся в заголовочном файле.

У меня тогда есть "фабрика" ObjectBroker. Когда кому-то нужен объект, вызывайте GetObjectByID (). Бокер просматривает список STL, чтобы увидеть, существует ли объект, если он существует, он возвращает его. Если нет, он создает его, помещает в список и возвращает его. Все хорошо.

BrokeredObject *GetObjectByID(int id)
{
  BrokeredObject *pObject;
  ObjectMap::iterator = m_objectList.find(id);
  // etc.
  if(found) return pObject;

  // not found, so create
  switch(id)
  {
    case 0: pObject = new TypeA; break;
    case 1: pObject = new TypeB; break;
    // etc.
    // I loathe this list
  }
  // add it to the list
  return pObject;
}

Что мне кажется болезненным, так это поддержание этого списка идентификаторов и необходимость того, чтобы каждый класс его реализовывал. По крайней мере, я немного облегчил жизнь своим потребителям, так как каждый тип хранит информацию о своем собственном идентификаторе, например:

class TypeA : public BrokeredObject
{
  static int get_InterfaceID() { return IID_TYPEA; }
  int GetInterfaceID() { return get_InterfaceID(); }
};

Так что я могу получить такой объект:

GetObjectByID(TypeA::get_InterfaceID());

Нам нужно знать, что такое отображение идентификаторов, но я все еще не в восторге от обслуживания и возможности ошибок. Кажется, что если я знаю тип, зачем мне также знать идентификатор?

То, к чему я стремлюсь, примерно такое в C #:

BrokeredObject GetOrCreateObject<T>() where T : BrokeredObject
{
  return new T();
}

Где ObjectBroker будет создавать объект на основе переданного типа .

Испортил ли меня C #, и это просто факт жизни, что C ++ не может этого сделать или есть способ добиться этого, которого я не вижу?

Ответы [ 10 ]

9 голосов
/ 12 декабря 2008

Да, есть способ. Довольно просто даже в C ++ к тому, что делает этот код C # (хотя без проверки наследования):

template<typename T>
BrokeredObject * GetOrCreateObject() {
  return new T();
}

Это будет работать и делать то же самое, что и код C #. Это также безопасно для типов: если тип, который вы передаете, не унаследован от BrokeredObject (или это не тот тип), тогда компилятор стонет в операторе return. Однако он всегда будет возвращать новый объект.

Singleton

Как предположил другой парень (благодарность ему), все это очень похоже на хороший пример синглтон-паттерна. Просто выполните TypeA::getInstance(), чтобы получить один и тот же экземпляр, хранящийся в статической переменной этого класса. Я полагаю, что это было бы намного проще, чем описанным выше способом, без необходимости использования идентификаторов для его решения (я ранее показал способ использования идентификаторов для хранения идентификаторов в этом ответе, но я обнаружил, что он фактически является именно тем, что является синглтоном).

Я читал, что у вас будет возможность открыть несколько экземпляров классов. Один из способов сделать это - иметь Mingleton (я придумал это слово:))

enum MingletonKind {
    SINGLETON,
    MULTITON
};

// Singleton
template<typename D, MingletonKind>
struct Mingleton {
    static boost::shared_ptr<D> getOrCreate() {
        static D d;
        return boost::shared_ptr<D>(&d, NoopDel());
    }

    struct NoopDel {
        void operator()(D const*) const { /* do nothing */ }
    };
};

// Multiton
template<typename D>
struct Mingleton<D, MULTITON> {
    static boost::shared_ptr<D> getOrCreate() {
        return boost::shared_ptr<D>(new D);
    }
};

class ImASingle : public Mingleton<ImASingle, SINGLETON> {
public:
    void testCall() { }
    // Indeed, we have to have a private constructor to prevent
    // others to create instances of us.
private:
    ImASingle() { /* ... */ }
    friend class Mingleton<ImASingle, SINGLETON>;
};

class ImAMulti : public Mingleton<ImAMulti, MULTITON> {
public:
    void testCall() { }
    // ...
};

int main() {
    // both do what we expect.
    ImAMulti::getOrCreate()->testCall();
    ImASingle::getOrCreate()->testCall();
}

Теперь вы просто используете SomeClass::getOrCreate(), и он заботится о деталях. Пользовательское средство удаления в случае синглтона для shared_ptr делает удаление недопустимым, поскольку объект, принадлежащий shared_ptr, размещается статически. Однако следует помнить о проблемах порядка уничтожения статических переменных: Статический порядок инициализации fiasco

5 голосов
/ 13 декабря 2008

Я бы решил эту проблему, используя то, что я назвал бы Статическим шаблоном реестра, который, по моему мнению, представляет собой версию внедрения зависимостей на C ++.

По сути, у вас есть статический список объектов-строителей типа, который вы используете для создания объектов другого типа.

Базовая реализация статического реестра будет выглядеть так:

template <class T>
class StaticRegistry
{
public:
    typedef std::list<T*>   Container;

    static  StaticRegistry<T>&  GetInstance()
    {
        if (Instance == 0)
        {
            Instance = new StaticRegistry<T>;
        }
        return *Instance;
    }

    void    Register(T* item)
    {
        Items.push_back(item);
    }

    void    Deregister(T* item)
    {
        Items.remove(item);
        if (Items.empty())
        {
            delete this;
            Instance = 0;
        }
    }

    typedef typename Container::const_iterator  const_iterator;

    const_iterator begin() const
    {
        return Items.begin();
    }

    const_iterator end() const
    {
        return Items.end();
    }

protected:
    StaticRegistry() {}
    ~StaticRegistry() {}

private:
    Container               Items;

    static StaticRegistry<T>*   Instance;
};

template <class T>
StaticRegistry<T>* StaticRegistry<T>::Instance = 0;

Реализация BrokeredObjectBuilder может выглядеть следующим образом:

class BrokeredObjectBuilderBase {
public:
    BrokeredObjectBuilderBase() { StaticRegistry<BrokeredObjectBuilderBase>::GetInstance().Register(this); }
    virtual ~BrokeredObjectBuilderBase() { StaticRegistry<BrokeredObjectBuilderBase>::GetInstance().Deregister(this); }

    virtual int GetInterfaceId() = 0;
    virtual BrokeredObject* MakeBrokeredObject() = 0;
};


template<class T>
class BrokeredObjectBuilder : public BrokeredObjectBuilderBase {
public:
    BrokeredObjectBuilder(unsigned long interface_id) : m_InterfaceId(interface_id) { } 
    virtual int GetInterfaceId() { return m_InterfaceId; }
    virtual T* MakeBrokeredObject() { return new T; }
private:
    unsigned long m_InterfaceId;
};


class TypeA : public BrokeredObject
{
   ...
};

// Create a global variable for the builder of TypeA so that it's 
// included in the BrokeredObjectBuilderRegistry
BrokeredObjectBuilder<TypeA> TypeABuilder(TypeAUserInterfaceId);

typedef StaticRegistry<BrokeredObjectBuilderBase> BrokeredObjectBuilderRegistry;

BrokeredObject *GetObjectByID(int id)
{
  BrokeredObject *pObject(0);
  ObjectMap::iterator = m_objectList.find(id);
  // etc.
  if(found) return pObject;

  // not found, so create
  BrokeredObjectBuilderRegistry& registry(BrokeredObjectBuilderRegistry::GetInstance());
  for(BrokeredObjectBuilderRegistry::const_iterator it = registry.begin(), e = registry.end(); it != e; ++it)
  {
    if(it->GetInterfaceId() == id)
    {
      pObject = it->MakeBrokeredObject();
      break;
    }
  }

  if(0 == pObject)
  {
    // userinterface id not found, handle this here
    ...
  }      

  // add it to the list
  return pObject;
}

Плюсы:

  • Весь код, который знает о создании типов, разделен на компоновщики, и классы BrokeredObject не должны знать об этом.
  • Эту реализацию можно использовать в библиотеках, и вы можете контролировать на уровне проекта, какие сборщики втягивают в проект, используя ряд различных методов.
  • Строители могут быть такими же сложными или простыми (как выше), как вы хотите.

Минусы:

  • Включена небольшая часть инфраструктуры (но не слишком).
  • Гибкость определения глобальных переменных, включающая то, что строители включают в ваш проект, делает его немного беспорядочным.
  • Я нахожу, что людям трудно понять эту модель, я не знаю почему.
  • Иногда непросто узнать, что находится в статическом реестре в любой момент времени.
  • Приведенная выше реализация приводит к утечке одного бита памяти. (Я могу жить с этим ...)

Вышеприведенная реализация очень проста, вы можете расширить ее различными способами в зависимости от ваших требований.

3 голосов
/ 12 декабря 2008

Используйте шаблонный класс в качестве брокера.
Сделайте экземпляр статическим членом функции. Он будет создан при первом использовании и автоматически уничтожен при выходе из программы.

template <class Type>
class BrokeredObject
{
    public:
        static Type& getInstance()
        {
            static Type theInstance;

            return theInstance;
        }
}; 

class TestObject
{
    public:
       TestObject()
       {}
};


int main()
{
    TestObject& obj =BrokeredObject<TestObject>::getInstance();
}
3 голосов
/ 12 декабря 2008

Вместо GetInterfaceId () в базовом классе BrokeredObject вы можете определить этот чисто виртуальный метод:

virtual BrokeredObject& GetInstance()=0;

И в производных классах вы вернете из этого метода экземпляр определенного производного класса, если он уже создан, если нет, вы сначала создадите его, а затем вернете.

2 голосов
/ 12 декабря 2008

Не похоже, что вам нужен глобальный объект для управления, так почему бы не переместить все в сами классы?

template <class Type>
class BrokeredObject
{
protected:
    static Type *theInstance;

public:
    static Type *getOrCreate()
    {
        if (!theInstance) {
            theInstance = new Type();
        }

        return theInstance;
    }

    static void free()
    {
        delete theInstance;
    }

};

class TestObject : public BrokeredObject<TestObject>
{
public:
    TestObject()
    {}

};


int
main()
{
    TestObject *obj = TestObject::getOrCreate();
}
1 голос
/ 13 декабря 2008

Если вы всегда знаете тип во время компиляции, бессмысленно вызывать BrokeredObject* p = GetObjectByID(TypeA::get_InterfaceID()) вместо TypeA* p = new TypeA или TypeA o напрямую.

Если вы, с другой стороны, не знаете точный тип во время компиляции, вы можете использовать какой-нибудь реестр типов.

template <class T>
BrokeredObject* CreateObject()
{
    return new T();
}

typedef int type_identity;
typedef std::map<type_identity, BrokeredObject* (*)()> registry;
registry r;

class TypeA : public BrokeredObject
{
public:
     static const type_identity identity;
};

class TypeB : public BrokeredObject
{
public:
     static const type_identity identity;
};

r[TypeA::identity] = &CreateObject<TypeA>;
r[TypeB::identity] = &CreateObject<TypeB>;

или если у вас включен RTTI, вы можете использовать type_info как type_identity:

typedef const type_info* type_identity;
typedef std::map<type_identity, BrokeredObject* (*)()> registry;
registry r;

r[&typeid(TypeA)] = &CreateObject<TypeA>;
r[&typeid(TypeB)] = &CreateObject<TypeB>;

Разумеется, в любом случае каждый новый класс может самостоятельно регистрироваться в реестре, делая регистрацию децентрализованной, а не централизованной.

1 голос
/ 12 декабря 2008

Если у вас включен RTTI , вы можете получить имя класса, используя typeid .

Один вопрос, почему вы используете фабрику, а не шаблон синглтона для каждого класса?


Редактировать: ОК, так что вы не хотите быть заблокированными в синглтоне; нет проблем. Замечательная вещь в C ++ - это то, что он дает вам большую гибкость. Вы можете иметь функцию-член GetSharedInstance (), которая возвращает статический экземпляр класса, но оставить конструктор открытым, чтобы вы могли создавать другие экземпляры.
0 голосов
/ 31 декабря 2008

Мой вариант использования имел тенденцию становиться немного более сложным - мне нужна была возможность немного инициализировать объекты, и мне нужно было иметь возможность загружать объекты из разных библиотек DLL в зависимости от конфигурации (например, моделируемая и фактическая для оборудования) , Он начал выглядеть так, будто COM и ATL были тем местом, куда я направлялся, но я не хотел добавлять вес COM в ОС (это делается в CE).

То, что я закончил, было основано на шаблонах (спасибо litb за то, что поставили меня на путь) и выглядит так:

class INewTransModule
{
  public:
    virtual bool Init() { return true; }
    virtual bool Shutdown() { return true; }
};

template <typename T>
struct BrokeredObject
{
public:
    inline static T* GetInstance()
  {
    static T t;
    return &t;
  }
};

template <> 
struct BrokeredObject<INewTransModule>
{
public:
    inline static INewTransModule* GetInstance()
  {
    static INewTransModule t;
    // do stuff after creation
    ASSERT(t.Init());
    return &t;
  }
};

class OBJECTBROKER_API ObjectBroker
{
  public: 
    // these calls do configuration-based creations
    static ITraceTool  *GetTraceTool();
    static IEeprom     *GetEeprom();
    // etc
};

Затем, чтобы убедиться, что объекты (поскольку они шаблонизированы) действительно скомпилированы, я добавил следующие определения:

class EepromImpl: public BrokeredObject<EepromImpl>, public CEeprom
{
};

class SimEepromImpl: public BrokeredObject<SimEepromImpl>, public CSimEeprom
{
};
0 голосов
/ 12 декабря 2008

Почему не это?

    template 
    BrokeredObject* GetOrCreateObject()
    {
      return new T();
    }
0 голосов
/ 12 декабря 2008

Вы почти наверняка должны использовать внедрение зависимостей.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...