对象工厂设计模式

   

如果你在你的某个系统中增加了一个子类,你要创建这个子类的对象,但又不想改变任何原有代码,有可能么?

 

答案是肯定的,用“对象工厂”设计模式。

 

对象工厂(Object Factory)是GoF 23种设计模式之外的模式,它既不是抽象工厂(Abstract Factory),也不是工厂方法(Factory Method),尽管可能跟它们有些渊源。我第一次看到介绍“对象工厂”的书是《C++设计新思维(Modern C++ Design)》,但我第一次看到对象工厂的代码,却比看到这书早,但我当时不知道它叫“对象工厂”。

 

C++设计新思维》(下载地址:http://d.download.csdn.net/down/2627586/pcevil/)第8章详细讲解了我们为什么会需要对象工厂,如何实现并泛化它等内容。本文并不想重复这些内容,而是想通过一个小例子,将使用对象工厂和不使用对象工厂的情况进行对比,来说明对象工厂会带来哪些好处。

 

         还是面向对象教科书上那个经典的Shape的例子。基于多态,你用C++编写了一套关于形状的系统,Shape是基类。可能你已经有了LineRectangle等子类。在你的客户程序里,通过传入形状的类型标识(假设我们用字符串来标识类型,当然用整型来标识也可以)来创建具体的(ConcreteShape。你的代码看起来可能是这样。

 

Shape * CreateShapeById(const std::string& strShapeId)
{
    Shape 
* pShape = NULL;

    
if (strShapeId == "Line")
    {
        pShape 
= new Line;
    }
    
else if (strShapeId == "Rectangle")
    {
        pShape 
= new Rectange;
    }

    
return pShape;
}

 

    这像是GoF《设计模式》里所说的参数化工厂方法。但这里违反了面向对象的最重要的规则:

 

1.       它基于型别标记执行了if-else语句(当用整型标识而换为switch语句时同理),这正是面向对象程序竭力消除的东西。

2.       它在一个源码文件中收集所有关于Shape子类的相关信息,这也是我们应该竭力避免的。客户代码文件都因此必须包含其头文件,造成编译依存性和维护上的瓶颈。

3.       它难以扩充。现在你需要增加Ellipse子类,如果没有使用对象工厂模式,除了增加Ellipse本身的代码,你至少还要增加以下代码:

 

a)       在你的客户代码文件里,增加

 

#include "Ellipse.h"

 

b)       CreateShapeById中加入以下代码:

 

    else if (strShapeId == "Ellipse")
    {
        pShape 
= new Ellipse;
    }

  

c)       如果你用整型定义类型标识,你还要定义Ellipse形状的类型标识。比如:

 

#define ELLIPSE 3

 

         现在让我们来改改,用对象工厂来实现。泛化的对象工厂的代码如下:

 

template
<
    
class AbstractProduct,
    
class IdentifierType,
    typename ProductCreator 
= AbstractProduct* (*)()
>
class Factory
{
private:
    Factory() {};
    Factory(Factory
& factory);
    Factory
& operator=(const Factory& factory);

public:
    
bool Register(const IdentifierType& id , ProductCreator creator)
    {
        associations_[id] 
= creator;
        
return true;
    }

    
bool Unregister(const IdentifierType& id)
    {
        
return associations_.erase(id) == 1;
    }
  
    AbstractProduct 
* CreateObject(const IdentifierType& id)
    {
        AssocMap::const_iterator i 
= associations_.find(id);
        
if (i != associations_.end())
        {
            
return (i->second)();
        }
        
return NULL;
    }

    
static Factory* Instance()
    {
        
static Factory * pFactory = NULL;
        
if (!pFactory)
        {
            
static Factory factory;
            pFactory 
= &factory;
        }
        
return pFactory;
    }

private:
    typedef std::map
<IdentifierType, ProductCreator> AssocMap;
    AssocMap associations_;
};

 

简单说说其工作机理。更详细的、深入的内容还是请看《C++设计新思维》。

 

1.       此对象工厂泛化了3样东西:

a)       抽象产品(Abstract product)。对应本例,就是Shape

b)       产品类型标识符(Product type identifier)。对应本例,我们用字符串标识,就是std::string

c)       产品生产者(Product creator)。对应本例,我们将用缺省的(也是最简单的)原型,也就是无参数、返回值为抽象产品指针的函数。

