在我的上一篇博客对象工厂(1)---和万恶的 switch 说再见中,我们通过使用函数指针索引的方法,为我们的工厂类代码中消除了 switch 语句。本篇博客的目标是将实现一个泛化的工厂类,实现代码复用。

下面让我们先分析一下在对象工厂(1)---和万恶的 switch 说再见中的工厂类的几个主要角色:

  1. Abstract 基类(文中的 Shape),通过操作这个基类的指针,整个系统实现了运行期多态
  2. Concrete 子类(文中的 Line 等),每个携带真正逻辑代码的具体类。
  3. Identifier(文中的 int),Factory 类需要从 Identifier 映射到正确的具体类创建函数。
  4. Creator(文中的 CreateLine 等),用于真正产生具体类的各个函数。
如果我们使用模板编程,将上面的概念作为模板参数,那么一个泛型工厂类便呼之欲出了。可是,且慢,让我们分析一下这4个角色是否 Factory 类都需要了解呢?答案显然不是,Concrete 子类对 Factory 完全是透明的,Factory 在创建对象的时候,不需要也不应该关心到底有哪些具体子类(这个正是我们上篇博客所解决的问题)。

现在给出泛型工厂类的基本代码:

template
<
  class AbstractProduct,
  typename IdentifierType,
  typename ProductCreator = AbstractProduct *(*)(),
>
class Factory {
public:
  bool Register(const IdentifierType &id, ProductCreator creator) {
    return associations_.insert(typename AssocMap::value_type(id, creator)).second;
  }
  bool Unregister(const IdentifierType &id) {
    return associations_.erase(id) == 1;
  }
  AbstractProduct *CreateObject(const IdentifierType &id) const {
    typename AssocMap::const_iterator i = associations_.find(id);
    if (i == associations_.end()) {
      // 呜呼,出现错误了,这里应该给客户端自有处理这种情况的自由
    }
    return (i->second)();
  }
private:
  typedef std::map<IdentifierType, ProductCreator> AssocMap;
  AssocMap associations_;
};
上面的代码为 ProductCreator 提供了一个默认的类型,一个函数指针,语法乍看有点怪异,但是如果这样写想必有点经验的 c/c++ 程序员都可以轻易认出:
typedef AbstractProduct *(*CreateFn)();
上面的代码为不接受任何参数,返回 AbstractProduct 指针的函数指针定义了一个 CreateFn 别名。

接下来让我们完善一下这个设计,如代码中的红字指出,如果一个工厂类无法识别的 type 出现了,我们改如何处理呢?典型的处理方法有:

  1. 抛出异常
  2. 返回一个空指针
  3. 提供一个默认指针
每一个解决方法在不同的场景下都是一个合适的处理方式,比如如果你需要尽早发现这种意料之外的情况,那么抛出异常是个不错的选择,如果你在开发一个网络服务器,那么显然如果直接抛出异常可能会倒置服务器 down 掉,那这时返回一个空指针,并记录错误日志可能是一个更好的选择。所以我们必须给予客户端指定如何处理的权限。

这里使用的实现模式成为 Policy 模式(详见《moder c++ design》一书),这种模式将程序中一些可以有多种实现方法的地方区分出来,并抽象为一个 Policy,通过模板参数传入,使用类似组装的方法,将多个 Policy 组装成一个实用的 class,最大程度实现代码和组件的复用(其实个人感觉模板编程更像是面对接口编程,只是接口不是明确写出的,而是隐藏在代码中的)。

为了解决上面的问题,我们引入一个 FactoryErrorPolicy,这个 Policy 负责提供一个接口,接口用于处理未知的 type,也就是说其可能是这样子:

