GOF《Design Patterns》第三章
*GOF巨作《Design Patterns》毫无疑问是设计模式的圣经,然而“从风格上讲,该书与其说是为学习者而写作的教程范本,还不如说是给学术界人士看的学术报告,严谨有余,生动不足。”〔孟岩〕本系列将《Design Patterns》中文版(结合英文版)中重要句子按句解析,作为自学笔记也给新接触设计模式的朋友一点借鉴。文中原文以粗体标出。我自己不明白的地方以〔TODO:〕标出,希望高手多多指点。
创建型模式抽象了实例化过程。两个问题:
-
何谓实例化过程?
-
什么叫做抽象?
所谓实例化过程,说白了就是创建对象的过程。在面向对象语言中,一般是指通过构造器(constructor)创建对象的过程。在Java和C#中通常用new这个关键字来创建对象,当然还有通过反射的方法,后者用的较少。一个对象实例化结束后并不一定就可以使用了,对于复杂的对象有时候还需要做一些装配,有时候也叫做初始化。后面我们说到实例化没有特殊说明,就是指的用new这个关键创建对象。[e.g instance = new SomeConcreteClass();]
这个过程如何抽象呢?所谓抽象就是掩盖具体的东西,这里面那个东西是具体的概念呢?显然是构造器,也就是SomeConcreteClass这个类。这是一个具体类,当我们的代码里面出现具体类的时候,我们就依赖了具体实现,这违背了DIP原则。抽象就是客户端只了解自己需要某个实例,但是不了解改实例的创建过程,或者说不了解其构造函数的签名(包括构造器的名字即类名和构造参数)。
它们帮助一个系统独立于如何创建、组合和表示它的那些对象。原文是:They help make a system independent of how it’s objects created, composed and represented.这句话说明创建型模式的作用是使得系统独立于系统中对象的创建,组合和表示。从这一点上看,上一句说抽象了实例化过程中的实例化过程是包含装配过程(组合)的。创建型模式帮助系统独立于其对象的创建和组合是容易理解的。比如Factory Method通过一个返回接口的方法来返回对象实例,客户端并不了解对象的创建过程;Builder模式隔离了对象的各个部分的创建和其组合过程。那么独立于表示如何理解呢?“表示”在这里可以理解为对同一个接口的不同实现。
[e.g. (感谢Lenny Primark)
List<Number> numbers = numbersFactory.createNumbers(5, 10); // create five numbers with values of 10
工厂方法返回的实例可能是如下的任何一个“表示”:
-
it can give you LinkedList<Number> or ArrayList<Number>
-
it can give you List<IntegerNumber> or List<DoubleNumber>
-
or any other combination.
创建型模式返回给你的是借口,而实现和实际数据的表示细节则被隐藏了。]
一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。
随着系统演化得越来越依赖与对象复合而不是类继承,创建型模式变得更为重要。首先给我们指明了演化的方向,越来越依赖于对象复合而不是继承,然后在这个前提下,应用创建型模式,我想这是鼓励对象创建型模式。
当这种情况发生时,重心从对一组固定行为的硬编码(hard-coding)转移为定义一个较小的基本行为集,这些行为可以被组合成任意数目的更复杂的行为。类继承实际上是“静态的”固定的行为,是在编译期确定了的行为,而对象复合追求的是通过较小的基本行为集组合成复杂的行为,通常是支持运行时改变的组合方式的。
这样创建有特定行为的对象要求的不仅仅是实例化一个类。即,还有可能要负责装配过程。
在这些模式中有两个不断出现的主旋律。第一,它们都将关于该系统使用哪些具体的类的信息封装起来。第二,它们隐藏了这些类的实例是如何被创建和放在一起的。第一个里面封装也就是说,客户代码不了解自己使用的具体类是什么。第二个就很明白了,但是与第一段中相比,没有提到“represented”(表示)这个意思。
整个系统关于这些对象所知道的是有抽象类所定义的接口。这里整个系统实际上说的是客户代码,整个系统如果包含具体类在内当然要了解具体类了。
因此创建型模式在什么被创建,谁创建它,它是怎样被创建的,已经何时创建这些方面给予你很大的灵活性。什么被创建:通过产品接口隐藏具体类。谁创建它:通过工厂接口隐藏具体工厂。它是怎样被创建:Builder抽象方法隐藏创建过程。何时创建:单件模式隐藏创建时机。〔TODO:这里还要加上其他几个模式分别封装什么。〕
它们允许你用结构和功能差别很大的“产品”对象配置一个系统。配置可以是静态的(即在编译时指定),也可以是动态的(在运行时)。结构和功能差别很大的“产品”应该实现相同的接口。
有时创建型模式是相互竞争的。例如,在有些情况下Prototype或Abstract Factory用起来都很好。而在另外一些情况下它们是互补的:Builder可以用其他模式去实现某个构件的创建。Prototype可以在它的实现中使用Singleton。
接下来作者举了一个例子,这里不重述了。但是还是有几点需要注意。
作者把Enter这个方法放入MapSite中,而不是某个叫做Player的类中。这里面包含了很多的智慧和经验。当然我不排除Player类中应该包含一个类似与Move之类的方法,但是这个方法应该调用Mapsite的Enter方法。试想,对于Move这个动作和其响应如果全部放在Player这个类中完成将是如下的效果:
[
public void Move(Direction d)
{
if(currentRoom.GetSide(d) is Room){}
else if (currentRoom.GetSide(d) is Wall){}
else if (currentRoom.GetSide(d) is Door)
{
if (door.isOpen){}
else{}
}
}
] 毫无疑问这种方式的可读性,扩展性都很差。Enter为更复杂的游戏操作提供了一个简单基础。在一个真正的游戏中,Enter可以将游戏者对象作为一个参数。
作者在给出CreateMaze方法之后的评价,考虑到这个函数所做的仅是创建一个有两个房间的迷宫,它是相当复杂的。这个复杂性是因为客户端负责了太多的职能。Room的构造器可以提前用墙壁来初始化房间的每一面。这样会使得客户端代码稍微简单一些。这个成员函数真正的问题不在于它的大小而在于它不灵活。
创建型模式显示如何使得这个设计更灵活,但未必会更小。所以使用模式的时候,要抛弃这个观点,认为好的设计是小的设计,而是灵活的可扩展的设计。
当前的设计不易扩展,其最大的障碍是对被实例化的类进行硬编码。在本章导论部分的最后,作者给出了创建型模式的解决方案。
-
Factory Method:CreateMaze调用虚函数而不是通过new来创建Room,Wall和Door。Factory Method实际上简单的令你惊讶!任何时候,当你存在一个虚拟的(抽象的)函数其返回值是一个接口(抽象类),你就在使用Factory Method了。(插一句,理解一个模式的简单性有时候比理解其复杂性更重要,很多人对Factory Method模式敬而远之,就是没有理解其简单性。)建议您参考一下我的另一篇文章:《没有Factory这个模式》。
-
Abstract Factory:CreateMaze不是调用自己的虚函数,而是从外界传入一个对象(抽象工厂)给它。CreateMaze方法调用这个对象的方法(也是虚方法)来创建Room,Wall和Door。
-
Builder:(TODO:等我看完了再补充:)
-
Prototype:(TODO:等我看完了再补充:)
-
Singleton:保证每个游戏中仅有一个迷宫,而且所有的对象都可以迅速访问它――二不需要求助于全局变量或函数。Singleton也使得迷宫易于扩展或者替换,且不需要变动已有代码。Singleton如此受到争议,我就不多说了。作者在最后加一句“易于扩展或者替换”,还强调“不需要变动已有代码”确实让人不解。(TODO:哪位高手出来解释一下?)