文章目录
第二章是以一个可视文本编辑为例,讲解设计模式使用,笔者跳过,可以自行翻看原文。
作为替换,此处记录面向对象设计原则。
二、面向对象设计原则(补充)
面向对象设计原则目的:抵御变化。
2.1 重新认识面向对象
-
理解隔离变化:宏观层面,面向对象的构建方式能够更适应软件的变化
-
各司其职:从微观层面来看,面向对象的方式更强调各个各类的“责任”。即,需求的变化导致的新增类型,不应该影响原来的实现。
-
对象是什么?
- 从语言实现层面上来看,对象封装了代码和数据。
- 从规格层面将,对象是一系列可被使用的公共接口
- 从概念层面将,对象是某种拥有责任的抽象
2.2 面向对象设计原则
2.2.1 依赖倒置原则(DIP)
- 高层模块(稳定)不应该依赖底层模块(变化),二者应该依赖与抽象(稳定)。
- 抽象(稳定)不应该依赖实现细节(变化),实现细节应该依赖于抽象(稳定)。
我们将
class MainForm:public form{
private:
Point p1;
Point p2;
vector<Rect> lineVector;
vector<Line> rectVector;
public:
MainForm(){
// ...
}
};
改为
class MainForm:public form{
private:
Point p1;
Point p2;
vector<Shape> lineVector;
vector<Shape> rectVector;
public:
MainForm(const vector<Shape>& shape1,const vector<Shape>& shape2){
lineVector = shape1;
rectVector = shape2;
// ...
}
};
Shape此时是一个抽象。Rect、Line实现它。如果我们以后要变化Rect,Line为其他的,只要是基于Shape就没有问题。
2.2.2 开放封闭原则(OCP)
- 对扩展开放,对更改封闭
- 类模块应该是可扩展,但是不可修改
针对于需求变更时,不应该去修改,而是去增加(增强)。
也正是如此,我们通常也要预留增强的方式。比如许多框架都预留的的BeforeXX与AfterXXX。无论是抽象方法,还是拦截器,过滤器,代理等方式。
我们通过它们,让我们改动更小,对应的测试范围也更小。
2.2.3 单一职责原则(SRP)
- 一个类应该仅有一个引起它变化的原因
- 变化的方向隐含者类的责任
2.2.4 Liskov替换原则(LSP)
- 子类必须能够替换它们的基类(IS-A)。
- 继承类型表达类型抽象
所有需要父类的地方,子类都可以传过去并且使用。
2.2.5 接口隔离原则(ISP)
- 不应该强迫客户程序依赖它们不用的方法
- 接口应该小而完备
避免用户和我们的接口产生依赖
2.2.6 优先使用对象组合,而不是类继承
- 累积成通常为“白箱复用“,对象组合通常为”黑箱复用“(见上一篇笔记)
- 继承在某种程度上破坏了封装性,子类父类耦合度高。
- 而对象组合则只要求被组合对象具有良好定义的接口,耦合程度低。
2.2.7 封装变化点
- 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良影响,从实现层次间的松耦合,
2.2.8 针对接口编程,而不是针对实现编程
- 不将变量类型声明为某个特定的具体类
- 客户程序无需获知对象具体的类,只需要知道对象所具有的接口
- 减少系统中各部分的依赖关系,从而实现”高内聚,松耦合“的类型设计方案。
也即是调用时用抽象类的方法执行,不考虑到底示例是什么。
2.3 小结
由上面的设计原则,可以总结为如下三类设计经验
-
设计习语 Design Idioms:描述与特定编程语言相关的底层模式、技巧、习惯用法
-
设计模式 Design Patterns:类与相互通信的对象之间的组织关系。包括它们的角色、职责、协作方式等方面
-
架构模式 Architectural Patterns:描述系统中与基本结构组织关系密切的高层模式,包括子系统划分、职责、以及如何组织它们之间关系的规则。
三、创建型模式
创建型模式抽象了实例化过程。
它们帮助一个系统独立于如何创建、组合和表示它的那些对象。
一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。
包含有
针对类:
- Abstract Factory
针对对象:
- Builder
- Factory Method
- Prototype
- Singleton
目的就是:
- 具体使用哪些具体的类信息封装起来
- 隐藏了类的实例是如何被创建和放在一起的
绕开new来避免对象创建过程中的紧耦合。
是接口抽象之后的第一步工作。
例子
书中举了一个迷宫作为例子。帮助我们理解这几种创建型模式的区别。
我们忽略许多迷宫中的细节以及一个迷宫游戏中有一个还是多个游戏者。我们仅关注迷宫是怎样被创建的。
我们将一个迷宫定义为一系列房间,一个房间知道它的邻居;可能的邻居要么是另一个房间,要么是一堵墙、或者是到另一个房间的一扇门。
如下
类MapSite是所有迷宫组件的公共抽象类。
为简化例子,MapSite仅定义了一个操作Enter,它的含义决定于你在进入什么。如果你进入一个房间,那么你的位置会发生改变。如果你试图进人一扇门,那么这两件事中就有一件会发生:
- 如果门是开着的,你进人另一个房间
- 如果门是关着的就会碰壁。
每一个房间有四面,我们使用C++中的枚举类型Direction来指定房间的东南西北:
enum Direction {North,South, East, west} ;
Enter为更加复杂的游戏操作提供了一个简单基础。
class Mapsite {public:
virtual void Enter () = 0;
};
例如,如果你在一个房间中说“向东走”,游戏只能确定直接在东边的是哪一个MapSite并对它调用Enter。特定子类的Enter操作将计算出你的位置是发生改变,还是你会碰壁。在一个真正的游戏中,Enter可以将移动的游戏者对象作为一个参数。
Room是MapSite的一个具体的子类,而MapSite定义了迷宫中构件之间的主要关系。Room有指向其他MapSite对象的引用,并保存一个房间号,这个数字用来标识迷宫中的房间。
class Room : public Mapsite {
public:
Room (int roomNo) ;
Mapsite* Getside (Direction) const ;
void setside (Direction,Mapsite*) ;
virtual void Enter ( );
private:
Mapsite*_sides [4];
int _roomNunber;
} ;
下面的类描述了一个房间的每一面所出现的墙壁或门。
class wall : public Mapsite {
public:
wall();
virtual void Enter ( ) ;
};
class Door : public Mapsite {
public:
Door (Room* = 0,Room* - 0);
virtual void Enter ( ) ;
Room*OthersideFrom (Room*);
private:
Room_room1 ;
Room*.room2 ;
bool _isopen ;
};
我们不仅需要知道迷宫的各部分,还要定义一个用来表示房间集合的Maze类。用RoomNo操作和给定的房间号,Maze就可以找到一个特定的房间。
class Maze {
public:
Maze () ;
void AddRoom (Room*) ;
Room*RoomNo (int) const;
private:
// ...
};
RoomNo可以使用线形搜索、hash表、甚至一个简单数组进行一次查找。但我们在此处并不考虑这些细节,而是将注意力集中于如何指定一个迷宫对象的构件上。
我们定义的另一个类是MazeGame,由它来创建迷宫。一个简单直接的创建迷宫的方法是使用一系列操作将构件增加到迷宫中,然后连接它们。
例如,下面的成员函数将创建一个迷宫,这个迷宫由两个房间和它们之间的一扇门组成:
Maze*MazeGame::CreateMaze (){
Maze*aMaze = new Maze;
Room*r1 = new Room ( 1) ;
Room* r2 = new Room ( 2) ;
Door* theDoor = new Door (r1, r2);
aMaze->AddRoom (r1 );
aMaze->AddRoom (r2) ;
r1->Setside (North, new wall);
r1->setside (East, theDoor);
r1->Setside (south, new wall);
r1->setside (west, new wall) ;
r2->setside (North, new wall);
r2->setside (East, new wall);
r2->setside (south, new wall);
r2->setside (west, theDoor) ;
return aMaze;
}
考虑到这个函数所做的仅是创建一个有两个房间的迷宫,它是相当复杂的。
显然有办法使它变得更简单。
例如,Room的构造器可以提前用墙壁来初始化房间的每一面。但这仅仅是将代码移到了其他地方。
这个成员函数真正的问题不在于它的大小而在于它不灵活。它对迷宫的布局进行硬编码。改变布局意味着改变这个成员函数,或是重定义它——这意味着重新实现整个过程——或是对它的部分进行改变——这容易产生错误并且不利于重用。
创建型模式显示如何使得这个设计更灵活,但未必会更小。特别是,它们将便于修改定义一个迷宫构建的类。
如果我们现在要增加游戏的特点。给迷宫中现在是一个魔法迷宫。
施了魔法的迷宫游戏有新的构件,
- 像DoorNeedingSpell,它是一扇仅随着一个咒语才能被锁上和打开的门
- 以及EnchantedRoom,一个可以有不寻常东西的房间,比如魔法钥匙或是咒语
你怎样才能较容易的改变CreateMaze以让它用这些新类型的对象创建迷宫呢?
这种情况下,改变的最大障碍是对被实例化的类进行硬编码。
创建型模式提供了多种不同方法从实例化它们的代码中除去对这些具体类的显式引用:
-
如果CreateMaze调用虚函数而不是构造器来创建它需要的房间、墙壁和门,那么你可以创建一个MazeGame的子类并重定义这些虚函数,从而改变被例化的类。
这一方法是Factory Method ( 3.3)模式的一个例子。 -
如果传递一个对象给CreateMaze作参数来创建房间、墙壁和门,那么你可以传递不同的参数来改变房间、墙壁和门的类。
这是Abstract Factory ( 3.1)模式的一个例子。 -
如果传递一个对象给CreateMaze,这个对象可以在它所建造的迷宫中使用增加房间、墙壁和门的操作,来全面创建一个新的迷宫,那么你可以使用继承来改变迷宫的一些部分或该迷宫被建造的方式。
这是Builder ( 3.2)模式的一个例子。 -
如果CreateMaze由多种原型的房间、墙壁和门对象参数化,它拷贝并将这些对象增加到迷宫中,那么你可以用不同的对象替换这些原型对象以改变迷宫的构成。
这是Prototype(3.4)模式的一个例子。 -
Singleton ( 3.5),可以保证每个游戏中仅有一个迷宫而且所有的游戏对象都可以迅速访问它——不需要求助于全局变量或函数。Singleton也使得迷宫易于扩展或替换,且不需变动已有的代码。
3.1 ABSTRACT FACTORY(抽象工厂)
1. 意图
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
2. 别名
Kit
补充部分
直接接受书中内容理解起来较难,因此补充如下场景,帮助理解。
抽象工厂目的
“一些列相互依赖对象”的创建工作
提供一个接口,让该接口负责创建所有相互依赖的接口。
以此避免,子类分别创建出的类,在彼此调用时不兼容的情况。
我们考虑一场景
生产中客户可能想要用各类数据库
如果只靠如下代码,肯定是无法复用的
class EmployeeDAO{
public:
vector<EmployeeDO> GetEmployees(){
SqlConnection* connect = new SqlConnection();
connection->ConnectionString("...");
SqlCommand* command = new SqlCommand();
command->CommandText("...");
command->SetConnection(connection);
SqlDataReader* reader = command->ExecuteReader();
while(reader->Read()){
//...
}
}
};
我们现在让它支持多种数据库,即做一系列的基础类
// 访问相关基类
// 连接
class IDBConnection{
//...
};
// 工厂方法
class IDBConnectionFactory{
public:
virtual IDBConnection* CreatDBConnection() = 0;
};
// 命令
class IDBCommand{
//...
};
// 工厂方法
class IDBCommandFactory{
public:
virtual IDBCommand* CreateDBCommand() = 0;
};
// 结果
class IDataReader{
//...
};
// 工厂方法
class IDBDataReaderFactory{
public:
virtual IDataReader* CreateDataReader() = 0;
}
/**************************SQL*******************************/
// sql相关
class SqlConnection:public IDBConnection{
//...
};
// 具体工厂
class SqlConnection:public IDBConnectionFactory{
//...
}
// sql相关
class SqlCommand: public IDBCommand{
//...
};
// 具体工厂
class SqlCommand:public IDBCommandFactory{
//...
}
// sql相关
class SqlDataReader: public IDataReader{
//...
};
// 具体工厂
class SqlDataReader:public IDataReaderFactory{
//...
}
/**************************ORACLE********************************/
// oracle相关
class OracleConnection: public IDBConnection{
//...
};
// 具体工厂
class OracleConnection:public IDBConnectionFactory{
//...
}
// oracle相关
class OracleCommand: public IDBCommand{
//...
};
// 具体工厂
class OracleCommand:public IDBCommandFactory{
//...
}
// oracle相关
class OracleDataReader: public IDataReader{
//...
};
// 具体工厂
class OracleDataReader:public IDBDataReaderFactory{
//...
}
class EmployeeDAO{
private:
IDBConnectionFactory* dbConnectionFactory;
IDBCommandFactory* dBCommandFactory;
IDBDataReaderFactory* dbDataReaderFactory;
public:
vector<EmployeeDO> GetEmployees(){
IDBConnection* connect = dbConnectionFactory->CreateDBConnection();
connection->ConnectionString("...");
IDBCommand* command = dBCommandFactory->CreateDBCommand();
command->CommandText("...");
command->SetConnection(connection);
IDataReader* reader = command->ExecuteReader();
while(reader->Read()){
//...
}
}
};
如上场景,虽然我们解决了多个数据库对接问题,但是由于我们工厂之间需要相互关联,这样容易出问题。
也即是,如果我们不小心将Sql的Connection被放进了Oracle的Command中怎么办。
因此,我们需要抽象工厂来管理所有的方法工厂,来保证不会出现上述问题。
// 抽象工厂
class IDBFactory{
/** 做到了高内聚,低耦合 */
public:
virtual IDBConnection* CreateDBConnection() = 0;
virtual IDBCommand* CreateDBCommand() = 0;
virtual IDataReader* CreateDataReader() = 0;
}
/**************************SQL*******************************/
// sql相关
class SqlConnection:public IDBConnection{
//...
};
// sql相关
class SqlCommand: public IDBCommand{
//...
};
// sql相关
class SqlDataReader: public IDataReader{
//...
};
// 具体工厂
class SqlDBFactory:public IDBFactory{
//...
}
/**************************ORACLE********************************/
// oracle相关
class OracleConnection: public IDBConnection{
//...
};
// oracle相关
class OracleCommand: public IDBCommand{
//...
};
// oracle相关
class OracleDataReader: public IDataReader{
//...
};
// 具体工厂
class OracleDBFactory:public IDBFactory{
//...
}
3. 动机
考虑一个支持多种视感( look-and-feel)标准的用户界面工具包,例如Motif和Presentation Manager。不同的视感风格为诸如滚动条、窗口和按钮等用户界面“窗口组件”定义不同的外观和行为。
为保证视感风格标准间的可移植性,一个应用不应该为一个特定的视感外观硬编码它的窗口组件。在整个应用中实例化特定视感风格的窗口组件类将使得以后很难改变视感风格。
为解决这一问题我们可以定义一个抽象的WidgetFactory类,这个类声明了一个用来创建每一类基本窗口组件的接口。每一类窗口组件都有一个抽象类,而具体子类则实现了窗口组件的特定视感风格。
对于每一个抽象窗口组件类,WidgetFactory接口都有一个返回新窗口组件对象的操作。客户调用这些操作以获得窗口组件实例,但客户并不知道他们正在使用的是哪些具体类。
这样客户就不依赖于一般的视感风格,如下页图所示。
(此处可以对着笔者补充的Sql、Oracle部分看)
每一种视感标准都对应于一个具体的WidgetFactory子类。每一子类实现那些用于创建合适视感风格的窗口组件的操作。
例如,MotifWidgetFactory的CreateScrollBar操作实例化并返回一个Motif滚动条,而相应的PMWidgetFactory操作返回一个Presentation Manager的滚动条。
客户仅通过WidgetFactory接口创建窗口组件,他们并不知道哪些类实现了特定视感风格的窗口组件。换言之,客户仅与抽象类定义的接口交互,而不使用特定的具体类的接口。
WidgetFactory也增强了具体窗口组件类之间依赖关系。一个Motif的滚动条应该与Motif按钮、Motif正文编辑器一起使用,这一约束条件作为使用MotifWidgetFactory的结果被自动加上。
4. 适用性
在以下情况可以使用Abstract Factory模式
- 一个系统要独立于它的产品的创建、组合和表示时。
- 一个系统要由多个产品系列中的一个来配置时。
- 当你要强调一系列相关的产品对象的设计以便进行联合使用时。
- 当你提供个产品类库,而只想显示它们的接口而不是实现时。
5. 结构
6. 参与者
- AbstractFactory (widgetFactory)
—— 声明一个创建抽象产品对象的操作接口。 - ConcreteFactory (MotifWidgetFactory,PMWidgetFactory)
—— 实现创建具体产品对象的操作。 - AbstractProduct (Windows,ScrollBar)
—— 为一类产品对象声明一个接口。 - ConcreteProduct (MotifWindow,MotifScrollBar)
—— 定义一个将被相应的具体工厂创建的产品对象。
—— 实现AbstractProduct接口。 - Client
—— 仅使用由AbstractFactory和AbstractProduct类声明的接口。
7. 协作
- 通常在运行时刻创建一个ConcreteFactroy类的实例。这一具体的工厂创建具有特定实现的产品对象。为创建不同的产品对象,客户应使用不同的具体工厂。
- AbstractFactory将产品对象的创建延迟到它的ConcreteFactory子类。
8. 效果
AbstractFactory模式有下面的一些优点和缺点:
1. 它分离了具体的类
Abstract Factory模式帮助你控制一个应用创建的对象的类。因为一个工厂封装创建产品对象的责任和过程,它将客户与类的实现分离。客户通过它们的抽象接口操纵实例。产品的类名也在具体工厂的实现中被分离;它们不出现在客户代码中。
- 它使得易于交换产品系列
一个具体工厂类在一个应用中仅出现一次—即在它初始化的时候。这使得改变一个应用的具体工厂变得很容易。它只需改变具体的工厂即可使用不同的产品配置,这是因为一个抽象工厂创建了一个完整的产品系列,所以整个产品系列会立刻改变。在我们的用户界面的例子中,我们仅需转换到相应的工厂对象并重新创建接口,就可实现从Motif窗口组件转换为Presentation Manager窗口组件。
- 它有利于产品的一致性
当一个系列中的产品对象被设计成一起工作时,一个应用一次只能使用同一个系列中的对象,这一点很重要。而AbstractFactory很容易实现这一点。
- 难以支持新种类的产品难以扩展抽象工厂以生产新种类的产品
这是因为AbstractFactory接口确定了可以被创建的产品集合。支持新种类的产品就需要扩展该工厂接口,这将涉及AbstractFactory类及其所有子类的改变。我们会在实现一节讨论这个问题的一个解决办法。
9. 实现
下面是实现Abstract Factor模式的一些有用技术:
- 将工厂作为单件
一个应用中一般每个产品系列只需-个ConcreteFactory的实例。因此工厂通常最好实现为一个Singleton ( 3.5 )。
- 创建产品AbstractFactory仅声明一个
创建产品的接口,真正创建产品是由ConcreteProduct子类实现的。最通常的一个办法是为每一个产品定义一个工厂方法(参见Factory Method ( 3.3 ))。一个具体的工厂将为每个产品重定义该工厂方法以指定产品。虽然这样的实现很简单,但它却要求每个产品系列都要有一个新的具体工厂子类,即使这些产品系列的差别很小。
如果有多个可能的产品系列,具体工厂也可以使用Prototype(3.4)模式来实现。
具体工厂使用产品系列中每一个产品的原型实例来初始化,且它通过复制它的原型来创建新的产品。在基于原型的方法中,使得不是每个新的产品系列都需要一个新的具体工厂类。
此处是Smalltalk中实现一个基于原型的工厂的方法。具体工厂在一个被称为partCatalog的字典中存储将被复制的原型。
方法make:检索原型并复制它:
在将类作为第一类对象的语言中(例如Smalltalk和ObjectiveC),这个基于原型的方法可能有所变化。你可以将这些语言中的一个类看成是一个退化的工厂,它仅创建一种产品。
你可以将类存储在一个具体工厂中,这个具体工厂在变量中创建多个具体的产品,这很像原型。这些类代替具体工厂创建了新的实例。
你可以通过使用产品的类而不是子类初始化一个具体工厂的实例,来定义一个新的工厂。这一方法利用了语言的特点,而纯基于原型的方法是与语言无关的。
像刚讨论过的Smalltalk中的基于原型的工厂一样,基于类的版本将有一个唯一的实例变量partCatalog,它是一个字典,它的主键是各部分的名字。partCatalog存储产品的类而不是存储被复制的原型。
方法make:现在是这样
- 定义可扩展的工厂
AbstractFactory通常为每一种它可以生产的产品定义一个操作。产品的种类被编码在操作型构中。增加一种新的产品要求改变AbstractFactory的接口以及所有与它相关的类。一个更灵活但不太安全的设计是给创建对象的操作增加一个参数。该参数指定了将被创建的对象的种类。它可以是一个类标识符、一个整数、一个字符串,或其他任何可以标识这种产品的东西。实际上使用这种方法,AbstractFactory只需要一个“Make”操作和一个指示要创建对象的种类的参数。这是前面已经讨论过的基于原型的和基于类的抽象工厂的技术。
C++这样的静态类型语言与相比,这一变化更容易用在类似于Smalltalk这样的动态类型语言中。仅当所有对象都有相同的抽象基类,或者当产品对象可以被请求它们的客户安全的强制转换成正确类型时,你才能够在C++中使用它。Factory Method(3.3)的实现部分说明了怎样在C++中实现这样的参数化操作。
该方法即使不需要类型强制转换,但仍有一个本质的问题:所有的产品将返回类型所给定的相同的抽象接口返回给客户。客户将不能区分或对一个产品的类别进行安全的假定。如果一个客户需要进行与特定子类相关的操作,而这些操作却不能通过抽象接口得到。虽然客户可以实施一个向下类型转换( downcast)(例如在C++中用dynamic_cast ),但这并不总是可行或安全的,因为向下类型转换可能会失败。这是一个典型的高度灵活和可扩展接口的权衡折衷。
10. 代码示例
我们将使用Abstract Factory模式创建我们在这章开始所讨论的迷宫。
类MazeFactory可以创建迷宫的组件。它建造房间、墙壁和房间之间的门。它可以用于一个从文件中读取迷宫说明图并建造相应迷宫的程序。或者它可以被用于一个随机建造迷宫的程序。
建造迷宫的程序将MazeFactory作为一个参数,这样程序员就能指定要创建的房间、墙壁和门等类。
class MazeFactory {
public:
MazeFactory ( ) ;
virtual Maze* MakeMaze() const{ return new Maze; }
virtual wall* Makewall ( ) const {return new wall ; }
virtual Room* MakeRoom (int n) {const return new Room (n); }
virtual Door* MakeDoor ( Room* r1,Room* r2) const{ return new Door(r1, r2) ; }
};
回想一下建立一个由两个房间和它们之间的门组成的小迷宫的成员函数CreateMaze。
CreateMaze对类名进行硬编码,这使得很难用不同的组件创建迷宫。
这里是一个以MazeFactory为参数的新版本的CreateMaze ,它修改了以上缺点
Maze*MazeGame: :createMaze (MazeFactory& factory) {
Maze*aMaze = factory.MakeMaze() ;
Room*r1 = factory :MakeRoom (1) ;
Roomr2 = factory.MakeRoom (2);
Door*aDoor - factory.MakeDoor (r1, r2);
aMaze->AddRoom (r1) ;
aMaze->AddRoom(r2) ;
r1->setside (North, factory.Makewal1());
r1->setside (East, aDoor) ;
r1->setside (south, factory.Makewall () ;
r1->setside (west, factory.Makewall();
r2->setside (North, factory.Makewall());
r2->Setside (East, factory.Makewall(0);
r2->setside(south, factory.Makewall();
r2->setside (West, aDoor);
return aMaze;
}
我们创建MazeFactory的子类EnchantedMazeFactory,这是一个创建施了魔法的迷宫的工厂。EnchantedMazeFactory将重定义不同的成员函数并返回Room,Wall等不同的子类。
class EnchantedvazeFactory : public MazeFactory {
public:
EnchantedMazeFactory () ;
virtual Room*MakeRoom (int n)const { return new EnchantedRoom (n,Castspell()); }
virtual Door* MakeDoor(Room* ri,Room* r2)const { return new DoorNeedingspell(r1, r2) ; }
protected:
spell* castspell()const;
};
现在假设我们想生成一个迷宫游戏,在这个游戏里,每个房间中可以有一个炸弹。如果这个炸弹爆炸,它将(至少)毁坏墙壁。
我们可以生成一个Room的子类以明了是否有一个炸弹在房间中以及该炸弹是否爆炸了。我们也将需要一个Wall的子类以明了对墙壁的损坏。我们将称这些类为RoomWithABomb和BombedWall。
我们将定义的最后一个类是BombedMazeFactory,它是MazeFactory的子类,保证了墙壁是BombedWall类的而房间是RoomWithABomb的。BombedMazeFactory仅需重定义两个函数:
wall* BombedMazeFactory : :Makewall () const i
return new Bombedwal1;
}
Room*BombedMazeFactory : :MakeRoom (int n) const {
return new RoomwithABomb(n);
}
为建造一个包含炸弹的简单迷宫,我们仅用BombedMazeFactory调用CreateMaze。
MazeGame game;
BombedMazeFactory factory ;
game.createMaze ( factory);
CreateMaze也可以接收一个EnchantedMazeFactory实例来建造施了魔法的迷宫。
注意MazeFactory仅是工厂方法的一个集合。这是最通常的实现Abstract Factory模式的方式。
同时注意MazeFactory不是一个抽象类,因此它既作为AbstractFactory也作为ConcreteFactory。
这是Abstract Factory模式的简单应用的另一个通常的实现。因为MazeFactory是一个完全由工厂方法组成的具体类,通过生成一个子类并重定义需要改变的操作,它很容易生成一个新的MazeFactory。
CreateMaze使用房间的SetSide操作以指定它们的各面。
如果它用一个BombedMazeFactory创建房间,那么该迷宫将由有BombedWall面的Room WithABomb对象组成。
如果RoomWithABomb必须访问一个BombedWall的与特定子类相关的成员,那么它将不得不对它的墙壁引用以进行从Wall*到BombedWall*的转换。
只要该参数确实是一个BombedWall,这个向下类型转换就是安全的,而如果墙壁仅由一个BombedMazeFactory创建就可以保证这一点。
当然,像Smalltalk这样的动态类型语言不需要向下类型转换,但如果它们在应该是Wall的子类的地方遇到一个Wall类可能会产生运行时刻错误。使用Abstract Factory建造墙壁,通过确定仅有特定类型的墙壁可以被创建,从而有助于防止这些运行时刻错误。
(Smalltalk版本的Factory,笔记略)。
11.相关模式
AbstractFactory类通常用
- 工厂方法(Factory Method(3.3))实现
- 也可以用原型(Prototype(3.4))
- 一个具体的工厂通常是一个单件(Singleton ( 3.5))。
3.2 BUILDER(生成器)
1.意图
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
补充部分
直接接受书中内容理解起来较难,因此补充如下场景,帮助理解。
主要用于“复杂对象”的创建工作
其通常各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临剧烈变化
但将它们组合在一起的算法却相对稳定。
试图通过一种“封装机制”来隔离出“复杂对象的各个部分的变化”,从而保持系统中的”稳定构建算法“不随着需求的改变而改变。
我们思考如下场景。
我们想要建一个房子,他虽然可以有很多材料可以改动,但主要流程不会变。
class House{
public:
void Init(){
this->BuildPart1();
for(int i = 0;i < 4;i++){
this->BuildPart2();
}
bool flag = this->BuildPart3();
if(flag){
this->BuildPart4();
}
this->BuildPart5();
}
// ...
/**
* 此处不能用House(),因为在C++中,构造函数去调用虚函数,它这时静态绑定,又因为此时没有实现,会报错
* 此时C++在构造时,子类会首先调用父类构造函数,这时父类就不会去调用子类的实现函数而是会直接调用父类自己的函数
* Java 中就可以
**/
}
virtual ~House(){}
protected:
virtual void BuildPart1() = 0;
virtual void BuildPart2() = 0;
virtual void BuildPart3() = 0;
virtual void BuildPart4() = 0;
virtual void BuildPart5() = 0;
};
class StoneHouse:public House{
protected:
virtual void BuildPart1(){
//...
}
virtual void BuildPart2(){
//...
}
virtual void BuildPart3(){
//...
}
virtual void BuildPart4(){
//...
}
virtual void BuildPart5(){
//...
}
};
int main(){
House* pHouse = new StoneHouse();
pHouse->Init();
}
我们觉得这样创建(Init方法)将各个部分全都放在一起很不舒服。
于是我们通过生成器,将可替换的部分提出来,去单独使用。将稳定、变化的部分分开。
我们下面的代码,将对象的表示与构建分分离。
class House{
//...
};
// 生成器
class HouseBuilder{
public:
House* GerResult(){
return pHouse;
}
virtual ~HouseBuilder();
protected:
House* pHouse;
virtual void BuildPart1() = 0;
virtual void BuildPart2() = 0;
virtual void BuildPart3() = 0;
virtual void BuildPart4() = 0;
virtual void BuildPart5() = 0;
};
// 执行器
class HouseDirector{
public:
HouseBuilder* pHouseBuilder;
HouserDirector(HouseBuilder* pHouseBuilder){
this->pHouseBuilder = pHouseBuilder;
}
House* Construct(){
pHouseBuilder->BuildPart1();
for(int i = 0;i < 4;i++){
pHouseBuilder->BuildPart2();
}
bool flag = pHouseBuilder->BuildPart3();
if(flag){
pHouseBuilder->BuildPart4();
}
pHouseBuilder->BuildPart5();
return pHouseBuilder->GetResult();
}
};
class StoneHouse: public House{
// ...
};
class StoneHouseBuilder: public HouseBuilder{
protected:
virtual void BuildPart1(){
pHouse->....
//...
}
virtual void BuildPart2(){
//...
}
virtual void BuildPart3(){
//...
}
virtual void BuildPart4(){
//...
}
virtual void BuildPart5(){
//...
}
}
int main(){
HouseDirector* houseDirector = new HouseDirector (new StoneHouseBuilder());\
House* house = houseDirector->Construct();
}
2.动机
一个RTF (Rich Text Format)文档交换格式的阅读器应能将RTF转换为多种正文格式。该阅读器可以将RTF文档转换成普通ASCII文本或转换成一个能以交互方式编辑的正文窗口组件。但问题在于可能转换的数目是无限的。因此要能够很容易实现新的转换的增加,同时却不改变RTF阅读器。
一个解决办法是用一个可以将RTF转换成另一种正文表示的TextConverter对象配置这个RTFReader类。当RTFReader对RTF文档进行语法分析时,它使用TextConverter去做转换。无论何时RTFReader识别了一个RTF标记(或是普通正文或是一-个RTF控制字),它都发送一个请求给TextConverter去转换这个标记。TextConverter对象负责进行数据转换以及用特定格式表示该标记,如下图所示。
TextConvert的子类对不同转换和不同格式进行特殊处理。
例如,一个ASCIIConverter只负责转换普通文本,而忽略其他转换请求。另一方面,一个TeXConverter将会为实现对所有请求的操作,以便生成一个获取正文中所有风格信息的TEX表示。一个TextWidgetConverter将生成一个复杂的用户界面对象以便用户浏览和编辑正文。
每种转换器类将创建和装配一个复杂对象的机制隐含在抽象接口的后面。转换器独立于阅读器,阅读器负责对一个RTF文档进行语法分析。
Builder模式描述了所有这些关系。
每一个转换器类在该模式中被称为生成器( builder ),而阅读器则称为导向器( director )。
在上面的例子中,Builder模式将分析文本格式的算法(即RTF文档的语法分析程序)与描述怎样创建和表示一个转换后格式的算法分离开来。这使我们可以重用RTFReader的语法分析算法,根据RTF文档创建不同的正文表示——仅需使用不同的TextConverter的子类配置该RTFReader即可。
3.适用性
在以下情况使用Builder模式
- 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
- 当构造过程必须允许被构造的对象有不同的表示时。
4.结构
此模式结构如图所示。
5.参与者
- Builder ( TextConverter )
- 为创建一个Product对象的各个部件指定抽象接口。
- ConcreteBuilder ( ASCIIConverter、TeXConverter、TextWidgetConverter )
- 实现Builder的接口以构造和装配该产品的各个部件。
- 定义并明确它所创建的表示。
- 提供一个检索产品的接口(例如,GetASCIIText和GetTextWidget )o.Director ( RTFReader )
- 构造一个使用Builder接口的对象。
- Product ( ASCIIText、TeXText、TextWidget )
- 表示被构造的复杂对象。ConcreteBuilder创建该产品的内部表示并定义它的装配过程。一包含定义组成部件的类,包括将这些部件装配成最终产品的接口。
6.协作
- 客户创建Director对象,并用它所想要的Builder对象进行配置。
- 一旦产品部件被生成,导向器就会通知生成器。
- 生成器处理导向器的请求,并将部件添加到该产品中。
- 客户从生成器中检索产品。
下面的交互图说明了Builder和Director是如何与一个客户协作的。
7.效果
这里是Builder模式的主要效果:
- 它使你可以改变一个产品的内部表示 Builder对象提供给导向器一个构造产品的抽象接口。该接口使得生成器可以隐藏这个产品的表示和内部结构。它同时也隐藏了该产品是如何装配的。因为产品是通过抽象接口构造的,你在改变该产品的内部表示时所要做的只是定义一个新的生成器。
- 它将构造代码和表示代码分开 Builder模式通过封装一个复杂对象的创建和表示方式提高了对象的模块性。客户不需要知道定义产品内部结构的类的所有信息;这些类是不出现在Builder接口中的。每个ConcreteBuilder包含了创建和装配一个特定产品的所有代码。这些代码只需要写一次;然后不同的Director可以复用它以在相同部件集合的基础上构作不同的Product。在前面的RTF例子中,我们可以为RTF格式以外的格式定义一个阅读器,比如一个SGMLReader,并使用相同的TextConverter生成SGML文档的ASCIIText、TeXText和TextWidget译本。
- 它使你可对构造过程进行更精细的控制 Builder模式与一下子就生成产品的创建型模式不同,它是在导向者的控制下一步一步构造产品的。仅当该产品完成时导向者才从生成器中取回它。因此Builder接口相比其他创建型模式能更好的反映产品的构造过程。这使你可以更精细的控制构建过程,从而能更精细的控制所得产品的内部结构。
8.实现
通常有一个抽象的Builder类为导向者可能要求创建的每一个构件定义一个操作。这些操作缺省情况下什么都不做。一个ConcreteBuilder类对它有兴趣创建的构件重定义这些操作。
这里是其他一些要考虑的实现问题:
- 装配和构造接口
生成器逐步的构造它们的产品。因此Builder类接口必须足够普遍,以便为各种类型的具体生成器构造产品。
一个关键的设计问题在于构造和装配过程的模型。构造请求的结果只是被添加到产品中,通常这样的模型就已足够了。在RTF的例子中,生成器转换下一个标记并将它添加到它已经转换了的正文中。
但有时你可能需要访问前面已经构造了的产品部件。我们在代码示例一节所给出的Maze例子中,MazeBuilder接口允许你在已经存在的房间之间增加一扇门。像语法分析树这样自底向上构建的树型结构就是另一个例子。在这种情况下,生成器会将子结点返回给导向者,然后导向者将它们回传给生成者去创建父结点。
- 为什么产品没有抽象类
通常情况下,由具体生成器生成的产品,它们的表示相差是如此之大以至于给不同的产品以公共父类没有太大意思。在RTF例子中,ASCIIText和TextWidget对象不太可能有公共接口,它们也不需要这样的接口。因为客户通常用合适的具体生成器来配置导向者,客户处于的位置使它知道Builder的哪一个具体子类被使用和能相应的处理它的产品。
- 在Builder中却省的方法为空
C++中,生成方法故意不声明为纯虚成员函数,而是把它们定义为空方法,这使客户只重定义他们所感兴趣的操作。
9.代码示例
我们将定义一个CreateMaze成员函数的变体,它以类MazeBuilder的一个生成器对象作为参数。
MazeBuilder类定义下面的接口来创建迷宫:
class MazeBuilder{
public:
virtual void BuildMaze(){}
virtual void BuildRoom(int room){}
virtual void BuildDoor(int roomFrom,int roomTo){}
virtual Maze* GetMaze() { return 0;}
protected:
MazeBuilder ();
};
该接口可以创建:
- 迷宫。
- 有一个特定房间号的房间。
- 在有号码的房间之间的门。
GetMaze操作返回这个迷宫给客户。MazeBuilder的子类将重定义这些操作,返回它们所创建的迷宫。
MazeBuilder的所有建造迷宫的操作缺省时什么也不做。不将它们定义为纯虚函数是为了便于派生类只重定义它们所感兴趣的那些方法。
用MazeBuilder接口,我们可以改变CreateMaze成员函数,以生成器作为它的参数。
Maze* MazeGame::createMaze (MazeBuilder& builder){
builder.BuildMaze();
builder.Bui1dRoom(1);
builder.BuildRoom(2);
builder.Bui1dDoor(1,2);
return builder.GetMaze () ;
}
将这个CreateMaze版本与原来的相比,注意生成器是如何隐藏迷宫的内部表示的——即定义房间、门和墙壁的那些类——以及这些部件是如何组装成最终的迷宫的。
有人可能猜测到有一些类是用来表示房间和门的,但没有迹象显示哪个类是用来表示墙壁的。这就使得改变一个迷宫的表示方式要容易一些,因为所有MazeBuilder的客户都不需要被改变。
像其他创建型模式一样,Builder模式封装了对象是如何被创建的,在这个例子中是通过MazeBuilder所定义的接口来封装的。
这就意味着我们可以重用MazeBuilder来创建不同种类的迷宫。CreateComplexMaze操作给出了一个例子:
Maze*MazeGame::CreateComplexMaze (MazeBuilder& builder) {
builder. BuildRoom (1);
// ...
builder.BuildRoom (1001);
return builder.GetMaze();
)
注意MazeBuilder自己并不创建迷宫;它的主要目的仅仅是为创建迷宫定义一-个接口。它主要为方便起见定义一些空的实现。MazeBuilder的子类做实际工作。
子类StandardMazeBuilder是一个创建简单迷宫的实现。它将它正在创建的迷宫放在变量_currentMaze中。
class standardMazeBuilder:public MazeBuilder {
public:
standardMazeBuilder();
virtual void BuildMaze();
virtual void BuildRoom(int);
virtual void BuildDoor(int, int);
virtual Maze*GetMaze ()private:
Direction Commonwall (Room*, Room*);
Maze* _currentMaze;
};
CommonWall是一个功能性操作,它决定两个房间之间的公共墙壁的方位。StandardMazeBuilder的构造器只初始化了currentMaze。
standardMazeBuilder::standardMazeBuilder (){
_currentMaze = 0;
}
BuildMaze实例化一个Maze,它将被其他操作装配并最终返回给客户(通过GetMaze )。
void standardMazeBuilder::Buildvaze () {
_currentMaze = new Maze;
}
Maze* standardMazeBui1der::GetMaze(){
return _currentMaze;
}
BuildRoom操作创建一个房间并建造它周围的墙壁:
void standardMazeBui1der::BuildRoom (int n){
if (!_currentMaze->RoomNo (n)) {
Room* room = new Room(n);
_currentMaze->AddRoom(room) ;
room->setside(North, new wall);
room->setside(south, new wall);
room->Setside(East, new wall);
room->setside(west, new wall) ;
}
}
为建造一扇两个房间之间的门,StandardMazeBuilder查找迷宫中的这两个房间并找到它们相邻的墙:
void standardMazeBuilder::Bui1dDoor(int n1, int n2){
Room*r1 = _currentMaze->RoomNo (n1);.
Room r2 - _currentMaze->RoomNo (n2.);
Door* d = new Door (r1, r2);
r1->setside(Commonwall(r1,r2),d);
r2->setside(commonnall(r2,r1),d);
}
客户现在可以用CreateMaze和StandardMazeBuilder来创建一个迷宫:
Maze*maze;
MazeGame game ;
StandardMazeBuilder builder;
game.createMaze(builder);
maze = builder.GetMaze();
我们本可以将所有的StandardMazeBuilder操作放在Maze中,并让每一个Maze创建它自身。但将Maze变得小一些使得它能被更容易被理解和修改,而且StandardMazeBuilder 易于从Maze中分离。更重要的是,将两者分离使我们可以有多种的MazeBuilder,每一种使用不同的房间、墙壁和门的类。
一个更特殊的MazeBuilder是CountingMazeBuilder。这个生成器根本不创建迷宫;它仅仅对已被创建的不同种类的构件进行计数。
class CountingMazeBui1der : public MazeBuilder{
public:
countingMazeBuilder();
virtual void Bui1dMaze();
virtual void Bui1dRoom(int) ;
virtual void BuildDoor(int, int);
virtual void Addwall(int, Direction) ;
void Getcounts(int&, int&)const;
private:
int _doors;
int _rooms;
};
构造器初始化该计数器,而重定义了的MazeBuilder操作只是相应的增加计数。
CountingMazeBuilder: :CountingMazeBuilder(){
_rooms = _doors = 0;
}
void countingMazeBuilder::Bui1dRoom (int) {
_rooms++;
}
void countingMazeBuilder::BuildDoor (int, int) {
_doors++;
}
void countingMazeBuilder::Getcounts (int& rooms, int& doors)const{
rooms = _rooms;
doors = _doors;
}
下面是一个客户可能怎样使用CountingMazeBuilder:
int rooms,doors;
MazeGame game;
CountingMazeBuilder builder;
game.CreateMaze (builder) ;
bui1der.GetCounts (rooms,doors);
cout <<"The maze has " << rooms << " rooms and "<<doors<<" doors"<< endl;
10. 相关模式
Abstract Factory(3.1)与Builder相似,因为它也可以创建复杂对象。主要的区别是Builder模式着重于一步步构造一个复杂对象。而Abstract Factory着重于多个系列的产品对象(简单的或是复杂的)。Builder在最后的一步返回产品,而对于Abstract Factory来说,产品是立即返回的。
Composite(4.3)通常是用Builder生成的。
3.3 FACTORY METHOD(工厂方法)
1.意图
定义一个用于创建对象的接口,让子类决定实例化哪–个类。Factory Method使一个类的实例化延迟到其子类。
2.别名
虚构造器( Virtual Constructor )
补充部分
如下场景
有一个方法能够对文本进行分割。
class FileSplitter{
public:
void split(){
// ...
}
};
class MainForm:public Form{
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar* progressBar;
public:
void Button1_Click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
FileSplitter* splitter = new FileSplitter(filePath,number);
splitter->split();
}
};
如果我们想要新增分割符的能力(对图片、二进制等进行分割)。我们就不得不对其进行修改。抽象出一个基类
class ISplitter{
public:
virtual void split() = 0;
virtual ~ISplitter(){}
}
class FileSplitter : public ISplitter{
};
class TxtSplitter : public ISplitter{
};
class PictureSpliter : public ISplitter{
};
但是我们应该如何使用?我们如果要替换为其他的Splitter,我们仍旧要如下方式。于是我们得想办法绕开new。
ISplitter* spliiter = new PictureSpliter(...);
ISplitter* spliiter = new TxtSplitter(...);
ISplitter* spliiter = new FileSplitter(...);
就需要工厂方法
// 基类,抽象接口
class ISplitter{
public:
virtual void split() = 0;
virtual ~ISplitter(){}
};
// 工厂方法
class SplitterFactory{
public:
virtual ISplitter* CreateSplitter() = 0;
virtual ~SplitterFactory(){}
};
class MainForm:public Form{
private:
//...
SplitterFactory* factory;
public:
MainForm(SplitterFactory* factory){
this->factory = factory;
}
void Button1_Click(){
// ...
// 延迟创建
ISplitter* Splitter = factory->CreateSplitter();
splitter->split();
}
};
class FileSplitter : public ISplitter{
};
// 具体工厂
class FileSplitter Factory: public SplitterFactory{
public:
virtual ISplitter* CreateSplitter(){
return new FileSplitter();
}
};
class TxtSplitter : public ISplitter{
};
// 具体工厂
class TxtSplitter Factory: public SplitterFactory{
public:
virtual ISplitter* CreateSplitter(){
return new TxtSplitter();
}
};
class PictureSpliter : public ISplitter{
};
// 具体工厂
class PictureSplitter Factory: public SplitterFactory{
public:
virtual ISplitter* CreateSplitter(){
return new PictureSplitter();
}
};
注意,虽然我们仍然需要外部传递一个工厂来让我们去使用,但在这个类中,它全部都是用的抽象去完成的逻辑。
我们不是为了把变化消灭掉(也消灭不掉),我们只是把这种变化赶到某一个局部区域,让它在某些范围存在,而不是到处都是。
3.动机
框架使用抽象类定义和维护对象之间的关系。这些对象的创建通常也由框架负责。
考虑这样一个应用框架,它可以向用户显示多个文档。在这个框架中,两个主要的抽象是类Application和Document。这两个类都是抽象的,客户必须通过它们的子类来做与具体应用相关的实现。
例如,为创建一个绘图应用,我们定义类DrawingApplication和DrawingDocument。Application类负责管理Document并根据需要创建它们——例如,当用户从菜单中选择Open或New的时候。
因为被实例化的特定Document子类是与特定应用相关的,所以Application类不可能预测到哪个Document子类将被实例化—Application类仅知道一个新的文档何时应被创建,而不知道哪-一种Document将被创建。这就产生了一个尴尬的局面:框架必须实例化类,但是它只知道不能被实例化的抽象类。
Factory Method模式提供了一个解决办案。它封装了哪一个Document子类将被创建的信息并将这些信息从该框架中分离出来,如图所示。
Application的子类重定义Application的抽象操作CreateDocument以返回适当的Document子类对象。
一旦一个Application子类实例化以后,它就可以实例化与应用相关的文档,而无需知道这些文档的类。
我们称CreateDocument是一个工厂方法( factory method ),因为它负责“生产”一个对象。
4.适用性
在下列情况下可以使用Factory Method模式:
- 当一个类不知道它所必须创建的对象的类的时候。
- 当一个类希望由它的子类来指定它所创建的对象的时候。
- 当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。
5.结构
6.参与者
- Product(Document)
- 定义工厂方法所创建的对象的接口。. ConcreteProduct ( MyDocument )一实现Product接口。
- Creator ( Application )
- 声明工厂方法,该方法返回一个Product类型的对象。Creator也可以定义一个工厂方法的缺省实现,它返回一个缺省的ConcreteProduct对象。
- 可以调用工厂方法以创建一个Product对象。
- ConcreteCreator ( MyApplication)
- 重定义工厂方法以返回一个ConcreteProduct实例。
7.协作
Creator依赖于它的子类来定义工厂方法,所以它返回一个适当的ConcreteProduct实例。
8.效果
工厂方法不再将与特定应用有关的类绑定到你的代码中。代码仅处理Product接口;因此它可以与用户定义的任何ConcreteProduct类一起使用。
工厂方法的一个潜在缺点在于客户可能仅仅为了创建一个特定的ConcreteProduct对象,就不得不创建Creator的子类。当Creator子类不必需时,客户现在必然要处理类演化的其他方面;但是当客户无论如何必须创建Creator的子类时,创建子类也是可行的。
下面是Factory Method模式的另外两种效果:
- 为子类提供挂钩(hook)
用工厂方法在一个类的内部创建对象通常比直接创建对象更灵活。Factory Method给子类一个挂钩以提供对象的扩展版本。
在Document的例子中,Document类可以定义一个称为CreateFileDialog的工厂方法,该方法为打开一个已有的文档创建默认的文件对话框对象。Document的子类可以重定义这个工厂方法以定义一个与特定应用相关的文件对话框。在这种情况下,工厂方法就不再抽象了而是提供了一个合理的缺省实现。
- 连接平行的类层次
迄今为止,在我们所考虑的例子中,工厂方法并不往往只是被Creator调用,客户可以找到一些有用的工厂方法,尤其在平行类层次的情况下。
当一个类将它的一些职责委托给一个独立的类的时候,就产生了平行类层次。考虑可以被交互操纵的图形;也就是说,它们可以用鼠标进行伸展、移动,或者旋转。实现这样一些交互并不总是那么容易,它通常需要存储和更新在给定时刻记录操纵状态的信息,这个状态仅仅在操纵时需要。因此它不需要被保存在图形对象中。
此外,当用户操纵图形时,不同的图形有不同的行为。例如,将直线图形拉长可能会产生一个端点被移动的效果,而伸展正文图形则可能会改变行距。
有了这些限制,最好使用一个独立的Manipulator对象实现交互并保存所需要的任何与特定操纵相关的状态。不同的图形将使用不同的Manipulator子类来处理特定的交互。得到的Manipulator类层次与Figure类层次是平行(至少部分平行),如下图所示。
Figure类提供了一个CreateManipulator工厂方法,它使得客户可以创建一个与Figure相对应的Manipulator。
Figure子类重定义该方法以返回一个合适的Manipulator子类实例。做为一种选择,Figure类可以实现CreateManipulator以返回一个默认的Manipulator实例,而Figure子类可以只是继承这个缺省实现。这样的Figure类不需要相应的Manipulator子类——因此该层次只是部分平行的。
注意工厂方法是怎样定义两个类层次之间的连接的。.它将哪些类应一同工作工作的信息局部化了。
9.实现
当应用Factory Method模式时要考虑下面一些问题:
- 主要有两种不同的情况Factory Method模式主要有两种不同的情况:
- 第一种情况是,Creator类是一个抽象类并且不提供它所声明的工厂方法的实现。
- 第二种情况是,Creator是一个具体的类而且为工厂方法提供一个缺省的实现。也有可能有一个定义了缺省实现的抽象类,但这不太常见。
第一种情况需要子类来定义实现,因为没有合理的缺省实现。它避免了不得不实例化不可预见类的问题。在第二种情况中,具体的Creator主要因为灵活性才使用工厂方法。它所遵循的准则是,“用一个独立的操作创建对象,这样子类才能重定义它们的创建方式。”这条准则保证了子类的设计者能够在必要的时候改变父类所实例化的对象的类。
- 参数化工厂方法惇该模式的另一种情况使得工厂方法可以创建多种产品。
工厂方法采用一个标识要被创建的对象种类的参数。工厂方法创建的所有对象将共享Product接口。在Document的例子中,Application可能支持不同种类的Document。你给CreateDocument传递一个外部参数来指定将要创建的文档的种类。
图形编辑框架Unidraw使用这种方法来重构存储在磁盘上的对象。Unidraw定义了一个Creator类,该类拥有一个以类标识符为参数的工厂方法Create。类标识符指定要被实例化的类。当Unidraw将一个对象存盘时,它首先写类标识符,然后是它的实例变量。当它从磁盘中重构该对象时,它首先读取的是类标识符。
一旦类标识符被读取后,这个框架就将该标识符作为参数,调用Create。Create到构造器中查询相应的类并用它实例化对象。最后,Create调用对象的Read操作,读取磁盘上剩余的信息并初始化该对象的实例变量。
一个参数化的工厂方法具有如下的一般形式,此处MyProduct和YourProduct是Product的子类:
class creator {
public:
virtual Product*create ( Productid);
};
Product* creator::create ( ProductId id){
if (id == MINE)return new MyProduct ;
if (id -= YOURS) return new YourProduct;
// repeat for remaining products. . .
return 0;
)
重定义一个参数化的工厂方法使你可以简单而有选择性的扩展或改变一个Creator生产的产品。你可以为新产品引入新的标识符,或可以将已有的标识符与不同的产品相关联。
例如,子类MyCreator可以交换MyProduct和YourProduct并且支持一个新的子类TheirProduct:
Product* Mycreator::create (ProductIa id){
if (id =a YOURS)return new MyProduct;
if (id == .MINE)return new YourProduct;
// N.B. : switched YoURs 'and MINE
if (id == THEIRS) return new TheirProduct;
return Creator::create(id); // called if.all others fail
注意这个操作所做的最后一件事是调用父类的Create。这是因为MyCreator::Create仅在对YOURS、MINE和THEIRS的处理上和父类不同。它对其他类不感兴趣。因此MyCreator扩展了所创建产品的种类,并且将除少数产品以外所有产品的创建职责延迟给了父类。
- 特定语言的变化和问题
不同的语言有助于产生其他一些有趣的变化和警告( caveat )。Smalltalk程序通常使用一个方法返回被实例化的对象的类。Creator工厂方法可以使用这个值去创建一个产品,并且ConcreteCreator可以存储甚至计算这个值。这个结果是对实例化的ConcreteProduct类型的一个更迟的绑定。
Smalltalk版本的Document的例子可以在Application中定义一-个documentClass方法。该方法为实例化文档返回合适的Document类,其在MyApplication中的实现返回MyDocument类。这样在类Application中我们有
clientMethod
document := self documentC1ass new.
documentclass
self subclassResponsibiity
在类MyApplication中我们有
documentclass
^ MyDocument
它把将被实例化的类MyDocument返回给Application。一个更灵活的类似于参数化工厂方法的办法是将被创建的类存储为Application的一个类变量。你用这种方法在改变产品时就无需用到Application的子类。
C++中的工厂方法都是虚函数并且常常是纯虚函数。一定要注意在Creator的构造器中不要调用工厂方法——在ConcreteCreator中该工厂方法还不可用。
只要你使用按需创建产品的访问者操作,很小心地访问产品,你就可以避免这一点。构造器只是将产品初始化为0,而不是创建一个具体产品。访问者返回该产品。但首先它要检查确定该产品的存在,如果产品不存在,访问者就创建它。这种技术有时被称为lazyinitialization。下面的代码给出了一个典型的实现:
class creator {
public:
Product*Get Product () ;
protected:
virtual Product* createProduct () ;
private:
Product* _product;
};
Product* creator::GetProduct {
if (_product == 0){
product = createProduct ( );
}
return product;
}
- 使用模板以避免创建子类
正如我们已经提及的,工厂方法另-个潜在的问题是它们可能仅为了创建适当的Product对象而迫使你创建Creator子类。在C++中另一个解决方法是提供Creator的一个模板子类,它使用Product类作为模板参数:
c1ass Creator{
public:
virtual Product* CreateProduct () = 0;
};
template <class TheProduct>
class standardcreator: public Creator {
public:
virtual Product* createProduct ();
};
template <class TheProduct>
Product* standardcreator<TheProduct>::CreateProduct () {
return new TheProduct;
}
使用这个模板,客户仅提供产品类——而不需要创建Creator的子类。
class MyProduct : public Product {
public:
MyProduct ();
// ...
};
standardcreator<MyProduct> mycreator:
- 命名约定
使用命名约定是一个好习惯,它可以清楚地说明你正在使用工厂方法。
例如,Macintosh的应用框架MacApp总是声明那些定义为工厂方法的抽象操作为Class*DoMakeClass(),此处Class是Product类。
10.代码示例
函数CreateMaze(第3章)建造并返回一个迷宫。这个函数存在的一个问题是它对迷宫、房间、门和墙壁的类进行了硬编码。我们将引入工厂方法以使子类可以选择这些构件。首先我们将在MazeGame中定义工厂方法以创建迷宫、房间、墙壁和门对象:
class MazeGame i
public:
Maze* CreateMaze ( ) ;
// factory methods:
virtual Maze* MakeMaze() const
{ return new Maze;}
virtual Room* MakeRoom (int n) const
{ return new Room (n); }
virtual wall*MakeWall(const
{ return new wall;}
virtual Door* MakeDoor(Room* r1; Room* r2) const
{ return new Door(r1, r2);}
};
每一个工厂方法返回一个给定类型的迷宫构件。MazeGame提供一些缺省的实现,它们返回最简单的迷宫、房间、墙壁和门。
现在我们可以用这些工厂方法重写
CreateMaze:Maze* MazeGame: :CreateMaze){
MazeaMaze = MakeMaze();
Room* r1 = MakeRoom(1);
Room* r2 = MakeRoom(2):
Door* theDoor = MakeDoor(r1. r2) ;
aMaze->AddRoon(r1);
aMaze->AddRoom(r2);
r1->setside(North,Makewall());
r1->setside(East, theDoor);
r1->setside(south,Makewall());
r1->setside(west, Makewall());
r2->Setside(North, Makewall ());
r2->setside(East,.Makewall());
r2->Setside(south,Makewall());
r2->setside (west, theDoor);
return aMaze;
}
不同的游戏可以创建MazeGame的子类以特别指明一些迷宫的部件。
MazeGame子类可以重定义一些或所有的工厂方法以指定产品中的变化。
例如,一个BombedMazeGame可以重定义产品Room和Wall以返回爆炸后的变体:
class BombedMazeGame:public MazeGame{
public:
BombedMazeGame();
virtual wall* Makewa11()const
{ return new Bombedwall; }
virtual Room*MakeRoom(int n)const
{ return new RoomWithABomb(n);}
};
一个EnchantedMazeGame变体可以像这样定义:
class EnchantedMazeGame : public MazeGame {
public:
EnchantedMazeGame();
virtual Room*MakeRoom (int n) const
{ return new EnchantedRoom(n,Castspell();}
virtual Door* MakeDoor (Room* r1,Room* r2) const
{ return new DoorNeedingspell(r1,r2);}
protected:
Spell* CastSpell()const ;
);
11.相关模式
Abstract Factory ( 3.1)经常用工厂方法来实现。Abstract Factory模式中动机一节的例子也对Factory Method进行了说明。
工厂方法通常在Template Methods ( 5.10)中被调用。在上面的文档例子中,NewDocument就是一个模板方法。
Prototypes (3.4)不需要创建Creator的子类。但是,它们通常要求一个针对Product类的Initialize操作。Creator使用Initialize来初始化对象。而Factory Method不需要这样的操作。
资料
[1]《设计模式:可复用面向对象软件的基础》(美) Erich Gamma Richard Helm、Ralph Johnson John Vlissides 著 ; 李英军、马晓星、蔡敏、刘建中 等译; 吕建 审校