软件开发>杂文>3

共享软件开发的点滴心得

[中文][English][日本語

 [机器人制作] [软件开发] [豆知识] [Squeak] [关于]

 Factory

二月春风

刘新宇

2006年二月

 

在汉文化区域中,我们大概是唯一还用农历的国家,在农历的二月,已经是“早春”了,唐代贺知章有“咏柳”一诗:

 

碧玉妆成一树高,万条垂下绿丝绦,

不知细叶谁裁出,二月春风似剪刀。

 

这拟人化的“二月春风”,不仅裁得了婀娜的柳树,还巧夺天工地裁得了花草万物,“春风又绿江南岸”——简直是大自然最伟大的造物主了。

 

走进C++开发的世界,就发现“制造”是多么的困难,相比大自然的造物主,人类的智慧是多么的渺小和苍白。然而这并不妨碍执着者不懈的努力。

 

春天,万物复苏,莺飞草长,细雨润物,一个最初的嫩芽诞生了:

 

#include <iostream>

using namespace std;

void grow(){

     cout<<"root\n";

     cout<<"|----leaf\n";

}

int main(int argc, char* argv[]){

     cout<<"hello world!\n";

     grow();

}

 

这个生命首先向世界问好,然后先扎下根,生长出一个叶子。

 

好雨知时节,当春乃发生。

随风潜入夜,润物细无声。

 

在杜甫笔下的“春夜喜雨”中,这个生命需要继续生长,于是开始重构grow,可是,重构的速度很慢,大概刚刚重构了一半,可是又不能让这个生命休克(不能run)时间过长了,所以折衷的办法是:

 

#include <iostream>

#include <vector>

using namespace std;

class Node{

public:

     virtual void grow() = 0;

     virtual ~Node(){}

};

class Branch: public Node{

public:

     Branch(){}

     void grow(){

          //To do: grow here...

     }

private:

     vector<Node*> SubBranch_;

};

void grow2(){

     Node* p=new Branch();

     p->grow();

delete p;

}

void grow(){

     cout<<"root\n";

     cout<<"|----leaf\n";

}

int main(int argc, char* argv[]){

     cout<<"hello world!\n";

     grow();

}

 

没有办法,由于还没有时间来得及写好Branchgrow,所以只好临时写一个名字叫grow2的函数,测试重构的时候,把main里的grow改成grow2,提交的时候,再改回来。

 

[大概有读者会认同:“没错,我这么干过,明天就要demo,以前的代码一点都不敢动。”]

 

所以,经常在程序里看到Function(), Function1(), Function2()这样的东西出现。为了防止“乱花渐欲迷人眼”的局面,可以把“多态”引入重构。于是代码变成这样:

 

//前面略去

class DemoPlant: public Node{

public:

     void grow(){

          cout<<"root\n";

          cout<<"|----leaf\n";

     }

};

class PlantFactory{

public:

     static Node* Create(const string& name){

          if(name=="Demo")

              return new DemoPlant();

          if(name=="Branch")

              return new Branch();

          throw runtime_error("unknown type");

     }

};

int main(int argc, char* argv[]){

     cout<<"hello world!\n";

     Node* p=PlantFactory::Create(argv[1]);

     p->grow();

     delete p;

}

 

这里用了一个“工厂”[1],这样当调试重构的部分时,只要在命令行输入:

C\:>spring.exe Branch

就可以。而需要demo时,就直接输入:
C\:>spring.exe Demo

进一步的建议还包括,把各个类放到单独的模块(cpph文件)中等等,为了节省篇幅,一概略去。

 

下面可以在“沾衣欲湿杏花雨,吹面不寒杨柳风”中安心继续重构了。首先的设想是建立一个植物——例如一颗树——的基本结构:树由树枝和树叶组成,每一个树枝可以包含若干“子树枝”和叶子。大概可以这样描述:

 

class Branch: public Node{

public:

     //略……

private:

     vector<Node*> SubBranch_;

};

class Leaf: public Node{

public:

     //略……

};

 

并且将来可以在Node的基础上继续扩展出“花”,“果实”等等内容。基本的思路是这样,首先获得一个基本的branch作为树干,然后让它生长(grow),这个branch在自己身上生产出几种产品,当然他们是大自然随机选择产生的,可以是:

  • 新的子branch——节外生枝
  • 树叶
  • 果实

 

产生了若干产品后,树枝就把它们作为自己的“子Node”安放到自己身上,然后它遍历这些子Node,让他们依次grow,这个思路可以这样实现:

 

void Branch::grow(int depth){

     if(depth){

          int n=rand()%5;         //假设一个Branch上最多有5个子Node;

          while(n--){

               SubBranch_.push_back(NodeFactory::Create());

          }

          for(vector<Node*>::iterator it=SubBranch_.begin();

               it!=SubBranch_.end(); ++it)

               (*it)->grow(depth-1);

     }

}

这里有一些变化,已经用黑体字标出来了。Grow增加了一个参数depth,这个参数用来表示节点深度的信息,叶子节点,果实节点和花节点等“终端节点”的深度为0,每增加一级父节点,depth就相应增加1depth的设置,主要是考虑为了防止无限递归下去最终导致堆栈溢出。

 

另外一处尚未清晰的地方是NodeFactory::Create(),这里从字面上看,应该为一“对象工厂(Object Factory)”,但是又和对象工厂的通常实现感觉不大相同。前面的PlantFactory是一个典型的对象工厂实现,工厂根据传入的字符产参数,决定返回Demo的实现,还是正在重构的实现。而这个NodeFactory,目前的状态下应该返回哪种concrete class的对象呢?子节点?叶子?花?果实?

 

备选的有两种思路:

1、NodeFactory::Create内部随机产生各Concrete class object,也就是随机长出子节点,叶子,花,果实等对象

2、制造出一组“DNA, 然后根据DNA序列,决定应该长出子节点,叶子,花,果实

 

第一种方案的好处是简单易行,立刻就可以写出来:

class NodeFactory{

public:

     static Node* Create(){

          int type=rand()%4;

          switch(type){

              case 0:

                   return new Branch();

              case 1:

                   return new Leaf();

              case 2:

                   return new Fruit();

              case 4:

                   return new Flower();

              default:

                   throw runtime_error("unknown type");

          }

     }

};

 

现在评估第一种方案的缺点时,不妨把所有前面描述的Object-Factory一起讨论,这类方案可以通称为“参数化的对象工厂parameterized object factory”,特点是根据某一运行时特定的参数(如name type),决定产生何种对象。带来的问题有如下这些:

  • 增加新类的困难:增加一个新的类,例如在枝干上增生出一个mushroom,那么要做的工作包括:增加一条case,改变类型参数的定义范围,因为switch的参数很可能是enum之类的。同样,减少一个类(禁用,例如有些植物不开花)也是会有困难,并且由此带来的错误不易发现。
  • 类型安全的缺乏,产生的对象类型和传递进来的参数,没有紧密的联系,这种关系的验证必须要拖到运行时。比如前面的PlantFactory工厂,使用字符串参数,如果用户由于输入把“Demo” 错误拼写为“demo”或者“Deamon”,那么这个用户得不到任何编译期的警告,而会在运行期得到一个runtime error

 

对比第二条缺点,Java似乎做得差强人意,可以利用反射的机制,进行实践:

public class NodeFactory {

     public static Node create(Class aClass){

          try{

              Node node=(Node)aClass.newInstance();

              return node;

          }

          catch(Exception e){

              System.out.println("unknow class");

          }

          return null;

     }

     public static void main(String[] args) {

          Node node=NodeFactory.create(Leaf.class);

     }

}

这样,只要用户在调用create时,传入了正确的子类,就不会保证抛出运行时异常。

 

那么C++有没有办法做到呢?至少做到真正的编译时类型安全检查?有,那就是利用模板偏特化[2]来实做出安全类型工厂:

template<typename T>

class SafeNodeFactory{

};

template <>

class SafeNodeFactory<Branch>{

public:

     static Node* Create(){return new Branch;}

};

template <>

class SafeNodeFactory<Leaf>{

public:

     static Node* Create(){return new Leaf;}

};

调用工厂时,只需要如下的代码:

Node* node=SafeNodeFactory<Leaf>::Create();

//do other things node->grow() etc.

delete node;

 

这个工厂首先解决了参数和类型运行时弱绑定的问题,因为传入的参数就是类型本身,其次,这个工厂是编译期安全的,如果用户传入了任何错误的参数,编译器会在编译期给出错误。最后,这个工厂的扩展性非常好,如果要增加一个新类,例如Mushroom,只需要特化出一个Mushroom的版本;如果需要禁用一个类,只需要注释该类的特化版本种的Create成员即可。

 

现在这个静态的类型安全工厂,有没有缺点呢?有,那就是不能利用思路1,也就是不能随机产生对象,而制作出千奇百怪的植物模型。但是它也许能实现思路2,就是利用一组DNA作为模板,来生成特定模式的植物。

 

DNA可以理解为一组生长规则,植物体的每个单元,会按照DNA序列指定的规则生长,例如DNA序列为:

叶子,树枝,叶子”的规则,将会得到这样一株植物:

……

叶子              树枝              叶子

<-----------------|----------------->

|

叶子              树枝              叶子

<-----------------|----------------->

 

DNA序列为:

“叶子,树枝,花,树枝,叶子”的规则将会生成下面的植物:

……        ……                    ……        ……

|           |                       |           |

叶子  树枝  花朵  树枝  叶子        叶子  树枝  花朵  树枝  叶子

|                                   |

-------------           -------------

|           |

叶子  树枝  花朵  树枝  叶子

|

 

如果将每个生长单元中某一元素(比如,树枝,花朵,叶子等)定义为一个基因Genome,那么必须把这些Genome连接成串,才能构成DNA序列。根据这个思路,Genome实做如下[2]

template<class T, class U>

struct Genome{

     typedef T Current;

     typedef U Next;

};

每个Genome中包含当前的基因Current和下一个基因Next,然后就可以把基因连接成串了,例如“叶子,树枝,叶子”型的植物,可以这样实现:

 

class NullType{};

typedef Genome<Leaf,

Genome<Branch,

Genome<Leaf, NullType> > > SeedGenome;

 

NullType提供了DNA串的结束标志,最后我们把基因组装起来,就成了指挥整个生命体成长的DNA

 

template<class Genome> struct DNA;

template<>

struct DNA<NullType>{

     DNA(vector<Node*>& list){};

};

template<class T, class U>

struct DNA<Genome<T, U> >{

     DNA(vector<Node*>& list){

          list.push_back(SafeNodeFactory<T>::Create());

          DNA<U> next(list);

     }

};

 

这个DNA接受一个遗传基因串作为生长规则,它将每个串上的环节,一个一个拆开,生成对应的花,树枝,或者果实,直到发现这个串结束(遇到NullType

 

现在可以看一看在DNA这只看不到的手的指挥下,植物体单元是如何生长的了:

void Branch::grow(int depth){

     if(depth){

          DNA<SeedGenome> dna(SubBranch_);

          for(vector<Node*>::iterator it=SubBranch_.begin();

              it!=SubBranch_.end(); ++it){

              (*it)->grow(depth-1);

          }

     }

}

 

当然,最后还要释放资源:

Branch::~Branch(){

     for(vector<Node*>::iterator it=SubBranch_.begin();

 it!=SubBranch_.end(); ++it)

          delete (*it);     

}

的确,这个方法有些让人难以置信,它真的工作么?为了验证一下,可以把这颗植物打印出来。首先给Node及其所有派生类,叶子,树枝,花,果实等等添加一个显示功能:

class Node{

public:

     //略去其他

     virtual void display()=0;

};

 

void Branch::display(string& prefix){

     cout<<prefix<<"branch\n";

     for(vector<Node*>::iterator it=SubBranch_.begin();

it!=SubBranch_.end(); ++it){

          (*it)->display(prefix+"  |--");

     }

}

void Leaf::display(string& prefix){ cout<<prefix<<"leaf\n";}

 

此后,就可以输出来看看效果了:

Node* p=PlantFactory::Create("Branch");    //可以换成argv[1]

p->grow(3);

p->display(string(""));

delete p;

程序输出:

branch

  |--leaf

  |--branch

  |--  |--leaf

  |--  |--branch

  |--  |--  |--leaf

  |--  |--  |--branch

  |--  |--  |--leaf

  |--  |--leaf

  |--leaf

 

的确是用“叶子,树枝,叶子”生成规则长出来的一颗树!

 

本文到此,似乎可以结束了,进入:太史公曰,观诸般工厂方法,各有不足,独DNA泛型工厂,集各家之大成……云云之类。但是“峰回路转,柳暗花明”,现在又有一个新问题提出了:明年怎么办?

 

“青山隐隐水迢迢,秋近江南草木凋”,秋末的时候,这些植物用save方法,把自己保存起来,明年春天的时候,却发现没有办法再次生长(load)出来了!因为C++无论如何提供不了这样的办法:

NodeType type=read(file);

Node* node=SafeNodeFactory<type>::Create();

 

看来一切还要推倒重来。

 

问题是由save-load引起的,save非常容易实现,Node定义一个virtualsave(),然后各个子类分辨实现各自的save():树叶,花,果实等等,仅仅存储各自的内容;树枝则遍历自己的子节点,分别调用各子节点的save,基本上就是一个composite模式和多态的混合应用。比如下面的例子,是一个存储后文件的内容,右侧是为了理解方便添加的注释:

 

4, black,         //一个黑色的树枝,4个子节点

  1, green        //一个绿色的叶子

  1, red          //一个红色的果实

  1, green        //一个绿色的叶子

  2, black        //一个黑色的树枝,2个子节点

    1, red        //一个红色的花朵

    1, yellow     //一个黄色的花朵

 

相对于简单的saveload却非常难以实现,从文件系统读出一组数据后,这组数据究竟应该放入到那个类型的对象中?树叶?果实?还是树枝?解决方案是在存储的时候,把对象所属的类型信息也存进去,这样load的时候,就可以根据类型信息,决定应该创建什么类的对象,并将后继数据放入对象。

 

进一步的问题是,类型信息以什么表示?整数?枚举(enum)?字符串?还是128UUID?如果采用整形或者枚举,那么缺点就又回到了前面下大力气解决的“型别信息和型别的弱绑定”问题。并且扩展一个类,或者禁用一个类都不方便。C++ RTTI本身提供了std::type_info,但是被证明也是存在一定问题的(具体论述可以参见[2]8.4节),究竟有没有办法实作出一个强绑定的类型信息呢?

 

这个问题,仁者见仁,智者见智,不如留给使用者决策。也许环境简单,利用一个字符串就可以;也许集成体系相对稳定,可以利用枚举;或者有强劲的工具,可以产生128位的独特整数;也不排除拥有强大的编译器,可以提供稳定的type_info。为了支持所有这些情况,工厂必须进一步抽象,下面这个实现是Andrei[2]中给出的一种:

 

template

     typename BaseType,

     typename TypeInfo,

     typename Creator

class GenericFactory{

public:

     static GenericFactory& Instance(){

          static GenericFactory inst;

          return inst;

     }

     bool RegisterCreator(const TypeInfo& id, Creator func){

          return creators_.insert(make_pair(id, func)).second;

     }

     BaseType* Create(const TypeInfo& id){

          if(creators_.find(id)==creators_.end())

              throw std::runtime_error("unknow type");

          return creators_[id]();

     }

private:

     std::map<TypeInfo, Creator> creators_;

};

 

使用方法可以由用户定制,例如:

enum NodeType{

     BranchType,

     LeafType,

     FruitType,

     FlowerType

};

typedef Node* (*CreateFunc)();

之后,在Branch自己的模块内部,用一个匿名namespace封装一下全局函数:

namespace{

     Node* CreateBranch(){

          return SafeNodeFactory<Branch>::Create();

     }

     const bool res=GenericFactory<Node, NodeType, CreateFunc>

::Instance().RegisterCreator(BranchType, CreateBranch);

}

然后使用时:

Node* ptr=GenericFactory<Node, NodeType, CreateFunc>

::Instance().Create(BranchType);

//do something with ptr

delete ptr;

由于存在一个类型信息和型别的映射,因此可以在save时,将类型信息数据存如文件,而在将来load时再次读取出来,读取到类型信息后,利用上述的工厂生成对应型别的对象,最后再将具体的数据放入对象内。

 

至此,终于可以“太史公曰”了。很遗憾,虽然大自然的“二月春风”催生万物,本文却始终没有找出万能的工厂——“no silver bullet”。归纳下来,一共有3个工厂:

  1. 传统简单工厂:利用switch辨别类型参数,生产不同的对象;
  2. 类型安全工厂:利用模板参数确定型别,生产不同的对象,如果搭配本文的DNA-Genome手法,可以实现批量生产;
  3. 进一步抽象的泛型工厂:由用户自由定制的工厂外壳。

 

反观这些看似灵巧,却在大自然的比较下黯然失色的作品,不仅想起王安石那句:“春风又绿江南岸,明月何时照我还”了。

 

参考书目:

[1] Eric Gamma, etc. Design pattern: Elements of Reusable Object-Oriented Software. Addison-Wesley.

[2] Andrei Alexandrescu, Modern C++ Design: Generic Programming and Design Pattern Applied. Addison Wesley, Feb. 2001


[English][上一篇][下一篇][目录][主页][联系我们: liuxinyu95@gmail.com]