对象工厂设计模式
2011-03-08 10:43
197 查看
如果你在你的某个系统中增加了一个子类,你要创建这个子类的对象,但又不想改变任何原有代码,有可能么?
答案是肯定的,用“对象工厂”设计模式。
对象工厂(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 * 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++(从VC6到VC9)的/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,包括:Distance,Angle等),还有下达各种图像操作的命令(Command,比如:Zoom,Pan,Rotate等)。
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 });
答案是肯定的,用“对象工厂”设计模式。
对象工厂(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 * 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++(从VC6到VC9)的/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,包括:Distance,Angle等),还有下达各种图像操作的命令(Command,比如:Zoom,Pan,Rotate等)。
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 });
相关文章推荐
- 一步步学习javascript基础篇(4):面向对象设计之创建对象(工厂、原型和构造函数等模式)
- C#面向对象设计模式纵横谈 学习笔记3 Abstract Factory 抽象工厂
- 设计模式深入浅出(一)对象创建——工厂方法,抽象工厂
- 工厂设计模式的改进 通过反射中Class类下的newInstance()对象
- C#面向对象设计模式纵横谈(四) --- Factory Method 工厂方法(创建型模式)
- 类对象工厂设计模式(Factory Pattern)
- 远程对象工厂设计模式
- 面向对象设计模式之---简易工厂模式(Simple Factory Pattern)
- 远程对象工厂设计模式
- 设计模式(三)、FACTORY METHOD(工厂方法)---对象创建型模式
- 设计模式【3】:抽象工厂【创建对象】
- Symbian常用设计模式之可伸缩对象工厂
- 设计模式:对象生成(单例、工厂、抽象工厂)
- 【java设计模式】单例设计模式案例:工厂加工零件(对象是工厂,所以就需要创建一个工厂的类)
- 【设计模式】工厂模式——创建对象的最佳方式
- 设计模式之对象工厂(泛型实现)
- 可复用面向对象软件基础——设计模式(二)之工厂方法模式
- Javascript面向对象设计一 工厂模式
- 设计模式【2】:工厂方法【创建对象】