template<class AbstractProduct, typename IdentifierType>
class FactoryErrorPolicy {
protected:
  (static) (static) AbstractProduct *OnUnknownType(const IdentifierType &id);
};
注意代码中被括起来的 static 关键字,这并不是什么晦涩的语法,而是我想在此说明,因为我们使用的是模板编程,它是面向语法的,不是面向标记的,不管你是 static 与否,只要可以正常运行就满足我们的接口。这个我会在之后再加以解释,现在我们先彻底完善我们的 Factory 类:
template
<
  class AbstractProduct,
  typename IdentifierType,
  typename ProductCreator = AbstractProduct *(*)(),
  template<class, class> class FactoryErrorPolicy = DefaultFactoryError
>
class Factory
  : public FactoryErrorPolicy<AbstractProduct, IdentifierType> {
public:
  bool Register(const IdentifierType &id, ProductCreator creator) {
    return associations_.insert(typename AssocMap::value_type(id, creator)).second;
  }
  bool Unregister(const IdentifierType &id) {
    return associations_.erase(id) == 1;
  }
  AbstractProduct *CreateObject(const IdentifierType &id) const {
    typename AssocMap::const_iterator i = associations_.find(id);
    if (i == associations_.end()) {
      return this->OnUnknownType(id);
    }
    return (i->second)();
  }
private:
  typedef std::map<IdentifierType, ProductCreator> AssocMap;
  AssocMap associations_;
};
注意新引入的两行代码,这里我们为模板参数增加了一个 Policy 类,并在代码中隐式的要求
FactoryErrorPolicy<AbstractProduct, IdentifierType>
类可以提供一个 OnUnknownType() 函数接口,其可以接受 int 作为参数(它的签名可以是接受 double, char, 任何可以通过int隐式转化的类型)调用就可以了,至于是不是 static 我们也不关心,甚至它的声明可以是这样:
void *OnUnknownType(double d, int i = 0, string="");
这是完全可以的,在模板编程中,我们只需要 this->OnUnknownType() 这句语句可以通过编译就可以了,这是模板编程和面向对象最大的不同点之一,个人认为其灵活性远超面向对象编程。还要注意的是 Policy 模式往往通过定义的模板类 public 继承 Policy 类来实现。

我们甚至还为这个 Policy 提供了一个默认的 class 来减少客户端的编码成本:

template<class AbstractProduct, typename IdentifierType>
class DefaultFactoryError {
protected:
  static AbstractProduct *OnUnknownType(const IdentifierType &id,
                                        int i = 0,
                                        std::map<int, int> *p=NULL) {
    return NULL;
  }
};
为了佐证我上面的论点,我特意选择了一个很奇怪的实现,至此我们完整实现了一个泛型工厂类,上一篇博客中可以很容易的复用这个类,只需要用:
typedef Factory<Shape, int> ShapeFactory;
替代之前的 ShapeFactory 实现,并且适当修改 Factory 类的调用处(因为接口名字有所改变,比如 CreateShape 改为 CreateObject),就可以直接使用现成的 Factory 类。

一个通用、客户端可以轻易重新定义出现 UnKnown Type 时的行为的泛型工厂类到此就完全实现了。再次感慨一下 c++ 模板的强大。

当然,上面的泛型类任然有其缺陷,比如在构造的时候无法传入参数,这对于很多没有默认构造函数的对象来说是无法接受的,一个可能的改进方法是在 CreateObject 函数添加一个 void * 参数。具体对参数的使用逻辑重新落在每个 Creator 中。

    AbstractPtr CreateObject(const Identifier &id, void *arg) const {
        typename Creators::const_iterator it = creators_.find(id);
        if (it == creators_.end()) {
            return this->OnUnknownType(id);
        }
        return (it->second)(arg);
    }
当然这样会导致很多不需要参数的类在注册 Creator 的时候必须使用一个可用一个 void *做参数的 Creator,增加了一点程序员的负担。笔者暂时无法想到更好的解决方法,如果有同学可以指教的话欢迎在下面留言。

鉴于自身水平有限,文章中有难免有些错误,欢迎大家指出。也希望大家可以积极留言,与笔者一起讨论编程的那些事。


朱茂华
8 声望1 粉丝