2.       它使用std::map作为产品类型标识符与产品生产者的映射的存储结构。

3.       Register负责向map中注册一个产品类型标识符与产品生产者的映射,Unregister则负责注销。

4.       CreateObject是对象工厂的核心,它会根据传入的产品类型标识符,找到对应的产品生产者,并调用它,创建出具体产品(Concrete Product)。

5.       Instance是实现了对象工厂的单件模式。这里用的是“Meyers Singleton”的一个变种。当然这里不是讨论Singleton的地方。

 

有了对象工厂,我们再在Shape.h里定义一个用来注册Shape具体类的模板类,这里有真正的形状的生产者(Create函数)。代码如下:

 

template <class DerivedShape> class RegisterShapeClass
{
public:
  
static Shape * Create()
  {
    
return new DerivedShape;
  }
  RegisterShapeClass(
const std::string& strShapeId)
  {
    Factory
<Shape, std::string>::Instance()->Register(strShapeId, RegisterShapeClass::Create);
  }
};

 

再定义一个将类名转换为字符串的宏:

 

#define ClassNameToString(x) #x

 

好了,有了对象工厂,CreateShapeById就变成这样:

 

Shape * CreateShapeById(const std::string& strShapeId)
{
    
return Factory<Shape, std::string>::Instance()->CreateObject(strShapeId);
}

 

首先,这个函数短多了,而且不会随着子类的增加而膨胀,但这不是关键。这里面没有对具体Shape类型的引用。当我们需要增加Ellipse子类,只需在Ellipse类自己的代码里加上下面这句(向工厂注册自己),而不需要改变任何原有代码!

 

RegisterShapeClass<Ellipse> RegisterEllipse(ClassNameToString(Ellipse));

 

这看起来有些奇异,但更奇异的是,不仅从原有代码中我们看不到任何引用新子类的代码,而且连Linker都会认为新子类没有被引用,而将新子类的obj排除在Link之外。当然,你也许会认为Link的时候使用/OPT:NOREF选项可以避免这个问题。但现实是, Visual C++(从VC6VC9)的/OPT:NOREF选项都有一个问题(参见http://social.msdn.microsoft.com/forums/en-US/vclanguage/thread/2aa2e1b7-6677-4986-99cc-62f463c94ef3):即使用此选项,仍然不能将新子类的obj文件Link进去。解决的办法也是在这个网址里看到的,加入类似下面这样一句,以使Linker强行将RegisterShapeClass<Ellipse>连接进去

 

#pragma comment(linker, "/include:??0?$RegisterShapeClass@VEllipe@@@@QAE@ABV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z")

 

在我曾开发过的医学图像处理系统中,需要用到对象工厂的地方,至少有3种:

 

1.       由用户输入类型,系统动态生成对应的对象实体。比如:用户选择不同的测量工具(Measurement,包括:DistanceAngle等),还有下达各种图像操作的命令(Command,比如:ZoomPanRotate等)。

2.       序列化。比如上条所说的Measurement,我们能够保存下来,并能够在某个时刻恢复(Restore)。保存时,用Measurement的名称来标识测量工具的类型并序列化,恢复时,根据这个类型标识动态生成对象,并反序列化。

3.       同步。我们称其为会议模式(Conference Mode),比如一个客户端上画出的Measurement,其它客户端上能同步看到,我们使用XML并进行流(std::stringstream)的输入和输出,来传输和同步数据和状态。当数据在其它客户端流入的时候,与反序列化相似,根据类型标识动态生成对象。

 

对象工厂使得客户不再需要(或较少)改变原有系统,但却很容易扩展系统。所以说:对象工厂是对OO开闭原则(OCP,对变更关闭,对扩展开放)非常好的阐释。

 

这里再多说一句:由于.Net的反射(Reflection)机制,使我们不用自己再去建造对象工厂就可以动态地生成对象。用C#写出来的代码应该类似于这样:

 

string strShapeId = "Ellipse";
Type type 
= Type.GetType(strShapeId);
Shape shape 
= (Shape)Activator.CreateInstance(type, new Object[] { this });

 

posted @ 2011-03-08 10:43  wanghui  阅读(7995)  评论(9编辑  收藏  举报