前段时间,在自己糊里糊涂地写了一年多的代码之后,接手了一坨一个同事的代码。身边很多人包括我自己都在痛骂那些乱糟糟毫无设计可言的代码,我不禁开始深思:自己真的比他高明很多吗?我可以很自信地承认,在代码风格和单元测试上可以完胜,可是设计模式呢?自己平时开始一个project的时候有认真考虑过设计模式吗?答案是并没有,我甚至都数不出有哪些设计模式。于是,我就拿起了这本设计模式黑皮书。
中文版《设计模式:可复用面向对象软件的基础》,译自英文版《Design Patterns: Elements of Reusable Object-Oriented Software》。原书由Erich Gamma, Richard Helm, Ralph Johnson 和John Vlissides合著。这几位作者常被称为“Gang of Four”,即GoF。该书列举了23种主要的设计模式,因此,其他地方经常提到的23种GoF设计模式,就是指本书中提到的这23种设计模式。
把书看完很容易,但是要理解透彻,融汇贯通很难,能够在实际中灵活地选择合适的设计模式运用起来就更是难上加难了。所以,我打算按照本书的组织结构(把23种设计模式分成三大类)写三篇读书笔记,一来自我总结,二来备忘供以后自己翻阅。与此同时,如果能让读者有一定的收获就更棒了。我觉得本书的前言有句话很对,“第一次阅读此书时你可能不会完全理解它,但不必着急,我们在起初编写这本书时也没有完全理解它们!请记住,这不是一本读完一遍就可以束之高阁的书。我们希望你在软件设计过程中反复参阅此书,以获取设计灵感”。
本节介绍创建型模式,它包括抽象工厂模式、生成器模式、工厂方法模式、原型模式和单例模式。创建型模式抽象了实例化过程,它们帮助一个系统独立于如何创建、组合和表示它的那些对象。创建型模式在“什么被创建”、“谁创建它”、“它是怎样被创建的”和“何时创建”这些方面有很大的灵活性。
1. 工厂模式(Simple Factory, Abstract Factory, Factory Method)
其实并没有一个设计模式叫工厂模式,这里我把三种与工厂相关的设计模式放在一起来讲,所以小标题就叫工厂模式了。之所以把它们放在一起,是因为它们有一个共性,就是工厂,都是用来封装对象的创建的。
(1)简单工厂模式
简单工厂模式并不属于GoF的23种设计模式,由于它过于直接,甚至有的地方认为它不是一种真正的设计模式,而是一种编程习惯。
例如,我们要生产一种产品Product
,具体有ProductA
和ProductB
。简单工厂就是将“根据用户指定的类型去生成对应的产品对象的过程”封装起来。一般使用静态方法,这样就不需要创建一个具体的对象去调用生产的工厂方法。
下面以Head First所采用的pizza的例子来说明几种不同的工厂模式。我们首先需要新建Pizza
基类,并在其基础上生成继承的相关的PizzaA
和PizzaB
子类,这里就不提供它们的代码了。而简单工厂模式就是,(如下代码所示)把根据用户指定的类型来看是创建PizzaA
还是PizzaB
子类的过程,封装在SimplePizzaFactory
类中。
public class SimplePizzaFactory
{
public Pizza CreatePizza(string type)
{
Pizza pizza = null;
if(type == "A")
{
pizza = new PizzaA();
} else if(type == "B"){
pizza = new PizzaB();
} else {
throw new ArgumentException($"{type} is not support");
}
return pizza;
}
}
public class PizzaStore
{
SimplePizzaFactory factory;
public PizzaStore(SimplePizzaFactory factory)
{
this.factory = factory;
}
public Pizza OrderPizza(string type)
{
Pizza pizza = this.factory.CreatePizza(type);
pizza.Prepare();
pizza.Bake();
pizza.Cut();
pizza.Box();
return pizza;
}
}
(2)工厂方法模式
工厂方法模式,通过让子类来决定该创建的对象是什么,来达到将对象创建的过程封装的目的。工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个,工厂方法让类把实例化推迟到子类。
工厂方法的结构如下图所示:
上面的简单工厂方法,通过用户输入的type
来判断该生成什么样的产品。如果有多个PizzaStore
,而它们对应的工厂和所需的产品可能不太一样,要同时支持不同Store的需求,我们可以把实例化Pizza
的过程推迟到具体的工厂子类去。
例如,现在有PizzaStore1
和PizzaStore2
两个商店,它们对用户输入的用户类型A和B时可能会创建出适合这两个商店自身的两种产品。而如果生成这两种产品由这两个商店自己决定。我们新建一个抽象类PizzaStore
,并为它定义了一个抽象的工厂方法CreatePizza
,这样任何store在定义的时候都会实现这个抽象工厂。
public abstract class PizzaStore
{
public Pizza OrderPizza(string type)
{
Pizza pizza = CreatePizza(type);
pizza.Prepare();
pizza.Bake();
pizza.Cut();
pizza.Box();
return pizza;
}
protected abstract Pizza CreatePizza(string type);
}
(3)抽象工厂模式
抽象工厂模式,提供一个接口,用于创建相关或者依赖的对象家族,而不需要指明具体的类。其结构如下图所示:
上一节中的共产方法模式让不同的store可以用不同的方式实现不同的工厂方法,这样会过于灵活。如果我们想对不同的工厂进行一定的控制,则上述工厂方法则很难做到。所以我们可以新建一个抽象工厂模式,让工厂方法基于这个抽象工厂模式进行生产,则可以实现统一的控制。抽象工厂模式一般用于用于创建相关或相互依赖的对象家族,例如生产的产品一定要由a, b, c三个部分组成,不同的工厂可以选择这三个部分的不同子类,但是一定要按照这种模式进行组合。这个时候就是抽象工厂模式就上场了。
例如,我们要生产的产品X需要由Xa, Xb, Xc组成,而产品Y则需要由Ya, Yb, Yc组成,所以我们可以定义一个抽象的接口,即生产产品的时候,先生产a, b, c,再把它们组装起来。然后再分别用工厂X和工厂Y去实现这个抽象的接口来生产产品X和产品Y。
// abstract factory
public interface IngredientFactory
{
public PartA CreatePartA();
public PartB CreatePartB();
public PartC CreatePartC();
}
// implement factory X to produce product X
public class ProductXFactory : IngredientFactory
{
public PartA CreatePartA()
{
return new PartXA();
}
public PartB CreatePartB()
{
return new PartXB();
}
public PartC CreatePartC()
{
return new PartXC();
}
}
// implement factory Y to produce product Y
public class ProductYFactory : IngredientFactory
{
public PartA CreatePartA()
{
return new PartYA();
}
public PartB CreatePartB()
{
return new PartYB();
}
public PartC CreatePartC()
{
return new PartYC();
}
}
//
public class Product
{
IngredientFactory factory;
public Product(IngredientFactory factory)
{
this.factory = factory;
}
void Produce()
{
var a = this.factory.CreatePartA();
var b = this.factory.CreatePartB();
var c = this.factory.CreatePartC();
// todo: a+b+c and return the intact product
}
}
下面简单小结一下与工厂相关的设计模式。
简单工厂模式很直观,就是把生产产品对象的部分封装起来。下面主要谈谈工厂方法和抽象工厂之间的区别。实际上抽象工厂模式中也有工厂方法的影子,例如上述例子中我们定义了抽象工厂IngredientFactory
,而在它内部又定义了三个方法,这些方法就是工厂方法。如果你此时觉得“咦,有道理耶”,那么恭喜你,你应该理解了这两种模式的本质。如果你觉得更困惑了,没关系,下面我们再来细看一下他们的区别吧。
(a)工厂方法和抽象工厂的本质都是负责创建对象,但是工厂方法使用的是类,而抽象工厂通过的是对象。我们回到上面创建不同PizzaStore
的例子,为了让不同的store可以生产不同的产品,工厂方法是在抽象的PizzaStore
类中定义了一个CreatePizza()
的抽象方法,这样不同的store会有自己对应的类,而这个类继承自PizzaStore
,然后在该类中需要实现它自己的CreatePizza()
方法。而同样为了解决这个问题,抽象工厂所用的方法则是在PizzaStore
中添加了一个抽象工厂类的对象,这样创建不同的store只需要在构造PizzaStore
类的时候传入不同的抽象工厂类的对象就可以了。
(b)工厂方法被用来创建一个产品,而抽象工厂则被用来创建整个产品家族。所以抽象工厂需要一个很大的接口,一旦需要新增产品则需要修改接口。
(c)应用场景不同。当你要将客户代码从需要实例化的具体类中解耦时,或者你目前还不知道将来需要实例化哪些具体的类时,可以使用工厂方法。而当你需要创建产品家族或者想让制造的相关产品集合起来的时候,可以使用抽象工厂。
2. 单例模式(Singleton)
单例模式可能大家都比较熟悉,就是保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式看起来非常简单(结构图如下所示),但是实现起来还是有很多注意事项需要考虑。
首先简单说说单例模式的优点。知己知彼,才能更好地利用它。很多人包括我自己认为它的作用就是保证唯一的一个实例,实际上它还有一些其他优点。1)对唯一实例的受控访问;2)缩小名空间,它是对全局变量的一种改进,从而避免了那些存储唯一实例的全局变量污染名空间;3)允许对操作和表示的精化,Singleton
类可以有子类,而且用这个扩展类的实例来配置一个应用是很容易的,你可以用你所需要的类的实例在运行时刻配置应用。4)允许可变数目的实例,Singleton
模式是允许你修改为支持多个实例的,可以根据相同的方法来控制实例的数目。5)比类操作更灵活,除了单例模式,另一种封装单件功能的方式是使用类操作(如C++
中的静态成员函数),但是单例模式控制实例的数目比类操作更灵活。
单例模式的具体实现:
(1)保证一个唯一的实例。如何让一个类智能被实例化一次呢?首先,我们把这个类的构造函数声明为私有的,这样就不能在外部用new关键字创建该类的实例的。然后,为了创建出这个类的实例,我们可以在该类的内部定义一个GetInstance()
的方法,这样在GetInstance()
方法内部就可以访问到该类的私有构造函数了。最后,为了保证GetInstance()
方法返回的实例只能是唯一的一个,我们需要再在类中定义个静态的类对象成员,当第一次调用GetInstance()
时为创建一个实例,之后的调用就直接返回该实例即可,这样就能保证唯一性了。
(2)如何处理多线程的情况。上面我们提到在第一次调用GetInstance()
的时候创建实例,但是在多线程的情况下,这个第一次是很难判断和保证的。下面有几个方法来处理这个问题:
(a)将GetInstance()
方法设置为同步方法,这样多线程调用该方法的时候就会排队了,所以就不会出现并发访问的问题了。但是变成同步的方法可能会使得性能下降100倍,如果调用该方法不是很频繁,这样做还是无可厚非的。
(b)不采用延迟实例化的方法。我们直接对静态的数据成员进行初始化
private static Singleton uniqueInstance = new Singleton();
(c)采用双重加锁(double-checked locking)来减少使用同步。
public class Singleton
{
private static volatile Singleton uniqueInstance;
private static object syncRoot = new Object();
private Singleton() {}
public static Singleton Instance
{
get
{
if(uniqueInstance == null)
{
lock(syncRoot)
{
if(uniqueInstance == null)
{
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
}
3. 原型模式(Prototype)
原型模式,是指用原型实例指定创建对象的种类,并且通过拷贝这类原型创建新的对象。有时候我们要创建一系列实例,它们之间大部分成员都是相同的,所以我们希望能够有一个类似于复制粘贴的功能来实现对象的拷贝,原型模式就是做这种事情的。在使用原型模式时,我们首先创建一个原型对象,然后再通过复制该对象创建出更多同类型的对象。
下面是它的结构图:
适用情况:
(1)当一个系统应该独立于它的产品创建、构成和表示时,
(2)当要实例化的类是在运行时刻指定的,例如,通过动态加载,
(3)为了避免创建一个与产品类层次平行的工厂类层次时,
(4)当一个类的实例只能有几个不同状态组合中的一种时,建立相应数目的原型并克隆它们可能比每次用合适的状态手动实例化该类更方便一些。
原型模式的实现非常简单,就是为原型对象提供一个Clone()方法。但是需要注意的是对于原型对象中的引用,我们需要非常谨慎地实现其拷贝(深拷贝还是浅拷贝)。
与抽象工厂一样,原型模式对客户隐藏了具体的产品类,因此减少了客户需要知道的名字的数目,此外它的主要优点如下:
(1)运行时刻增加和删除产品。
(2)改变值以指定新对象。由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
(3)改变结构以指定新对象。原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
(4)减少子类的构造。
(5)用类动态配置应用。
(6)辅助实现撤销操作。可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用,如恢复到某一历史状态。
下面是一个简单的例子。我们需要创建一个迷宫Maze,它由Wall、Room和Door组成,我们可能会写一个抽象工厂来组装这些部分,而在工厂中创建一个迷宫的时候需要new很多的Wall和Room等实例。如果创建这些部件需要很复杂的操作则会使得创建一个Maze非常繁琐,所以我们可以使用Prototype模式为Maze和这些部件提供克隆函数,使得新建它们的实例的时候不再是new一个对象,而是克隆现有的prototype对象。
public class MazeFactory
{
public MazeFactory();
public virtual Maze MakeMaze()
{
return new Maze;
}
public virtual Wall MakeWall()
{
return new Wall;
}
public virtual Room MakeRoom()
{
return new Room;
}
public virtual Door MakeDoor()
{
return new Door;
}
}
public class MazePrototypeFactory : MazeFactory
{
public MazePrototypeFactory(Maze m, Wall w, Room r, Door d);
public virtual Maze MakeMaze()
{
return this.prototypeMaze.Clone();
}
public virtual Wall MakeWall()
{
return this.prototypeWall.Clone();
}
public virtual Room MakeRoom()
{
return this.prototypeRoom.Clone();
}
public virtual Door MakeDoor()
{
return this.prototypeDoor.Clone();
}
private Maze prototypeMaze;
private Wall prototypeWall;
private Room prototypeRoom;
private Door prototypeDoor;
}
4. 生成器模式(Builder)
生成器模式的意图,是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。它一般用来创建包含多个组成部分的较复杂对象,它将客户端与复杂对象的创建过程分离,使得客户端只需知道所需建造者的类型。它关注如何一步一步创建一个的复杂对象,不同的具体建造者定义了不同的创建过程,且具体建造者相互独立,增加新的建造者非常方便,无须修改已有代码,系统具有较好的扩展性。
生成器模式的结构:
适用情况:
(1)当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
(2)当构造过程必须允许被构造的对象有不同的表示时。
生成器和抽象工厂都可以用来创建复杂对象,它们之间的主要区别是,前者着重于一步步构造一个复杂对象,而后者着重于多个系列的产品对象。
生成器的主要优点有:
(1)它使你可以改变一个产品的内部表示。Builder对象提供给Director一个构造产品的抽象接口,该接口使得生成器可以隐藏这个产品的表示和内部结构,以及具体的装配方式。
(2)它将构造代码和表示代码分开。
(3)它使你可对构造过程进行更精细的控制。
我们还是利用上一小节中构建迷宫的例子来简单介绍生成器模式。对应着上面的结构图来看,迷宫就是我们最终的Product。接下来我们为生成Maze创建一个Builder接口MazeBuilder,然后可以在MazeBuilder的基础上实现不同的创建Maze的子类。最后Director要做的事情就是调用MazeBuilder提供的接口利用用户提供的具体参数来生成迷宫。
public interface MazeBuilder
{
void BuildMaze();
void BuildRoom(int roomNo);
void BuildDoor(int roomFrom, int roomTo);
Maze GetMaze();
}
public class StandardMazeBuilder : MazeBuilder
{
private Maze currentMaze = new Maze();
public override void BuildMaze()
{
currentMaze = new Maze();
}
public override void BuildRoom(int roomNo)
{
if(!currentMaze.ContainsRoom(roomNo))
{
var room = new Room(roomNo);
currentMaze.AddRoom(room);
}
}
public override void BuildDoor(int roomFrom, int roomTo)
{
if(currentMaze.ContainsRoom(roomFrom) && currentMaze.ContainsRoom(roomTo))
{
var door = new Door(roomFrom, roomTo);
currentMaze.AddDoor(door);
}
}
public override Maze GetMaze()
{
return currentMaze;
}
}
5. 小结
用一个系统创建的那些对象的类对系统进行参数化有两种常用方法。一种时生成创建对象的类的子类,对应的设计模式有工厂方法模式。另一种方法则更多的依赖于对象复合,对应的设计模式有抽象工厂、生成器和原型模式。所有这三个模式都涉及到创建一个新的复杂创建产品对象的“工厂对象”。抽象工厂由这个工厂对象产生多个类的对象;生成器由这个工厂对象使用一个相对复杂的协议,逐步创建一个复杂产品。原型模式由该工厂对象通过拷贝原型对象来创建产品对象。