4

看来这个问题在我们的工作中很常见。

我们通过网络发送一个 int 或 enum 值,然后我们收到它,我们想创建/调用一个特定的对象/函数。

最简单的解决方案是使用 switch 语句,如下所示:

switch (value) {
    case FANCY_TYPE_VALUE: return new FancyType();
}

它工作得很好,但是我们会有很多这样的开关块,当我们创建新的值和类型时,我们需要改变所有这些。这似乎是对的。

其他可能性是使用模板。但我们不能,因为枚举的值是在运行时定义的。

是否有任何正确的设计模式或任何正确的方法?

在日常编码中,这似乎是一个非常普遍和常见的问题......

4

4 回答 4

5

试试地图:

struct Base { };
struct Der1 : Base { static Base * create() { return new Der1; } };
struct Der2 : Base { static Base * create() { return new Der2; } };
struct Der3 : Base { static Base * create() { return new Der3; } };

std::map<int, Base * (*)()> creators;

creators[12] = &Der1::create;
creators[29] = &Der2::create;
creators[85] = &Der3::create;

Base * p = creators[get_id_from_network()]();

(这当然很粗略;至少你会有错误检查,以及每类自注册方案,这样你就不会忘记注册一个类。)

于 2012-08-06T15:33:50.913 回答
3

一种选择是维护可以创建具体类型的创建者字典(具有相同的接口)。现在,创建代码将在字典中搜索一个 int 值(来自客户端发送的枚举)并调用 create 方法,该方法通过基类指针返回具体对象。

字典可以在一个地方初始化,具体创建者对应于每个可能的枚举值。

这里的问题是,当您添加新类型的对象时,您必须扩展此字典初始化代码。一种避免的方法如下。

  1. 让创建者寻找一个单例工厂实例,并在构造函数中注册自己,使用它可以创建具体对象的类型 enums(integers)。
  2. 为一个/一组创建者创建一个 DLL,并拥有一个创建者的全局实例。
  3. DLL 的名称可以输入到一个配置文件中,该文件由工厂在初始化时读取。工厂加载此文件中的所有 DLL,这导致创建向工厂注册自身的静态对象。
  4. 现在,工厂拥有了它可以使用具体对象创建者创建的所有类型枚举的映射。
  5. 实现相同的对象创建者查找机制来创建对象。

现在,工厂根本不需要扩展,因为步骤 3,4 和 5 不会因为引入的新对象而改变。步骤 1 可以在一个地方实施。

您唯一需要做的就是为每个应该存在的新具体类型添加一个全局对象,因为 C++ 本身不支持反射。

于 2012-08-06T15:43:36.013 回答
3

您实际上可以使用一些模板技巧来做到这一点:

#include <map>

template <typename Enum, typename Base>
class EnumFactory {
  public:
    static Base* create(Enum e) {
      typename std::map<Enum,EnumFactory<Enum,Base>*>::const_iterator const it = lookup().find(e);
      if (it == lookup().end())
        return 0;
      return it->second->create();
    }
  protected:
    static std::map<Enum,EnumFactory<Enum,Base>*>& lookup() {
      static std::map<Enum,EnumFactory<Enum,Base>*> l;
      return l;
    }
  private:
    virtual Base* create() = 0;
};

template <typename Enum, typename Base, typename Der>
class EnumFactoryImpl : public EnumFactory<Enum,Base> {
  public:
    EnumFactoryImpl(Enum key)
      : position(this->lookup().insert(std::make_pair<Enum,EnumFactory<Enum,Base>*>(key,this)).first) {
    }
    ~EnumFactoryImpl() {
      this->lookup().erase(position);
    }
  private:
    virtual Base* create() {
      return new Der();
    }
    typename std::map<Enum,EnumFactory<Enum,Base>*>::iterator position;
};

这允许您从给定创建一个新的派生对象enum,通过说

// will create a new `FancyType` object if `value` evaluates to `FANCY_TYPE_VALUE` at runtime
EnumFactory<MyEnum,MyBase>::create(value)

但是,您必须有一些 EnumFactoryImpl 对象,这些对象在某些函数或命名空间中可能是静态的。

namespace {
  EnumFactoryImpl<MyEnum,MyBase,Derived1> const fi1(ENUM_VALUE_1);
  EnumFactoryImpl<MyEnum,MyBase,Derived2> const fi2(ENUM_VALUE_2);
  EnumFactoryImpl<MyEnum,MyBase,Derived3> const fi3(ENUM_VALUE_3);
  EnumFactoryImpl<MyEnum,MyBase,FancyType> const fi1(FANCY_TYPE_VALUE); // your example
}

这些行是源代码将enum值映射到派生类型的单点。因此,您将所有内容都放在同一个位置,并且没有冗余(这消除了在添加新派生类型时忘记在某些地方更改它的问题)。

于 2012-08-06T18:32:09.420 回答
2

kogut,我不建议将其作为答案,但是由于您要求我扩展我对原始问题的评论,因此这里是.net 环境为您提供的内容的非常简短的摘要...

public enum MyEnum
{
    [MyAttribute(typeof(ClassNone))]
    None,
    [MyAttribute(typeof(ClassOne))]
    One,
    [MyAttribute(typeof(ClassTwo))]
    Two,
    [MyAttribute(typeof(ClassThree))]
    Three
}

所以你有你的基本枚举一、二、三等,它的工作原理就像....呃....一个枚举!

但是您还编写了一个名为 MyAttribute 的类(事实上,要了解这方面的更多信息,只需搜索 Attributes)。但是正如您所看到的,这允许您在设计时说,某某枚举值与某某类相关联。

此信息存储在枚举的元数据中(托管环境的值!),并且可以在运行时查询(使用反射)。不用说这非常强大,我已经使用这种机制系统地删除了您问题的其他答案中提出的那种地图的负载。

有用性的一个例子是……在我与之合作的一个客户中,约定是将状态作为字符串存储在数据库中,因为它们对于需要运行表查询的人来说更具可读性。但这在应用程序中毫无意义,其中状态作为枚举推送。采用上述方法(使用字符串而不是类型),当数据被读取和写入时,这种转换发生在一行代码中。另外,当然,一旦您定义了 MyAttribute,它就可以被标记到您喜欢的任何枚举上。

如果这些天我选择的语言是 c#,但这在(托管)c++ 中也很好。

于 2012-08-07T09:12:21.950 回答