对象工厂设计模式
如果你在你的某个系统中增加了一个子类,你要创建这个子类的对象,但又不想改变任何原有代码,有可能么?
答案是肯定的,用“对象工厂”设计模式。
对象工厂(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是基类。可能你已经有了Line、Rectangle等子类。在你的客户程序里,通过传入形状的类型标识(假设我们用字符串来标识类型,当然用整型来标识也可以)来创建具体的(Concrete)Shape。你的代码看起来可能是这样。
{
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) 在你的客户代码文件里,增加
b) 在CreateShapeById中加入以下代码:
{
pShape = new Ellipse;
}
c) 如果你用整型定义类型标识,你还要定义Ellipse形状的类型标识。比如:
现在让我们来改改,用对象工厂来实现。泛化的对象工厂的代码如下:
<
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函数)。代码如下:
{
public:
static Shape * Create()
{
return new DerivedShape;
}
RegisterShapeClass(const std::string& strShapeId)
{
Factory<Shape, std::string>::Instance()->Register(strShapeId, RegisterShapeClass::Create);
}
};
再定义一个将类名转换为字符串的宏:
好了,有了对象工厂,CreateShapeById就变成这样:
{
return Factory<Shape, std::string>::Instance()->CreateObject(strShapeId);
}
首先,这个函数短多了,而且不会随着子类的增加而膨胀,但这不是关键。这里面没有对具体Shape类型的引用。当我们需要增加Ellipse子类,只需在Ellipse类自己的代码里加上下面这句(向工厂注册自己),而不需要改变任何原有代码!
这看起来有些奇异,但更奇异的是,不仅从原有代码中我们看不到任何引用新子类的代码,而且连Linker都会认为新子类没有被引用,而将新子类的obj排除在Link之外。当然,你也许会认为Link的时候使用/OPT:NOREF选项可以避免这个问题。但现实是, Visual C++(从VC6到VC9)的/OPT:NOREF选项都有一个问题(参见http://social.msdn.microsoft.com/forums/en-US/vclanguage/thread/2aa2e1b7-6677-4986-99cc-62f463c94ef3):即使用此选项,仍然不能将新子类的obj文件Link进去。解决的办法也是在这个网址里看到的,加入类似下面这样一句,以使Linker强行将RegisterShapeClass<Ellipse>连接进去:
在我曾开发过的医学图像处理系统中,需要用到对象工厂的地方,至少有3种:
1. 由用户输入类型,系统动态生成对应的对象实体。比如:用户选择不同的测量工具(Measurement,包括:Distance,Angle等),还有下达各种图像操作的命令(Command,比如:Zoom,Pan,Rotate等)。
2. 序列化。比如上条所说的Measurement,我们能够保存下来,并能够在某个时刻恢复(Restore)。保存时,用Measurement的名称来标识测量工具的类型并序列化,恢复时,根据这个类型标识动态生成对象,并反序列化。
3. 同步。我们称其为会议模式(Conference Mode),比如一个客户端上画出的Measurement,其它客户端上能同步看到,我们使用XML并进行流(std::stringstream)的输入和输出,来传输和同步数据和状态。当数据在其它客户端流入的时候,与反序列化相似,根据类型标识动态生成对象。
对象工厂使得客户不再需要(或较少)改变原有系统,但却很容易扩展系统。所以说:对象工厂是对OO开闭原则(OCP,对变更关闭,对扩展开放)非常好的阐释。
这里再多说一句:由于.Net的反射(Reflection)机制,使我们不用自己再去建造对象工厂就可以动态地生成对象。用C#写出来的代码应该类似于这样:
Type type = Type.GetType(strShapeId);
Shape shape = (Shape)Activator.CreateInstance(type, new Object[] { this });