设计模式解析第二版 俆言声译
Design Pattern Explained: A New Perspective on Object-Oriented Design, Second Editon. By Alan Shallway and James R. Trott
——书非借不能读也
2013年11月19日06:00:01
前言
2013年11月19日06:09:45
如何使用共性与变性分析来设计应用程序的框架
测试是高质量编译的一个优先原则
使用工厂实例化与管理对象至关重要
对象正在的威力并不在继承,而在于自封装。
多设计模式协同工作才能帮助我们创建更加健壮的应用程序。
从面向对象到模式再到真正的面向对象:学习模式本身,再学习模式背后的思想,再将理解扩展到分析与测试领域,也扩展到学习模式与敏捷编程方法之中
接口编译(designing to interface),一个简单的理解、使用原则是:一个对象只知道一个对象的接口,不需知道另一个对象的具体情况与就可以使用它
不能将设计模式作为一个单独的东西使用,应该把它们结合起来使用,这样才能做出好的设计:模式应该相互配合,共同解决问题。
Christopher Alexander的奇书The Timeless Way of Building告诉我们所有层次(分析、设计、实现)都存在模式。模式有助于理解问题域
在问题域中尝试创建类,然后再将它们结合成一个系统,是Alexander认为是非常糟糕的方法。好的方式后面会介绍
通过学习设计模式,可以掌握基本的面向对象设计技术,能加深对面向对象中三大概念(封装、继承、多态)的理解
面向对象的设计:设计模式从分析到实现
作者:经曾以为设计模式之下所隐藏的执导性原则与策略在学习过一些设计模式之后已经非常清楚了,但设计模式所写时的一些背景知识对Smaltalk社区来说是根深蒂固的,但对其他人则未必,要求你在读*的设计模式之前对面向对象范型有很深的理解。当我将*的设计模式与Alexander的工作,Jim Coplien的共性与可变性分析的工作,Martin Fowler的方法学与设计模式的工作结合起来之后,那些指导性的原则才变得足够清晰。
极限编程,测试驱动开发TDD及Scrum的经验与设计模式也有极大的联系
无论在何种情况之下,都可以使用设计模式的指导性原则与策略来推导出几个设计模式,使用这些来得出设计模式更是一种思考过程的锻炼,使用这些来阐述设计模式及阅读*的设计模式书会有更深的理解,使用这些做出的设计大多数已经包含模式的精要在里面了,不知道具体的设计模式的名称也无关紧要,使用这些对面向对象分析和设计、对正确理解面向对象的原则都有好处
学习本书你将学习到模式为何有效,以及如何协同工作,以及模式背后的原则与策略
基于模式的分析能够使我们成为更加高效和有效的分析人员,因为它使我们可以更加抽象的处理模型,因它它代表了许多其他分析人员的集体经验;模式有助于人们理解面向对象的原理,并有助于我们处理对象的方式
基于模式的分析与设计:一个你还未知的领域,有空可以找一些书籍看一下
所有程序员的挑战之一:不能过早开始实现,需要三思而后行
示例代码位置:http://www.netobjectives.com/dpexplained
第一章
面向对象软件开发简介
2013年11月20日05:02:18
20世经80年代以来:“在需求列表中寻找名词,并将它们转化成对象”的指导原则。这种方式将封装定义成数据隐藏,将对象定义成含有数据和用来访问、操作数据行为的一些东西。其局限性在于只观注“如何实现对象”,在面向对象分析中损失了太多的内含。本章尝试对这些概念进行扩展,并以此为基础讨论另一种面向对象范式。这些定义都是源自设计模式研究的一些策略和原则的结果,反映了更完整的面向对象观
面向对象范型是应对结构化程序设计遇到的诸多问题才应运而生的,理解这些挑战有助于理解面向对象程序设计的优点
功能分解
功能分解:functional decomosition 是一种处理复杂问题的自然方法,主要做法是将问题分解成多个功能步骤(类似于算法中的devide-and-conque),小问题更容易解决。
功能分解的一个问题在于“能者多责”,它通常导致让一个主程序负责控制子程序,挑战随之而来:主程序能者多责、责任太多:要确保一切正常工作,还要协调和控制各函数的先后顺序,因此经常会产生非常复杂的代码。
换一种思路来想问题:让子函数负责自己的行为,主函数告知子函执行某些任务,并信任子函数知道如何进行,这种方式将比功能分解自然的多了。这其中一个重点的思想就是“委托 delegation”(我是这样理解委托的:我告诉你我的目标,你来实现(使用什么方法我不管))
功能分解的另一个问题在于“难以应对变化”,变化是无法避免了(各种维护需求,或者全新的需求都可能产生),经常要为已有主体增加新的变体以实现某种功能。为新需求修改目前运行良好的程序的后果就是,可能现有的程序也无法正常运行,bug随之产生:“许多bug都源于代码修改”
思考:代码非要关注所有函数使用它们的方式吗?函数应该怎样与另一个函数交互?函数关注的东西是否太多,比如要实现的逻辑、要交互的东西、要使用的数据?和人一样,如果程序试图同时关注过多的东西,一旦有变化出现,就只能坐等bug的出来。
无论我们多么努力,无论分析做的多好,也永远无法从用户那里获得所有的需求,因为未来有太多未知,万物皆变化。我们无法阻止变化,但对变化本身却并非无能为力。
需求问题
需求总在变化,原因多种多样。我们要做到的:我可能无法知道什么将变化,但是我能猜到哪里会发生变化
“第二次既然总能编写正确,你第一次也应该编写正确”这句话并非要求预期所有可能发生的变化,并相应的构建代码,而是要求预期哪里会发生变化,将这些区域给封装起来,从而做到将代码与变化所产生的影响隔离起来,类似(A <---> 抽象层 <---> B),通过抽象层将变化隔离在一则,从而把变化所带来的影响减小到最小。(记得沉思录里面好像有句话:“绝大多数问题都可以通过增加一个层来解决”,这个层当然就是指隔离变化的抽象层)
与其抱怨需求问题变化,不如改变开发过程,从而更有效的应用变化;代码可以设计的使需求的变化不至于产生太大影响,代码可以逐步演进,新代码可以影响较小的加入
应对变化:使用功能分解
与其编写一个大函数,不如使之模块化,模块化肯定有助于提供代码的可理解性,从而提升代码的可维护性,但模块化并不有助于代码应对所有可能的变化。
作者:这种方式(当然是指功能分析了)我一直在用,发现有两个主要问题:低内聚(weak cohesion)、紧耦合(tighg coupling)。
《Code Complete》 by Steve McConnell的精彩描述:
内聚性cohesion:一个例程(函数)内各操作之间联系的紧密程度。描述的是一个例程内各组成部分之间相互联系的紧密程度
耦合性coupling:两个例程(函数)之间联系的紧密程度。描述的是一个例程和其它例程之间相互联系的紧密程度
软件开发的目标应该是创建这样的例程:内部完整(高内聚),而与其它例程之间的联系则是小巧、直接、可见、灵活的(松耦合)
修改一个函数甚至是一个函数使用的数据都可能对其它函数产生严重破坏,这种bug称为“不良副作用”,即这些代码联系导致bug的生产。
维护和调试的绝大多数时间都用在了:努力弄清代码的运作机理、寻找bug和防止出现不良副作用上,真正的修改时间相当相当的短,所以功能分解是注意力放在了错误的地方
应对需求变化
示例:假如你在一个会议上担任讲师,听课的人在课后还要去听其它的课,但他们不知道下一堂课的听课地点,你的责任之一就是确保他们知道下一堂课去哪里上。
可能的结构化程序设计方法,按步骤分解:
1. 获得听课人的名单
2. 对名单上的每一个人做如下工作:
2.1 找到他要听的下堂课
2.2 找到该课的听课地点
2.3 找到从你的教室到下一堂课的地点怎样走
2.4 告诉这个人怎样去下一党课的教室
转化为如下方法:
1. 获得听课人员名单的方法
2. 获得每个众课程表的方法
3. 告诉某个人如何从你的教室到其它任何教室的方法
4. 为听课的每个人服务的一个控制程序
很少有人会这样的做,你可能的做法:
1. 把下一堂课的课表、教室位置分布图贴出来
2. 让每个人根据上述信息,找到如何到下一个教室
两种做法的区别在哪里?
第一种做法:直接给每个人提供指示,你必须密切关注大量细节,除你之外没有其他人负责,这样你会疯掉的。这里你要负责一切。
第二种做法:只给出通用提示,然后期待每个人自己弄清楚怎样完成任务。这里学生对自己的行为负责。
这里最大的区别就是责任的转移,责任从自己转移给每个人;要实现的目的相同,但是组织方式差异很大
为了看到责任转移带来的影响,我们考虑需求变化之后的修改情况:
新增需求:承担助教的学生要在下一课之前收集本节课的学生评价表,并交到教师办公室
第一种的修改:将不得不对控制程序进行修改以区分研究生和本科生,然后给研究生特殊指示,程序将做大幅度的修改
第二种的修改:在各司其职的情况下,只需要为研究生增加一个程序,而控制程序仍然只说“找到你们下一堂课的教室”,每个人以指示相应行事
这代表了控制程序的责任发生了明显变化,第一种情况下,每增加一类学生,控制程序都要自身做相应修改,要负责每一类学生如何去做,并做相应控制;第二种情况不会影响控制程序的主体功能,由学生自己负责弄清自己如何去做。
第二种做法有如下三方面的不同:
1. 人们对自己的行为负责,不再由*控制程序决定人们的行为
2. 控制程序与不同的人交流好像他们都是一样的
3. 控制程序无需知道学生从此教室到彼教室可能需要采取的任何特殊步骤
《UML Distilled》 by Martin Fowler描述的软件开发过程的三种不同视角:
1. 概念: “呈现了所研究领域中的各种概念。。。得出概念模型时应该很少考虑或者不考虑实现它的软件。。。”,这个视角回答的问题是:软件要负责什么?
2. 规约: “现在考虑软件,但是我们只关注软件的接口,不关注实现”,这个视角要回答的问题是:怎样使用软件?
3. 实现: “这里我们考虑软件本身, 这可能是最常用的视角,但在许多方面,采取规约视角通常会更好”,这个视角要回答的问题时:软件怎样履行自己的责任
作为讲师,你是在概念层次上与人交流,你告诉学生“你要他们做什么”,而非“如何去做”。如何去下一课堂是明确的,是实现层次上的事情。在一个层次是进行交流,在另一个层次上执行,这样请求者就无需知道具体的操作细节,只要一般性——概念性的知道即可。这一点的效力巨大:只要概念不变,请求者就与实现细节的变化隔离了。
面向对象范型
2013年11月20日07:02:39
面向对象范型以对象概念为中心,一切都集中在对象上,代码围绕对象而非函数来组织。
那什么是对象?
传统定义:带有方法的数据。这是一种非常糟糕的对象观
新定义:具有责任的实体
使用对象的优点在于,可以定义自己负责自己的事物,对象天生就知道自己的类型。对象中的数据知道自己的状态如何,对象的方法能够使要求他做的事情得到正确执行
对象与责任:
1. Student 知道自己现在的教室,知道自己下堂课的教室;从一个教室去下一个教室
2. Instructor 告诉学生到下堂课的教室去
3. Classroom 有明确的地址
4. Direction giver 对于给定的俩教室,指出从一个教室到另一个教室的路线
这种情况下,对象还是通过寻找问题领域的实体发现的,然后分析这些对象需要做什么,为每个对象明确责任,与在需求中找名词发现对象和找动词发现方法的技术是一致的。后面支给出更好的方法。
理解对象的最佳方式是将其看成是“具有责任的东东”。一条好的设计规则:对象应该自己负责自己,而且应该清楚的定义责任(定义概念、定义规约)。
使用Martin Fowler的视角框架来观察对象:
1. 概念层次上,对象是一组责任
2. 规约层次上,对象是一组能被其它对象或其自己调用的方法(或称行为)
3. 实现层次上,对象是代码与数据,以及它们之间的相互计算
糟糕的是,在面向对象设计的教学与讨论中更多的是停留在实现层次上,而略去了前两者的巨大效力(思考:这也不能太怪面向对象课程的教授者,他在教授学生基本的面向对象的语法,不是在教如何做好软件的设计,如何这位老师有意识的先从概念与规约视角来讲对象,再从实现的视角来讲对象,那自然最好,但是在学生都不知对象的语法的情况下,讲这些概念,是不是就强人所难了)
2013年11月22日05:51:09
对象提供给外界的,要求对象做什么的方法的集合称为对象的公开接口 public interface
类就是对象行为的定义,它包含如下完整描述:
1. 对象所包含的元素
2. 对象提供的方法(为完成某种责任的对外接口,及完成责任所需的一些内部方法)
3. 访问这些数据元素和方法的方式
类型(type), 实例(instance),创建类的实例的过程称为实例化(instantiation)
面向对象方法的“去下堂课教室”:
1. 开始控制程序
2. 实例化教室中学生的集合
3. 告诉此集合,让学生到自己下堂课的教室
4. 集合让每个学生自己去下堂课的教室
5. 每个学生都:
5.1 找到自己下堂课的教室在哪里
5.2 决定怎样去(步行、自行车、校车、开车、把自己邮寄过去。。。都行)
5.3 去那里
6. 完成
抽象类(abstract class)定义了【其它】一些相关类的行为,或者理解为抽象类定义了一组类可以做什么。实现角度来讲,【其它】类通常是抽象类的子类,称为具体类(concrete class),它通常代表着一个概念特定、不变的实现
is-a(是一个/种)关系是称之为继承(inheritance)关系的一种特例, 如上面讲的RegularStudent(普通学生,这里充当具体类),GraduateStudent(研究生,这里充当具体类),都是Student(学生类,这里充当抽象类)的子娄,称具体类是特化(specialize)了抽象类,称抽象类泛化(generalize)了具体类
从概念视角、规约视角来看待抽象类远比从实现视角看待抽象类要重要的多,把抽象类理解为其它类的占位符。可以使用抽象类来定义其派生类必须实现的方法。抽象类还可以包含派生类都能够使用的方法,派生类是使用抽象类的默认行为还是使用自己有所变化的行为,由派生类自己决定(这与“对象自己负责自己”的要求一致)
抽象类不只是不能实例化:抽象类通常被定义成“不能实例化的类”,这个定义本身没有错——在实现层次上来讲。但是局限性太大,从概念层次上把抽象类理解成其它的占位符对设计模式的理解更有帮助。也就是说,抽象类为我们提供了一种方法,能够给一组相关的类赋予一个名字,这使我们可以将这一组相关的类看成一个概念。 在面向对象范型中,必须从概念、规约、实现三个层次来思考问题
对象都自己负责自己,所以很多东东不需要暴露给其它对象,公开(public)接口——可被访问的方法,相应的不能访问的方法与数据则是私有(private)的,面向对象语法中区分上述可访问性为如下三种类型:
1. 公开(public)——任何对象都可以看见
2. 保护(protected) ——只有这个类及其派生类的对象可以看见
3. 私有(private) ——只有这个类的对象可以看见
这就引出了封装(encapsulation)的概念,封装通常简单的描述成“数据隐藏”,也就是说不应该暴露给外界的内部数据成员的可见性为protected或private(其它书里面多提倡:“优先使用组合而不是继承”的原则,这样一个对象只应该有public与private的可见性,protected的可见性其实是派生类对其父类数据的可见性的侵犯,在设计时应该尽量没有protected可见性的数据成员与方法。即使使用派生,也完全可以为数据提供访问接口的形式来进行,派生这一作法应该只发生在具体类对抽象接口的派生(这里接口理解为 java中的接口才对,或者是c++中的只包含纯虚接口的类),不应该出现具体类对具体类的派生(具体类这里指有数据成员的类))
封装不只是数据隐藏,封装意味着各种隐藏。抽象类隐藏了从其派生的类的类型
另一个重要的概念是多态(polymorphism): 抽象类的方法对不同具体类有不同的行为,这种情况称之为多态。具体的语法见面向对象教程课本,这里想说的是多态从概念与规约视角来看所带来的价值,就是让使用者根据调用接口(规约),可以概念性的操作对象
封装有几个优点,“对用户隐藏”这一事实直接蕴涵了如下优点:
1. 使用更加容易,用户无需操心实现问题
2. 可以在不考虑调用者的情况下进行修改,调用者从一开始就不应该知道对象是如何实现的,它们之间应该只有接口信赖,不应该有任何其它信赖关系(接口不变,则调用者的使用方式可以不变)
3. 其他对象对该对象内部是未知的——这些内部数据用来帮助对象实现其向外承诺的功能
再次考虑不良副作用的问题,这种bug通过封装有效的解决了。对象对其它对象的内部是未知的,使用封装,并遵循“对象自己负责自己”的策略,那么唯一能改变对象的方式就是:调用该对象的对外接口中提供的方法。对象的数据和实现其责任的方法,都会与其它对象所带来的变化屏蔽开
对象拯救了我们:
1. 对象对自己行为所负的责任越多,控制程序需要负的责任就越少
2. 封装使对象内部行为的变化对其它对象变得透明
3. 封装有助于防止不良副作用
封装什么东东时,必然将使其耦合变松,隐藏实现(即封装它们)有助于松耦合
特殊对象方法
构造函数(constructor)和析构函数(destructor)是在对象起始与结束时做一些工作,这是对象“自己负责自己”所要求的
第五章
设计模式简介
2013年11月25日05:50:29
学习设计模式对加深面向对象分析与设计的理解大有裨益
本章很多思想来自:Christopher Alexander的 The Timeless Way of Building 一书
在一种文化中,不同的人在很大程度上可以就“何为优秀的设计,何为美”之类的问题达成共识,文化对优秀设计的评价将超越个人的看法
文化学的一个重要分支,就是在寻找描述一种文化的行为与价值观的模式
设计模式背后的一个观点就是:软件系统的质量可能客观度量
怎样着手进行优秀的设计?
1. 在优秀设计中具有而在劣质设计中不具有的东东是什么?
2. 在劣质设计中具有而在优秀设计中不具有的东东又是什么?
Alexander的如下信念:如果设计的质量可以客观的评价,那么我们应该可以找出设计因何优秀,又因何拙劣的评价标准
在进行了大量观察之后,Alex发现在特定的建筑物中,优秀的结构都有一些共同之处 (幸福总是相似的,而不幸的原因则各有各的不同),即要在好的设计中寻找共性
Alex发现,通过观察解决相似问题的不同结构,可以看出优秀设计之间的相似之处,他将这种相似之处称为模式
模式:某一背景之下,某个问题的一种解决文案。每个模式都描述了一个在我们的环境中不断重复出现的问题,并进而叙述了这个问题解决方案的要素,通过这种方式,解决方案能够百万次的反复应用,但是具体方式又会完全不同
一个模式的描述应该包括4项:
1. 模式的名称
2. 模式的目的,即要解决的问题
3. 实现方法
4. 为了实现模式我们必须考虑的限制与结束因素
几乎任何设计问题中都存在模式,而且可以结合起来以解决复杂问题
从建筑模式到软件设计模式
1. 软件中是否存在不断重复出现,可以以某种相同的方式解决的问题?
2. 是否可以使用模式方法来设计软件,即先找出模式,然后根据模式来创建特定问题的解决方案?
这俩问题的答案,毋庸置疑都是肯定的,那接下来的事情就是找出一些模式,然后制定出新模式的编录标准,应运而生的就是Gof的《设计模式:可复用面向对象软件的基础》
本书有如下目的:
1. 将设计模式的思想应用于软件设计——并称它们为设计模式( design pattern )
2. 给出编录和描述设计模式的一种格式
3. 编录了23个设计模式
4. 在这些设计模式的基础上推导出一些面向对象设计的策略与方法
Gof并没有创建书中的模式,相反他们只是将软件界已经存在的、反映了(针对各种具体问题的)优秀设计经验的模式识别出来,这与Alex的工作类似。(与模式识别很相似哦^_^)
模式的关键特征:
1. 名称: 每个模式都有唯一的用于标识自己的名称
2. 意图: 模式的目的
3. 问题: 模式要解决的问题
4. 解决方案: 模式怎样为问题提供一个适合其环境的解决方案
5. 参与者与协作者: 模式所涉及的实体
6. 效果: 使用模式的效果,研究模式中起作用的各个因素
7. 实现:模式的实现???,这里实现只是模式的具体实现,并不能视为模式本身
8. 一般性结构: 显示模式典型结构的标准图
设计模式中的术语“后果(consequence)”这个词是指因果中的效果而已,也即,你???实现的模式,它将怎样影响已有的因素,又会怎样被已有因素所影响
为什么学习设计模式
设计模式有助于复用与沟通,模式还为我们提供了观察问题、设计过程和面向对象的更高层次的视角,将使我们从“过早处理结节”的桎梏中解放出来,改变人的思维定式,从而成为更加高效的分析人员
学习模式的其他好处
1. 改善团队的沟通与个人学习
2. 代码更易于修改与维护
3. 设计模式阐述了基本的面向对象原则
4. 采用更好的策略,即使不使用模式
设计模式一书对优秀的面向对象设计的策略提出的一些建议:
1. 按接口编程(designing to interface)
2. 尽量用聚合代替继承
3. 找出变化并封装之(在我现在的理解就是应该增加一个抽象层来封装变化,使变化被隔离在一则,另一则针对接口编程,从而使变化的影响减少到最小)
第八章
开拓视野
2013年11月26日06:33:12
本章讨论面向对象的三个基本概念:对象、封装与抽象类
本章尝试从理解设计模式的角度出发,描述一种全新的看待面向对象设计的方式,及高质量代码的本质品质
对象的传统看法与新看法
传统:具有方法的数据
新看法:具有责任的实体
“开始时寻找描述问题领域中状态的数据,然后添加处理数据的方法,瞧,对象就来了”,这个方式太简单与肤浅了,这种全从实现的视角来看待对象的方式不可取
更有意义的方式应该从概念视角出发来看待对象——具有责任的实体。这种责任定义了对象的行为。这种方式有利于我们关注对象的意图行为,而非如何实现对象。使我们把构建软件拆成两步:
1. 先做出一个初步设计(如宏观架构的整体框架),不用操心所有相关细节
2. 逐步细化,以实现该设计
不过早的操心实现细节,设计出的总体架构应该更加稳定,实现细节通过封装隐藏起来,更利于框架的稳定
这种方式行之有效,是因为我们只关注对象的公开接口,这是我们要求对象完成某些工作(完成责任)的交流渠道。有了好的接口,我们能要求对象完成其责任范围之内的任何工作,而且相信其能够完成的很好,我们不需要知道在对象内部到底是怎样运行的,不需要知道它怎样处理我传递给它的信息,也不需要知道它怎样收集其他需要的信息,我们大可以放心的【全权委托】给它
如有一个Shape对象,有如下责任:
1. 知道自己的位置
2. 能够在显示器上绘制自己
3. 能够从显示器上擦除自己
这些责任意味着如下方法:
1. getLocation(...)
2. drawShape(...)
3. unDrawShape(...)
关注动机而非实现,是设计模式中反复出现的主题。因为实现隐藏在接口之后,实际上将对象的实现与使用它们的对象解耦了
封装传统看法与新看法
传统看法:封装即数据隐藏
汽车能够起到伞的遮风蔽雨的目的,如果从中这一个角度来把洗车当成伞看待,那局限性就太大了;同样,封装也不仅仅是数据隐藏,这样看待局限性也太大了。
新看法:封装应该被视为“任何形式的隐藏”,除数据隐藏之后外还可以隐藏如下东东:
1. 实现细节
2. 派生类
3. 设计细节
4. 实例化规则
隐藏实现其实是封装了实现细节;类型的封装通过多态使用具有派生类的抽象类(或者具有多种实现的接口)实现的,(抽象类的使用者无需知道派生类的实际类型,这正是《设计模式》一书中封装的通常含义);
这种更加宽泛的方式看待封装,其优点是能够带来一种更好的切分(即分解)程序的方法。封装层就成为设计需要遵循的接口。
不从纯虚接口继承可能存在的三个问题(即从一个已经实现好了的、正常工作的类中继承):
1. 可能导致弱内聚:类的主体功能在派生类中又要关心一些细节等
2. 减少复用的可能性:为主体功能增加的代码,只包含在这个类里面,不如把其单独成类,以便复用
3. 无法根据变化很好的伸缩:新需求进来又如何,再次派生新的类吗?不如新功能是一个单独的类,使用组合或装饰的方式添加新的需求,这种方式还可以做到运行时动态组合添加
另外一种方式:将类按相同行为分类
2013年11月27日05:28:17
发现变化并将其封装
《设计模式》一书有如下建议:考虑你的设计中哪些地方可能变化。这种方式与关注会导致重新设计的原因相反,它不是考虑什么会迫使你设计改变,而是考虑你怎样才能在不进行重新设计的情况下进行改变。这里的关键在于封装发生变化的概念,这是许多设计模式的主题。简而言之:“发现变化并将其封装”
将封装看成是通过抽象类或接口隐藏类——“类型封装”,一个对象聚集了这种抽象类的引用,这些引用其实隐藏了表示行为变化的类。
事实上,很多设计模式都是使用封装在对象之间创建一个【层】。这样设计者可以在【层】的两侧进行修改,并不会对另一侧产生不良影响,做到了两侧的松耦合。
事例:对动物的不同特征建模。项目的需求如下:
1. 每种动物都有不同数量的腿
2. 动物对象必须能够记住并获取这一信息
3. 动物的移动方式可以不同
4. 对于给定的地形类型,动物必须能够返回从一个地方移动到另一个地方所花费的时间
可能的设计:
1. 腿的数量使用一个数据成员存放
2. 假设有两种不同的移动方式,这是处理行为上的变化,通常采用另一种方式:a. 使用一个数据成员说明对象有哪种移动方式,代码中使用if/else来进行区分。 b. 使用两种类表示,一种能够行走,一种能够飞翔
糟糕的是,当问题更加复杂时这两种方式都不可行。虽然这种方法在只有一种变化(移动方式)时能够奏效,但如果存在更多变化时会怎样呢(如再增加食性(草食性与肉食性的区分))?如果对于每种特殊的情况都使用继承,会生成很多类。使用一种开关变量表示动物的类型,将移动方式与食性结合起来,降低了对象的内聚性。再考虑如下新需求:一种动物在不同情况下会表现出不同的行为,又该如何?
另一种设计:让动物类包含一种合适移动行为的对象,即对于移动方式这种变化提取一个接口(抽象类)(假设的两种移动方式可为两种不同的子类),对于食性这种变化点也提取一个接口(抽象类)。即为对象(动物)增加移动方式的数据成员(对象),增加食性的数据成员(对象)。从中可以看出使用组合(或称聚合)优于使用继承。
用对象的属性来包含变化,和用对象的行为来包含变化其实非常相似,从一个数据成员变成一个类来封装行为的变化看似并没有太大的改变,但这种处理方式对于“对象应该自己负责自己”有好处,变化在持续,把很多代码都写在一个类中,复杂性将急剧上升,维护成本将越来越大。
将继承视为一种一致的处理概念上相同的几个具体类的方法,比看成一种特化方法要合理的多。
2013年11月27日06:18:15
共性与可变性分析与抽象类
Jim Coplien有关共性与可变性分析的工作,告诉了我们如何在问题领域找到不同的变化,如何在不同领域找到共同点:找到变化的地方,称为“共性分析”; 然后找出如何变化,称为“变性分析”
按照Coplien的说法:“共性分析就是寻找一个共同的要素,它们能够帮助我们理解系列成员的【共同之处】在哪里。”。这里的【共同之处】指通过表现方式或者所执行的功能相互关联的一些要素。找出事物共同点的这一过程将定义这些要素所属的系列。如记号笔,铅笔,圆珠笔都识别成“书写工具”的共性
可变性分析:揭示了系列成员之间的不同。可变性分析只有在给定了共性之后才有意义:
共性分析寻找的是不可能随时间而改变的结构,而可变性分析寻找的是可能变化的结构。可变性分析只有在相关联的共性分析定义的上下文中才有意义。。。从架构的视角看,共性分析为架构提供长效的要素,而可变性分析则促进它适应实际使用所需。
也就是说,如果变体是问题领域中各个特定的具体情况,共性就定义了问题领域中将这些情况相互联系起来的概念。共通的概念用抽象类表示。可变性分析所发现的变化将通过具体类实现。
三方面的联系:
共性分析 -> 概念视角 -> 抽象类
规约视角
可变性分析 -> 实现视角 -> 具体类
说明:
1.通过分析这些对象必须完成哪些接口(概念视角),我们能够确定如何调用它们(规约视角)。
2.在实现这些类的时候,要让API提供足够信息,能够保证正确实现而且解耦
3.规约视角位于其中,共性与可变性都要涉及的视角,规约描述了如何与概念上相似的一组对象通信。在实现层次上这个规约将成为抽象类或者接口。
使用抽名类进行特化的好处:
1.抽象类与核心概念:抽象类代表了将所有派生类联系起来的核心概念。正是这些核心概念定义了派生类的共性
2.共性与要使用的抽象类:共性定义了需要使用的抽象类
3.可变性与抽象类的派生类:从共性中识别出来的可变性将成为抽象类的派生类
4.规约与抽象类的接口:这些类的接口对应于规约层次
这样类的设计就简化成如下两个步骤:
1.定义抽象类(共性)时,必须问自己:需要用什么接口来处理这个类的所有责任
2.定义派生类(可变性)时,必须问自己:对于这个给定的特定实现(这个变化),应该如何根据给定的规约实现它
规约视角与概念视角之间的关系在于:规约标识了用来处理这些概念所有情况(即概念视角所定义共性)所需的接口
规约视角与实现视角之间的关系在于:对于规定的规约,怎样实现这个特定情况(这个变化)?
敏捷编程的品质
2013年11月28日06:16:55
设计模式常被人叙述成需要“预先设计”,它提倡从问题领域的一些主要概念着手,然后深入,考虑更多的细节。
极限编程(eXtreme programming XP)提倡另一种方法,它的核心是循序渐进的开发,在编程的同时进行验证,大的概念是从众多小的概念中演变出来的
极限编程似乎不提倡预先设计,而设计模式则要求预先设计,但两者并不是对立的,相反的其是相辅相成的,殊途同归的目的:高效、健壮和灵活的代码。
极限编程包含的一些品质(顺序与重要性无关):
1.无冗余
2.可读
3.可测试
无冗余
某个规则只在一个地方实现,或称“一个规则,一个地方”,或称“一次且仅一次规则”,代表了设计人员的一个最佳实践。这消除了重复,从而避免的很多可能的问题。冗余与耦合之间常常纠缠不清。
遵循“按接口设计”做法,找出变化之处,从而使代码高效内聚,是消除冗余代码所必须的,级限编程中无冗余,避免耦合,要求代码被封装在定义明确的接口之后。
可读性
与强内聚息息相关。“按意图编程”将这一品质提升一个新的高度。给函数指定一个“反映意图的名字”,编程变成了对一系列函数的调用,这些函数的命名清楚的说明的它们的意图。在更大的层次上,阅读者要看的是代码的意图,而不是代码的实现。Martin Fowler写到:“每次当我们感到什么东西需要加注释的时候,相反的,我们会编写一个方法”,其结果就是更简短,更清晰的方法。
按意图编程与按接口编程又是英雄所见略同
可测试性
这是级限编程的核心。编写代码之前就先编写测试,有几个目的:
1. 最后能得到一组自动化测试
2. 必须是按方法的接口而非实现来设计,从而得到内聚性、封装性更好,而耦合性更松的方法
3. 关注测试你会将这些概念分成可测试的部分,从而得到强内聚松耦合的代码
我们称容易测试的代码为可测试代码。“预先编写测试”的实践,本质上是会产生可测试性很高的代码。
许多开发人员将这种理念继续推进,通过测试驱动整个开发过程,这种方法称之为测试驱动开发(Test-Driven Development TDD),它乍看起来与模式要求互不相容,实则不然,其与模式基于相同的原则,只是处理代码编写任务的方式不同罢了。
第12章
专家设计之道(些专家非彼砖家)
2013年11月28日07:02:29
怎样着手设计?是获取细节,再看它们如何组合在一起?还是先从总体概念(big picture)开始, 再将其逐步分解?
Chirstopher Alexander的方法是先把注意力放到高层的关系上——某种意义上的自顶而下。他认为做出设计之前,理解所要解决的问题的背景至关重要。他用模式来定义这些关系。他不仅提出了一些模式,还给出了一整套设计方法。
Alex:设计常常被认为是一种合成过程,一种将事物放到一起的过程,一种组合的过程。按照这种观点,整体是由部分组合而成的。先有部分,然后才有整体的形式。
从部分构造整体,不可能等到优美的设计。
对于软件开发人员来说,这些部分就是对象与类,找出对象与类,并它们定义行为与接口,但不能只把注意力放在部分上,把预先形成的部分加起来,不可能形成自然特征的任何东西,(即把预先创建好的一些例程库加起来无法获得优秀的软件设计)Alex认为从部分构造并非好的设计方式。
2013年12月2日06:07:23
Alex这里的“模块化”是指建筑业中的那些完全相同、可以互换的部分,不是指软件业中的术语“模块”,他的意思是,如果在形成总体概念之前就开始构建模块,这些模块不可能有能力处理特殊的需要。遵循了Alex的方法,我发现创建的类比通用部分出发所得到的更好,而且可用性更强。
我们的目标是在它们所处的大背景中设计部分——类与对象,从而构建出健壮而灵活的系统。软件的设计看成是一种【复杂化】(complexification)的过程更好。
【复杂化】:结构是通过对整体操作、使其起皱(crinkling it)而注入其中的,而不是通过一小部分一小部分添加而成。在分化的过程中,整体孕育了部分,整体的形式及其各个部分是同时产生的。
Alex所描述的是这样一种设计思想:开始用最简单的术语考虑问题(概念层次上),然后添加更多特性,在此过程设计将越来越复杂,因为我们加入了越来越多的信息。
每一个模式都是一个分化空间的操作符,也就是说,它在以前没有差异的地方创建差异。语言就是一系列这样的操作符,其中每一个操作符进一步对以前分化形成的意象继续分化。
Alex断言:设计应该从问题的一个简单陈述开始,然后通过在这个陈述中加入信息,使它更加详细(也更回复杂)。这种信息应该采取模式的形式。对于Alex来讲,模式定义了问题域中实体之间的关系。
通过考虑实体互相之间关系应该怎样,为设计提供了大量信息,在考虑模式背景中存在的其它模式,可以改进设计。
Alex指出优秀设计师(当然是批建筑中的,是否可能扩充到其它领域?)应该遵循的规则:
1. 每次一个:模式应该按顺序每次只运用一个
2. 背景优先:首先应用那些要为其它模式创建背景的模式
Alex所描述的模式定义了问题域中实体间地的关系,这些模式的重要性次于这些关系,但它们为我们提供了讨论这些关系的方式。
Alex的步骤讨论:(《建筑的永恒之道》)
1. 找出模式:用这些模式思考问题,要记住,模式的用途是定义实体之间的关系
2. 从背景模式开始:为其它模式创建背景的模式应该为设计起点
3. 然后从背景转向内部:观察其余的模式或任何其它可能已经发现的模式,应该第二点,重复这一过程
4. 改进设计:改进过程中始终考虑模式所蕴涵的背景
5. 实现:实现应该融入模式所要要求的细节
通过在已经出现的概念的背景中添加新概念进行设计,应该是我们从Alex的工作中至少要学习到的一点。
用模式解决CAD/CAM问题
2013年12月3日06:45:58
问题回顾
基本需求:创建一个读取CAD/CAM模型(系统现有V1版本,并会有新版本加入V2,及将来可能的V3)并从中提取部分的计算机程序,将这些部件提供给一个现成的专家系统,从而进行智能化设计。复杂之处在于提取程序要与多个版本的CAD/CAM系统交互
系统如下表中的需求:
1. 读取CAD/CAM模式并提取部件 :【补】
2. 能够处理多种零件:
3. 处理多个版本的CAD/CAM系统:
用模式思考
模式为我们的思考指引了方向,用在问题域中发现的那些模式进行思考,只考虑关键的概念,先不要操心细节。
用模式思考的过程
1. 找出模式 : 【补】
2. 分析与应用模式
2.1 按背景的创建顺序将模式排序
2.2 选择模式并扩展设计
2.3 找到其它模式
2.4 重复
3. 添加细节
设计模式最有用的地方,可能就是提供了着物的方法,此后通过找出问题领域中各种概念之间的关系,充实其它部分,这就需要使用共性/可变性分析(见下面章内容)
找出模式
问自己:约束因素有哪些?问题域中的问题何在?它们与我们知道的模式如何对应?
按背景考察模式
背景和一种定义是:一些事物存在或发生所处的相互有关的情况——一种环境、一种场景
系统中一种模式经常与其它模式相关——为系统中的其它模式提供背景。在分析中,寻找一个模式与其它模式是否相关、如何相关、寻找这个模式为其它模式创建或提供的背景,以及这个模式自身存在的背景,都是非常重要的。也许这些并非每次都能找到,但是通过寻找你能得到更加高质量的解决方案
寻找背景是一种非常基本的工具,确定哪个模式为哪个模式创建背景,经常要借助于模式的概念性考察,称这些为械的“元”描述
考虑背景时使用的一条规则:在知道所需对象是什么之前,无需考虑如何实例化这些对象的问题。在设计中需要尽量减少脑子里思考的事情。我们应该确定了对象是什么之后再定义工厂
【最高模式】将约束其它模式,【最高模式】是指系统中为其它模式创建背景的一两个模式。或称“最外层模式”或“背景设定模式”
一个可选的寻找方法:
1. 有没有一个模式定义了另一个模式的行为?
2. 有没有两个模式彼此相互影响?
使用械还另一个优点:可以看出重用与灵活性,而从细节开始着手的设计,如果最终组合到高层做的不好,将很难重用
从总体概念开始,然后不断添加特性。通过分析找出系统的最高模式,注用程序的框架基本就出来了。【补细节】
未完待续。。。