一 目的
定义一个创建对象的接口,但是让他的子类去决定初始化哪种类型。工厂方法使得一个类能够推迟到他的子类去初始化。
二 动机
框架运用抽象类来定义和维护对象之间的关系。一个框架经常负责这些对象的创建。考虑一些这么一个情况:一个能够展现多个文档的应用程序的框架。在这个框架中有两个关键的抽象,一个是应用程序,一个是文档。两个类都是抽象的,客户端代码必须子类化他们,从而完成特定程序的实现。例如,去创建一个画图应用程序,我们会定义类似 DrawingApplication 和 DrawingDocument 这两个类。这个应用程序类负责管理文档,当有请求的时候,并且创建他们,比如当用户选择打开或者新建菜单时。
因为这个需要初始化的特别的文档子类是由应用程序指定的,所以 Application 这个类不能够指定哪种文档的子类需要初始化。这个 Applicaiton 类只知道什么时候去初始化一个文档,但是不知道是什么种类的文档。这里有一个困境:框架必须初始化类,但是他只知道一些它不能够初始化的抽象类。
工程方法提供了一个解决方法。他封装了哪个文档子类应该创建的信息,并且把这些信息从框架中移出去。
Application 的子类重新定义了CreateDocument这个接口,返回一个合适的文档子类对象。一旦一个Application 的子类被初始化,他就可以在不知道他们具体哪个类的情况下,初始化应用程序特定的文档。我们称CreateDocument为一个工程方法,因为它负责创建一个对象。
三 应用
当有以下情况的时候,可以考虑使用工厂方法;
1 一个类不能够预期知道它必须创建的对象的类;
2 一个类想让它的子类来指定它需要创建的对象;
3 有些类需要委托一些职责给他的一个帮助类型的子类,然后你想本地化那些被委托的帮助子类的信息。
四 结构
五 参与者
product(Document)
定义了工厂方法创建的对象的接口。
ConCreteProduct(MyDocument)
实现了 Product 的接口。
Creator(Application)
-- 声明了返回产品对象的工厂方法。创建者可以定义一个默认的工厂方法。这个工厂方法返回一个默认的具体产品对象。
-- 可以调用工厂方法来创建一个产品对象。
ConCreteCreator(MyApplication)
--重写了工厂方法,返回一个具体产品对象的实例。
六 合作
创建者依赖它的子类来定义工厂方法,从而使它能够返回一个合适的具体产品的实例。
七 影响
工厂方法忽略了把特定程序的类绑定到你的代码中的需求。代码只是处理产品接口,因此它能够和任何用户定义的类型一起工作。
一个潜在的缺点是为了创建一个特定的具体产品对象,客户端代码必须子类化创建者类。不管怎样,子类化在客户端必须要初始化创建者类的时候是很好的,但是客户端代码必须处理另外的一些评估事项。
以下是两个工厂方法模式额外的后果。
1 为子类提供钩子。用工厂方法在一个类中创建一个对象总是比直接创建一个对象更加灵活。工厂方法给子类一个钩子用来提供创建对象的可扩展版本。
在文档的例子中,文档类能够定义一个叫做CreateFileDialog的工厂方法,来创建一个为了默认的文件对话框用来打开一个已经存在的文档。一个文档的子类能够重新定义特定程序的文件对话框。在这种情况下,工厂方法不是抽象的,它可以提供一个合理的默认实现。
2 连接并行的类层次。在我们目前所考虑的例子中,工厂方法只被创建者调用。但这个并不是固定一成不变的。客户端代码会发现工厂方法很有用,特别是在并行的类层次中。
当一个类委托一些它自己的职责给一个单独的类时会产生并行的类层次。考虑到一个图形数据,它们可以被我们交互操作,也就是,他们可以被我们用鼠标拉伸,移动,旋转。实现这些交互不不是很容易。它常常要求我们存储和更新信息。这些信息记录了某个时刻的操作状态。因此,它并不需要被保存在这些图像对象中。更重要的是,当用户操作他们时,不同的图像的行为各异。例如,拉伸一条直线会产生移动末端节点的效果,二拉伸一个文本图形会改变它所占的行空间。
在这些限制下,用一个操作对象来实现交互和跟踪任何特定操作状态会更加好。不同的图形会用不同的操作对象的子类去处理某种特别的交互。那么操作结果的类层次和图形类层次并行,如下图:
这个图形类提供了一个叫 CreateManipulator的工厂方法,它能让客户端代码创建一个和图形相对应的操作对象。图形子类重写这个工厂方法,返回一个与他们相对应的操作对象的实例回来。另外,这个图形类可以实现CreateManipulator,返回一个默认的操作对象实例,图形子类可以简单的继承这个默认实现。这些图形类并不一定要相应的操作子类,因为这个层次可以部分并行。
我们需要注意工厂方法是怎样去把这两个类的关系连接起来的。它保留了哪些类需要放在一起的信息。
八 实现
当应用工厂方法时,需要考虑以下几个问题。
1 两个主要的变种。(1)一种情况是当这个创建者类是一个抽象类,并且不提供它声明的工厂方法的实现(2)第二种情况是当这个创建者是一个具体的类,并且提供了它声明的工厂方法的默认实现。也有可能抽象类中提供了工厂方法的实现,但是这个并不常见。
第一种情况要求子类去定义一个实现,因为没有一个合理的实现。这样逃避了不得不初始化不可预见的类的困境。第二种情况,具体的创建者为了灵活性而优先使用工厂方法。它遵循如下规则:“在一个单独的操作中创建一个对象,这样可以使得它的子类能够重写它”。这个规则保证了在需要的情况下,子类的设计者可以改变他们的父类初始的化的对象。
2 参数化工厂方法。另外一个工厂方法的版本是让工厂方法创建多种产品。工厂方法的参数标示了将要被创建对象的种类。所有这个工厂方法要创建的对象共享一个产品接口。在文档的例子中,Application 会支持不同种类的文档。你传入一个参数给CreateDocument 用来指定要创建的文档的种类。
一个有参数的工厂方法一般有如下的形式,MyProduct 和 YourProduct 是 Pruduct 的子类。
package com.hermeslch.pattern;
enum ProductId {
MINE,YOURS,THEIRS
}
public class Creator {
public Product Create(ProductId id){
if(id == ProductId.MINE) return new MyProduct();
if(id == ProductId.YOURS) return new YourProduct();
return null;
}
}
重写一个含参的工厂方法使得你能偶轻易和有选择性的扩展或者改变创建者创建的产品。你可以为新的产品增加新的标识符,或者你能够用已经存在的标识符创建不同的产品,如下代码:
class MyCreator{
public Product Create(ProductId id){
if(id == ProductId.MINE) return new YourProduct();
if(id == ProductId.YOURS) return new MyProduct();
if(id == ProductId.THEIRS) return new TheirProduct();
return Creator::Create(id);
}
}
注意到最后一步操作是调用父类的Create()。这是因为MyCreator::Creator 只处理 YOURS,MINE,THEIR,只有这三种是和父类不相同的,它对其他的类型不感兴趣。因此MyCreator 扩展了创建的类型。它延迟了创建所有种类的产品,只是创建一些。
3 由于实现语言的不同,会导致工厂方法有不同的变种和其他问题。这里不做详细翻译和描述了。比如,java,C++,SmallTalk 的实现方法有些稍微的区别。
4 可以使用模板来避免继承。我们提到过,另一个工厂方法潜在的问题是为了创建一个合适的产品对象,会使你不得不子类化。另外一个避免这个问题的方式是提供一个模板给Creator的子类。
5 名字约定。使用名字约定可以使得你明确表示你在使用工厂方法。比如MacApp 总是这样写函数 Class doMakeClass(),这个Class 就是产品的类名。
九 例子代码
函数 CreateMaze() 创建和返回一个迷宫。有一个问题就是这个函数里面硬编码了Maze,rooms,doors和其他的walls.我们这里介绍工厂方法,使得子类来选择这些组件。
首先,我们将在MazeGame中定义工厂方法来创建maze,room,wall,和door这些对象。
package com.hermeslch.pattern;
import org.junit.Test;
public class MazeGame {
public Maze makeMaze(){
return new Maze();
}
public Room makeRoom(int index){
return new Room(index);
}
public Wall makeWall(){
return new Wall();
}
public Door makeDoor(Room r1,Room r2){
return new Door(r1,r2);
}
}
每个工厂方法返回一个给定的迷宫组件。MazeGame提供一个默认的实现。他们返回墙壁,迷宫,门,房间的最简单的实现。现在我们可以重新写CreateMaze来使用这些工厂方法。
public Maze CreateMaze(MazeFactory factory){
Maze maze = makeMaze();
Room r1 = makeRoom(1);
Room r2 = makeRoom(2);
Door aDoor = makeDoor(r1, r2);
maze.AddRoom(r1);
maze.AddRoom(r2);
r1.setSide(Direction.North,makeWall());
r1.setSide(Direction.East,aDoor);
r1.setSide(Direction.South,makeWall());
r1.setSide(Direction.West,makeWall());
r2.setSide(Direction.North,makeWall());
r2.setSide(Direction.East,makeWall());
r2.setSide(Direction.South,makeWall());
r2.setSide(Direction.West,aDoor);
return maze;
}
不同的游戏可以子类化MazeGame ,从而来指定迷宫的一些局部特点。MazeGame的子类可以重新定义一些或者所有的工厂方法,并且指定某种特别的产品变种。例如:一个BomedMazeGame 能够重新定义房间和墙壁产品对象。
Class BombedMazeGame : public MazeGame{
public BomedMazeGame();
public Wall MakeWall(){
return new BombedWall();
}
public Wall MakeRoom(int n){
return new BombedRoom(n);
}
}
一个 EnchantedMazeGame 也许会定义成这样:
public class EnchantedMazeGame extends MazeGame{
public EnchantedMazeGame();
public Wall MakeDoor(Room r1,Room r2){
return new DoorNeedingSpell(r1,r2);
}
public Wall MakeRoom(int n){
return new EnchantedRoom(n,new CastSpell());
}
}