这个题目很奇怪,设计模式不就是指导程序员进行软件设计的么?呃,我的意思是这是与 面向“对象”的设计模式相对的。以前有看过有人写过《给妻子解释设计模式》,这种把计算机中的思想与生活相结合固然很好。人类解决问题的思想是相通的,计算机中的很多思想比如分治、递归、贪心等都能在生活中找到相似的例子。但是,不恰当的例子反而会误导人的思维,造成理解偏差。设计模式本就是类与类之间如何组织的一套软件设计经验,我们拿软件开发中的实例来讲解,会更加准确。
写在前面:
为什么要学设计模式?
很赞同《大话设计模式》一书作者所言:重要的不是你将来会不会用到这些模式,而是通过这些模式让你找到“封装变化”、“对象间松散耦合”、“针对接口编程”的感觉,从而设计出易维护、易扩展、易复用、灵活性好的程序。
以前读过《HeadFirst 设计模式》这本书对于小白入门很好,但是其中很多例子不符合国人的习惯,有些地方读起来很别扭(比如:木鸭子,披萨连锁店--这个例子太拖沓了!等)当然也有些很绝妙的例子(比如:星巴克咖啡)
还有秦小波的《设计模式之禅》,我觉得此书是国内原创技术书中较好的,但也有些例子不太合适(比如:女娲造人)。
这些设计模式的书为了通俗易懂,尽量以我们生活中的例子为例来讲设计模式,但有时候不恰当的例子会造成我们的理解偏差。
还曾借阅过《大话设计模式》,此书的写作风格轻快(但我一点也不觉得风趣幽默),个人很不喜欢,而且不少例子比较牵强。(尤其是"学雷锋"的例子,还有"姚明NBA""计划生育"等例)
对于有一定编程经验的程序员来说,大可不必如此,以编程中的实际问题为例,更加准确恰当。
GoF的《设计模式》就是设计模式的始祖、经典教科书。其描述严谨,举例恰当,分类组织,是应该反复细读的经典。
网上写设计模式的文章已经很多了,但是针对GoF《设计模式》的笔记总结,写的认真、精炼的很少。
此系列文章,是GoF《设计模式》的读书笔记,从中提炼出其精华,掺杂了一些个人的理解和其他书中恰当的例子,可供熟悉设计模式的朋友查阅复习。
欢迎大家一起探讨。
第0部分
面向对象的软件开发过程 :找到相关的对象,以适当的粒度将它们归类,再定义类的接口和继承层次,建立对象之间的基本关系。
你的设计应该对手头的问题有针对性,同时对将来的问题和需求也要有足够的通用性。(拥抱变化)
避免重复设计或尽可能少做重复设计。
什么是设计模式?
设计模式是对被用来在特定场景下解决一般设计问题的类和相互通信的对象的描述。(也就是如何组织类之间的关系)
设计模式确定了所包含的类和实例,它们的角色、协作方式以及职责分配。
客户请求(即调用对象的方法)是使对象执行操作的唯一方法,操作又是对象改变内部数据的唯一方法。(类与类之间的交互是通过调用对方的方法来实现的。)
面向对象设计最困难的部分是将系统分解成对象集合。因为要考虑许多因素:封装、粒度、依赖关系、灵活性、性能、演化、复用等,它们都影响着系统的分解,并且这些因素通常还是互相冲突的。
设计的许多对象来源于现实世界的分析模型。但是,设计结果所得到的类通常在现实世界中并不存在。例如,描述过程或算法的对象现实中并不存在,但它们却是设计的关键部分。
设计模式帮你确定并不明显的抽象和描述这些抽象的对象。
接口
在面向对象系统中,接口是基本的组成部分。对象只有通过它们的接口才能与外部交流,如果不通过对象的接口就无法知道对象的任何事情,也无法请求对象做任何事情。对象接口与其功能实现是完全分离的,不同对象可以对请求做不同的实现,也就是说,两个有相同接口的对象可以有完全不同的实现。
(面向过程的语言是接口与功能是绑定的,一个接口(函数名)对应一种功能。)
当给对象发送请求时,所引起的具体操作既与请求本身有关又与接受对象有关。
动态绑定允许你在运行时刻彼此替换有相同接口的对象。这种可替换性就称为多态(polymorphism)。
类指定了对象的内部数据和表示,也定义了对象所能完成的操作。
抽象类(abstract class)的主要目的是为它的子类定义公共接口。
1、类继承与接口继承的比较
理解对象的类(class)与对象的类型(type)之间的差别非常重要。
一个对象的类定义了对象是怎样实现的,同时也定义了对象的内部状态和操作的实现。但是对象的类型只与它的接口有关,接口即对象能响应的请求的集合。
一个对象可以有多个类型(有多个接口),不同类的对象可以有相同的类型(有相同的基类)。
当然,对象的类和类型是有紧密关系的。因为类定义了对象所能执行的操作,也定义了对象的类型。C++语言的类既指定对象的类型又指定对象的实现。(不像Java有专门的)
理解类继承和接口继承之间的差别也十分重要。
类继承根据一个对象的实现定义了另一个对象的实现。它是代码和表示的共享机制。
接口继承描述了一个对象什么时候能被用来替代另一个对象。(在C++中,类继承与接口继承在语法上的区别就是:基类是不是虚基类)
因为许多语言并不显式地区分这两个概念,所以容易被混淆。在C++中,继承既指接口的继承又指实现的继承。C++中接口继承的标准方法是公有继承一个含(纯)虚成员函数的类。C++中纯接口继承接近于公有继承纯抽象类,纯类继承接近于私有继承。
尽管大部分语言并不区分接口继承和实现继承的差别,但使用中人们还是分别对待它们的。
很多设计模式依赖于这种差别,例如,责任链模式中的对象必须有一个公共的类型,但一般情况下它们不具有公共的实现。
2、对接口编程而不是对实现编程
类继承是一个通过复用父类功能而扩展应用功能的基本机制。它允许你根据旧对象快速定义新对象。
当继承被恰当使用时,所有从抽象类导出的类将共享该抽象类的接口。这意味着子类仅仅添加或重定义操作,而没有隐藏父类的操作。这时,所有的子类都能响应抽象类接口中的请求。
只根据抽象类中定义的接口来操纵对象有以下好处:
1、 客户无需知道他们使用对象的特定类型,只须对象有客户所期望的接口。
2、 客户无需知道他们使用对象是用什么来实现的,他们只须知道定义接口的抽象类。
这将及大地减少子系统实现之间的互相依赖关系。
面向对象设计的原则:针对接口编程,而不是针对实现编程。
不将变量声明为某个特定的具体类的实例对象,而是让它遵从抽象类所定义的接口。
当你不得不在系统的某个地方实例化具体的类(即指定一个特定的实现)时,创建型模式(AbstractFactory, Builder, Factory Method, Prototype, Singleton)可以帮你。通过抽象对象的创建过程,这些模式提供不同方式以在实例化时建立接口和实现的透明连接。
创建型模式确保你的系统是采用针对接口的方式书写的,而不是针对实现而书写的。
运用复用机制
理解对象、接口、类和继承之类的概念对大多数人来说并不难,问题的关键在于如何运用它们写出灵活的、可复用的软件。设计模式将告诉你怎样去做。
1、继承和组合的比较
面向对象系统中功能复用的两种最常用技术是类继承和对象组合。
类继承:
允许你根据其他类的实现来定义一个类的实现。(此被称为“白箱复用”,在继承方式中,父类的内部细节对子类可见。破坏了封装性)
类继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现细节。如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更合适的类替换。
且子类中的实现与它的父类有如此紧密的依赖关系,以至于父类实现中的任何变化必然导致子类发生变化。
这种依赖关系限制了灵活性并最终限制了复用性。一个可用的解决方案是只继承抽象类(接口继承),因为抽象类通常提供较少的实现。
对象组合:
新的更复杂的功能可以通过组装或组合对象来获得。(此被称为“黑箱复用”,对象的内部细节是不可见的。)
对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。因为对象只能通过接口访问,所以我们并不破坏封装性。只要类型一致,运行时刻还可以用一个对象来替代另一个对象。
优先使用对象组合有助于保持每个类被封装,并被集中在单个任务上。且系统的行为将依赖于对象间的关系而不是被定义在某个类中。
面向对象设计的第二个原则:优先使用对象组合,而不是类继承。
2、委托
委托(delegation)是一种组合方法,它使组合具有与继承同样的复用能力。
在委托方式下,有两个对象参与处理一个请求,接受请求的对象将操作委托给它的代理者。
这类似于子类将请求交给它的父类处理。使用继承时,被继承的操作总能引用接受请求的对象。(即用父类的方法来处理子类对象)
委托方式为了得到同样的效果,接受请求的对象将自己传给代理人,使代理人的操作可以引用接受请求的对象。
【例】
我们可以在窗口类中保存一个矩形类的实例变量来代理矩形类的特定操作,这样窗口类可以复用矩形类的操作,而不必像继承时那样定义成矩形类的子类。一个窗口拥有一个矩形,而不是一个窗口就是一个矩形。 窗口现在必须显式的将请求转发给它的矩形实例,而不是像以前它必须继承矩形的操作。
下面的图显示了窗口类Window将它的Area操作委托给一个矩形实例:
(箭头线表示一个类对另一个类实例的引用关系。引用名是可选的,本例为"rectangle")
委托的优点是:它便于运行时刻组合对象操作以及改变这些操作的组合方式。
(假定矩形对象和圆对象有相同类型,我们只需简单的用圆对象替换矩形对象,则得到的窗口就是圆形的。)
有一些模式使用了委托,比如Stata、Strategy和Visitor。在Strategy模式中,一个对象将一个特定的请求委托给一个描述执行策略的对象,它是通过改变代理者来改变对象的行为。
委托是对象组合的特例(用到了转发)。它告诉你对象组合作为一个代码复用机制可以替代继承。
3、继承和参数化类型(模板)的比较
另一种功能复用技术(并非严格的面向对象)是参数化类型(即模板)。 它允许你在定义一个类时,并不指定该类所用到的其他所有类型。未指定的类型在使用时以参数形式提供。
模板给我们提供了类继承和对象组合外的第三种方法来组合面向对象系统中的行为。许多设计可以使用这三种技术中的任何一种来实现。
模板允许你改变所用到的类型,但继承和模板都不能在运行时刻改变。对象组合技术可以在运行时刻改变组合行为,但是它存在间接性,比较低效。
--------------
【附】依赖、关联、聚合和组合之间的区别
- 依赖(Dependency) 关系是类与类之间的联接。依赖关系表示一个类依赖于另一个类的定义。例如,一个人(Person)可以买车(car)和房子(House),Person类依赖于Car类和House类的定义,因为Person类引用了Car和House。与关联不同的是,Person类里并没有Car和House类型的属性,Car和House的实例是以参量的方式传入到buy()方法中去的。一般而言,依赖关系在Java语言中体现为局域变量、方法的形参,或者对静态方法的调用。
- 关联(Association)关系是类与类之间的联接,它使一个类知道另一个类的属性和方法。关联可以是双向的,也可以是单向的。在Java语言中,关联关系一般使用成员变量来实现。
- 聚合(Aggregation) 关系是关联关系的一种,是强的关联关系。聚合是整体和个体之间的关系。例如,汽车类与引擎类、轮胎类,以及其它的零件类之间的关系便整体和个体的关系。与关联关系一样,聚合关系也是通过实例变量实现的。但是关联关系所涉及的两个类是处在同一层次上的,而在聚合关系中,两个类是处在不平等层次上的,一个代表整体,另一个代表部分。
- 组合(Composition) 关系是关联关系的一种,是比聚合关系强的关系。它要求普通的聚合关系中代表整体的对象负责代表部分对象的生命周期,组合关系是不能共享的。代表整体的对象需要负责保持部分对象和存活,在一些情况下将负责代表部分的对象湮灭掉。代表整体的对象可以将代表部分的对象传递给另一个对象,由后者负责此对象的生命周期。换言之,代表部分的对象在每一个时刻只能与一个对象发生组合关系,由后者排他地负责生命周期。部分和整体的生命周期一样。(比如:人与心脏的关系,心脏没了,人也就没了。by-yang)
——摘自《Java面向对象编程》
UML中对象的关系都是在相应的软件环境或实际场景下定义的,这里区别聚合和组合的关系,关键还是在于它们之中整体和部分的关系强、弱,以及它们之间的依附关系。
在C++中,在语法层面,关联、聚合、组合是没有什么区别的,它们是由你的意图而不是由显式的语言机制决定的。
--------------
组织编目
设计模式在粒度和抽象层次上各不相同。由于存在众多的设计模式,我们希望用一种方式将它们组织起来。
范围\目的 | Creational (创建型) | Structural (结构型) | Behavioural (行为型) |
类 | Simple Factory | Adapter (class) | Interpreter |
Factory Method |
| Template method | |
对象 | Abstract Factory | Adapter (object) | Chain of Responsibility |
Builder | Bridge | Command | |
Prototype | Composite | Iterator | |
Singleton | Decorator | Mediator | |
| Facade | Memento | |
| Flyweight | Observer | |
| Proxy | State | |
|
| Strategy | |
|
| Visitor |
我们在两个维度上对模式进行分类。
第一是目的准则。即模式是用来完成什么工作的。模式依据其目的可分为创建型、结构型、行为型。
创建型模式与对象的创建有关;结构型模式处理类或对象的组合;行为型模式对类或对象怎样交互和怎样分配职责进行描述。
第二是范围准则。即模式主要是用于类还是用于对象。类模式处理类和子类之间的关系,这些关系通过继承建立,是静态的,在编译时刻便确定下来了。对象模式处理对象间的关系,这些关系在运行时刻是可以变化的,更具动态性。
创建型类模式,将对象的部分创建工作延迟到子类,而创建型对象模式则将它延迟到另一个对象中。
结构型类模式,使用继承机制来组合类,而结构型对象模式则描述了对象的组装方式。
行为型类模式,使用继承描述算法和控制流,而行为型对象模式则描述一组对象怎样协作完成单个对象所无法完成的任务。
封装变化,是许多设计模式的主题。