设计模式之禅(第2版)(txt+pdf+epub+mobi电子书下载)


发布时间:2020-07-28 06:21:31

点击下载

作者:秦小波

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

设计模式之禅(第2版)

设计模式之禅(第2版)试读:

前言

为什么写这本书

2009年5月份,我在JavaEye上发了一个帖子,其中提到自己已经工作9年了,总觉得这9年不应该就这么荒废了,应该给自己这9年的工作写一个总结,总结的初稿就是这本书。

在谈为什么写这本书之前,先抖抖自己前9年的职业生涯吧。大学时我是学习机械的,当时计算机刚刚热起来,自己也喜欢玩一些新奇的东西,记得最清楚的是用VB写了一个自由落体的小程序,模拟小球从桌面掉到地板上,然后计算反弹趋势,很有成就感。于是2000年毕业时,我削尖了脑袋进入了IT行业,成为了一名真正的IT男,干着起得比鸡早、睡得比狗晚的程序员工作,IT男的辛酸有谁知晓!

坦白地说,我的性格比较沉闷,属于典型的程序员型闷骚,比较适合做技术研究。在这9年里,项目管理做过,系统分析做过,小兵当过,团队领导人也当过,但至今还是一个做技术的。要总结这9年技术生涯,总得写点什么吧,最好是还能对其他人有点儿用的。那写什么好呢?Spring、Struts等工具框架类的书太多太多,很难再写出花样来,经过一番思考,最后选择了一个每一位技术人员都需要掌握的、但普及程度还不是非常高的、又稍微有点难度的主题——设计模式(Design Pattern,DP)。

中国人有不破不立的思维,远的如秦始皇焚书坑儒、项羽火烧阿房宫,近的如破“四旧”。正是由于有了这样的思想,于是乎能改的就改,不能改的就推翻重写,没有一个持续开发蓝图。为什么要破才能立呢?为什么不能持续地发展?你说这是谁的错呢?是你架构师的错,你不能持续地拥抱变化,这是一个系统最失败的地方。那怎么才能实现拥抱变化的理想呢?设计模式!

设计模式是什么?它是一套理论,由软件界的先辈们(The Gang of Four:包括Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides)总结出的一套可以反复使用的经验,它可以提高代码的可重用性,增强系统的可维护性,以及解决一系列的复杂问题。做软件的人都知道需求是最难把握的,我们可以分析现有的需求,预测可能发生的变更,但是我们不能控制需求的变更。问题来了,既然需求的变更是不可控的,那如何拥抱变化呢?幸运的是,设计模式给了我们指导,专家们首先提出了6大设计原则,但这6大设计原则仅仅是一系列“口号”,真正付诸实施还需要有详尽的指导方法,于是23种设计模式出现了。

设计模式已经诞近20年了,其间出版了很多关于它的经典著作,相信大家都能如数家珍。尽管有这么多书,工作5年了还不知道什么是策略模式、状态模式、责任链模式的程序员大有人在。不信?你找个机会去“虚心”地请教一下你的同事,看看他对设计模式有多少了解。不要告诉我要翻书才明白!设计模式不是工具,它是软件开发的哲学,它能指导你如何去设计一个优秀的架构、编写一段健壮的代码、解决一个复杂的需求。

因为它是软件行业的经验总结,因此它具有更广泛的适应性,不管你使用什么编程语言,不管你遇到什么业务类型,设计模式都可以自由地“侵入”。

因为它不是工具,所以它没有一个可以具体测量的标尺,完全以你自己的理解为准,你认为自己多了解它,你就有可能产生多少的优秀代码和设计。

因为它是指导思想,你可以在此基础上自由发挥,甚至是自己设计出一套设计模式。

世界上最难的事有两件:一是让人心甘情愿地把钱掏出来给你,二是把自己的思想灌输到别人的脑子里。设计模式就属于第二种,它不是一种具体的技术,不像Struts、Spring、Hibernate等框架。一个工具用久了可以熟能生巧,就像砌墙的工人一样,长年累月地砌墙,他也知道如何把墙砌整齐,如何多快好省地干活,这是一个人的本能。我们把Struts用得很溜,把Spring用得很顺手,这非常好,但这只是一个合格的程序员应该具备的基本能力!于是我们被冠以代码工人(Code Worker)——软件行业的体力劳动者。

如果你通晓了这23种设计模式就不同了,你可以站在一个更高的层次去赏析程序代码、软件设计、架构,完成从代码工人到架构师的蜕变。注意,我说的是“通晓”,别告诉我你把23种设计模式的含义、适应性、优缺点都搞清楚了就是通晓。错了!没有工作经验的积累是不可能真正理解设计模式的,这就像大家小时候一直不明白为什么爸爸妈妈要工作而不能每天陪自己玩一样。

据说有的大学已经开了设计模式这门课,如果仅仅是几堂课,让学生对设计模式有一个初步的了解,我觉得并无不妥,但如果是专门的一门课程,我建议取消它!因为对一个尚无项目开发经验的学生来说,理解设计模式不是一般困难,而是非常非常困难!之前没有任何的实战经验,之后也没有可以立即付诸实践的场景,这样能理解设计模式吗?

在编写本书之前,23种设计模式我都用过,而且还算比较熟练,但是当真正要写到书中时,感觉心里没谱儿了。这个定义是这样的吗?是需要用抽象类还是应该用接口?为什么在这里不能抽取抽象呢?为什么在实际项目中这个模式要如此蜕化?这类小问题有时候很纠结,需要花费大量的精力和时间去分析和确认。所以,在写作的过程中我有过很多忧虑,担心书中会有太多瑕疵,这种忧虑现在仍然存在。遇到挫折的时候也气馁过,但是我坚信一句话:“开弓没有回头箭,回头即是空”,既然已经开始,就一定要圆满完成。第2版与第1版的区别

本书是第2版,在写作中吸取了读者对上一版的许多意见和建议,修订了一些代码的变量、类、方法名称,以更加符合自然语言;删除了部分有争议的内容(如单例模式的垃圾回收问题);修改了一些常用的名词,确保与编程人员的习惯相匹配。希望通过这些改进,给读者提供一个更完美的设计模式盛宴,弥补上一版中的诸多不足。

第2版第38章中新增了4种新的设计模式:对象池模式、雇工模式、黑板模式、空指针模式。这些模式是我们在实际工作中经常遇到,或者在开源代码时常看到的,但是我们却没有升级到“ 模式”这一理性高度。特别是像空指针模式,我们在编码中经常会遇到空值判断问题,但我们没有去想一想是否可以有更好的方式解决。第2版对空指针模式进行了讲解,虽然简单,但相信对你提升编码质量有很大的帮助。本书的特色

简单、通俗、易懂,但又不肤浅,这是本书的最大特色。自己看过的技术书还算比较多,很痛恨那种大块头的巨著,搁家里当枕头都觉得太硬。如果要是再晦涩难懂点,那根本没法看,看起来实在是太累。设计模式原本就是理论性的知识,讲解的难度比较大,但我相信这本书能够把你对设计模式的恐惧一扫而光。不信?挑几页先看看!

我的理念是:像看小说一样阅读本书。我尽量用浅显通俗的语言讲解,尽量让你有继续看下去的欲望,尽量努力让你有兴趣进入设计模式的世界,兴趣是第一老师嘛!虽然我尽量让这本书浅显、通俗、易懂,但并不代表我的讲解就很肤浅。每个设计模式讲解完毕之后,我都附加了两个非常精华的部分:设计模式扩展和最佳实践,这是俺压箱底的技能了,为了博君一看,没招了,抖出来吧!尤为值得一提的是,本书还有设计模式PK和混编设计模式两部分内容教你如何自如地去运用这些设计模式,这是当前所有设计模式类的图书都不具备的,连最权威的那本书也不例外。

我很讨厌技术文章中夹杂着的那些晦涩难懂的文字,特别是一堆又一堆的名词堆砌,让人看着就反胃。但是为了学习技术,为了生存,还是必须看下去。国内的技术文档,基本上都是板着一副冷面孔讲技术,为什么要把技术弄得这么生硬呢?技术也有它幽默、柔情的一面,只是被我们的“孔夫子们”掩盖了,能用萝卜、白菜这种寻常人都熟悉的知识来讲解原子弹理论的人,那是牛人,我佩服这样的人。记住,用一堆名词把你忽悠晕的人很可能什么都不懂!

本书想告诉你的是,技术也可以很有乐趣,也可以让你不用皱着眉头思考,等待你的只是静静地看,慢慢地思考,本书的内容会润物细无声地融入你的思维中。本书面向的读者

热爱技术并且讨厌枯燥乏味技术文章的读者都可以看本书;

你是程序员,没问题,本书能够让你写出更加高效、优雅的代码;

你是架构师,那更好,设计模式可让你设计出健壮、稳定、高效的系统,并且自动地预防未来业务变化可能对系统带来的影响;

你是项目经理,也OK,设计模式可以让你的工期大大缩短,让你的项目团队成员快速地理解你的意图,最终的成果就是优质的项目:高可靠性、高稳定性、高效率和低维护成本。如何阅读本书

首先声明,本书中所有的例子都是用Java语言来实现的,但是你可以随手翻翻看,基本上能保证每三条语句一个注释,可以说是在用咱们的母语讲解设计模式。即使你不懂Java语言,也没有关系,只要知道在Java中双斜杠(//)代表注释就足够了,况且Java如此强大和盛行,多了解一点没有坏处。类图看不懂?没关系,不影响你理解设计模式,多看看就懂了!

如果你还没有编程经验,我建议你把它当做小说来看,懂行的看门道,不懂行的看热闹,这里的热闹足够多,够你看一壶的了。你现在能看懂多少是多少,不懂没有关系,你要知道,经验不是像长青春痘一样,说长就长出来了,它是需要时间积累的,需要你用心去感受,然后才能明白为什么要如此设计。

如果你已经对编程有感觉了(至少两年开发经验),我相信你都能看懂,但能“懂”到什么程度,就很难说了,看你的水平了。但是,我可以保证,这里的设计模式都是你能看懂的,没有你看不懂的!我建议你通读这本书,然后挑门你最得意的编程语言,动手写吧!给自己制定一个计划,每天编写一段代码,不需要太多,200行足够,时不时地把设计模式融入你的代码中。甭管是什么代码,比如你想编写一个识别美女图片的程序,好呀,抓紧时间去写吧,写好了就不用到处看美女了,程序一跑就把网上的美女图片都抓过来了,牛呀(记住,程序写好了要分享给我)。看吧,坚持下去,一年以后你再跟你的同侪比较一下,那差距肯定不是一般的大。

如果你是资深工程师、架构师、技术顾问等高等级的技术人员,那我告诉你,你找对这本书了。系统架构没有思路?没有问题,看看扩展部分,它会开阔你的思路。系统的维护成本居高不下?看看本书,设计模式也许能帮你省点银子。开发资源无法保证?设计模式能让你用有限的资源(软硬件资源和人力资源)设计出一个优秀的系统。项目质量参差不齐,缺陷一大堆?多用设计模式,它会给你意想不到的效果。给人讲课没有素材?没问题,本书中的素材足以让你赢得阵阵掌声!

编程是一门艺术活,我有一个同事,能把类图画成一个小乌龟的形状,天才呀!作为一位技术人员,最基本的品质就是诚实,“知之为知之,不知为不知,是知也”,自己不懂没有关系,去学,学无止境,但是千万不要贪多,这抓一点,那挖一点,好像什么都懂,其实什么都不懂。中国一直推崇复合型人才,我不是很赞成,因为这对年轻人来说是一个误导。先精一项技术,然后再发散学习,先点后面才是正道。

记得《武林外传》中有这样一段对话:

刑捕头:手中无刀,心中有刀。

老白:错了,最高境界是手中无刀,心中也无刀。

体验一下吧,我们的设计模式就是一把刀,极致的境界就是心中无设计模式,代码亦无设计模式——设计模式随处可见,俯拾皆是,已经融入软件设计的灵魂中,这才是高手中的高手,简称高高手。

哦,最最重要的忘记说了,请把附录中的“23种设计模式附图”撕下来,贴在你的办公桌前,时不时地看看,也让老板看看,咱是多么地用心!关于书名

乍一看,书名和内容貌似不相符呀,其实不然!

在我们的常规思维中,“禅”应该是很高深的东西,只可意会,不可言传。没错,禅宗也是如此说。禅是得道者的“悟”,是不能用言语来表达的,但是得道者为了能让更多的人“悟”,就必须用最容易让人理解的文字把自己的体会表达出来。本书的“禅”是作者对设计模式的“悟”,本书的“形”就是你现在看到的这些极其简单、通俗、易懂的文字。

至此,大家应该不会再对书名有疑虑了吧,嘿嘿。致谢

本书第1版的写作耗时7个月,第2版的更新又花了4个月,可以说是榨干了海绵里所有的水——基本上能用的时间都用上了。在公交车上打腹稿,干过!在马桶上查资料,干过!在睡梦中思考案例,也有过!就差没有走火入魔了!

首先,感谢杨福川编辑,没有他的慧眼,这本书不可能出版。其次,感谢妻子和儿子,每天下班回到家,一按门铃,儿子就在里面叫:“我来开门,我来开门。”儿子三岁,太调皮了,他不睡觉我基本上是不能开写的,我一旦开始写东西,他就跑过来问:“爸爸,你在干什么呀”,紧接着下一句就是“爸爸,你陪我玩”,基本都是拿我当玩具,别的小朋友都是把父亲当马骑,他却不,他把我当摩托车骑,还要加油门,发动……小家伙脚太重了,再骑摩托,非被他踩死不可!

还要感谢我的朋友王骢,周末只要小家伙在家,我只有找地方写书的份儿,王骢非常爽快地把钥匙给我,让我有一个安静的地方写书。一个人沉浸在自己喜欢的世界里也是一件非常幸福的事。

当然,还要感谢JavaEye上所有顶帖的网友,没有你们的支持我就没有写作的动力,就像希腊神话中的巨人安泰失去了大地的力量一样,是你们的回帖让我觉得不孤单,让我知道我不是一个人在战斗!

最后,再次对本书中可能出现的错误表示歉意,真诚地接受大家轰炸!如果你在阅读本书时发现错误或有问题想讨论,请发邮件给我。第一部分 大旗不挥,谁敢冲锋——6大设计原则全新解读

第1章 单一职责原则

第2章 里氏替换原则

第3章 依赖倒置原则

第4章 接口隔离原则

第5章 迪米特法则

第6章 开闭原则第1章 单一职责原则1.1 我是“牛”类,我可以担任多职吗

单一职责原则的英文名称是Single Responsibility Principle,简称是SRP。这个设计原则备受争议,只要你想和别人争执、怄气或者是吵架,这个原则是屡试不爽的。如果你是老大,看到一个接口或类是这样或那样设计的,你就问一句:“你设计的类符合SRP原则吗?”保准对方立马“萎缩”掉,而且还一脸崇拜地看着你,心想:“老大确实英明”。这个原则存在争议之处在哪里呢?就是对职责的定义,什么是类的职责,以及怎么划分类的职责。我们先举个例子来说明什么是单一职责原则。

只要做过项目,肯定要接触到用户、机构、角色管理这些模块,基本上使用的都是RBAC模型(Role-Based Access Control,基于角色的访问控制,通过分配和取消角色来完成用户权限的授予和取消,使动作主体(用户)与资源的行为(权限)分离),确实是一个很好的解决办法。我们这里要讲的是用户管理、修改用户的信息、增加机构(一个人属于多个机构)、增加角色等,用户有这么多的信息和行为要维护,我们就把这些写到一个接口中,都是用户管理类嘛,我们先来看它的类图,如图1-1所示。图1-1 用户信息维护类图

太Easy的类图了,我相信,即使是一个初级的程序员也可以看出这个接口设计得有问题,用户的属性和用户的行为没有分开,这是一个严重的错误!这个接口确实设计得一团糟,应该把用户的信息抽取成一个BO(Business Object,业务对象),把行为抽取成一个Biz(Business Logic,业务逻辑),按照这个思路对类图进行修正,如图1-2所示。图1-2 职责划分后的类图

重新拆封成两个接口,IUserBO负责用户的属性,简单地说,IUserBO的职责就是收集和反馈用户的属性信息;IUserBiz负责用户的行为,完成用户信息的维护和变更。各位可能要说了,这个与我实际工作中用到的User类还是有差别的呀!别着急,我们先来看一看分拆成两个接口怎么使用。OK,我们现在是面向接口编程,所以产生了这个UserInfo对象之后,当然可以把它当IUserBO接口使用。也可以当IUserBiz接口使用,这要看你在什么地方使用了。要获得用户信息,就当是IUserBO的实现类;要是希望维护用户的信息,就把它当作IUserBiz的实现类就成了,如代码清单1-1所示。

代码清单1-1 分清职责后的代码示例......IUserInfo userInfo = new UserInfo();//我要赋值了,我就认为它是一个纯粹的BOIUserBO userBO = (IUserBO)userInfo;userBO.setPassword("abc");//我要执行动作了,我就认为是一个业务逻辑类IUserBiz userBiz = (IUserBiz)userInfo;userBiz.deleteUser();......

确实可以如此,问题也解决了,但是我们来分析一下刚才的动作,为什么要把一个接口拆分成两个呢?其实,在实际的使用中,我们更倾向于使用两个不同的类或接口:一个是IUserBO,一个是IUserBiz,类图如图1-3所示。图1-3 项目中经常采用的SRP类图

以上我们把一个接口拆分成两个接口的动作,就是依赖了单一职责原则,那什么是单一职责原则呢?单一职责原则的定义是:应该有且仅有一个原因引起类的变更。1.2 绝杀技,打破你的传统思维

解释到这里,估计你已经很不屑了,“切!这么简单的东西还要讲?!”好,我们来讲点复杂的。SRP的原话解释是:

There should never be more than one reason for a class to change.

这句话初中生都能看懂,不多说,但是看懂是一码事,实施就是另外一码事了。上面讲的例子很好理解,在实际项目中大家都已经这么做了,那我们再来看看下面这个例子是否好理解。电话这玩意,是现代人都离不了,电话通话的时候有4个过程发生:拨号、通话、回应、挂机,那我们写一个接口,其类图如图1-4所示。图1-4 电话类图

我不是有意要冒犯IPhone的,同名纯属巧合,我们来看一个这个过程的代码,如代码清单1-2所示。

代码清单1-2 电话过程public interface IPhone { //拨通电话 public void dial(String phoneNumber); //通话 public void chat(Object o); //通话完毕,挂电话 public void hangup();}

实现类也比较简单,我就不再写了,大家看看这个接口有没有问题?我相信大部分的读者都会说这个没有问题呀,以前我就是这么做的呀,某某书上也是这么写的呀,还有什么什么的源码也是这么写的!是的,这个接口接近于完美,看清楚了,是“接近”!单一职责原则要求一个接口或类只有一个原因引起变化,也就是一个接口或类只有一个职责,它就负责一件事情,看看上面的接口只负责一件事情吗?是只有一个原因引起变化吗?好像不是!

IPhone这个接口可不是只有一个职责,它包含了两个职责:一个是协议管理,一个是数据传送。dial()和hangup()两个方法实现的是协议管理,分别负责拨号接通和挂机;chat()实现的是数据的传送,把我们说的话转换成模拟信号或数字信号传递到对方,然后再把对方传递过来的信号还原成我们听得懂的语言。我们可以这样考虑这个问题,协议接通的变化会引起这个接口或实现类的变化吗?会的!那数据传送(想想看,电话不仅仅可以通话,还可以上网)的变化会引起这个接口或实现类的变化吗?会的!那就很简单了,这里有两个原因都引起了类的变化。这两个职责会相互影响吗?电话拨号,我只要能接通就成,甭管是电信的还是网通的协议;电话连接后还关心传递的是什么数据吗?通过这样的分析,我们发现类图上的IPhone接口包含了两个职责,而且这两个职责的变化不相互影响,那就考虑拆分成两个接口,其类图如图1-5所示。图1-5 职责分明的电话类图图1-6 简洁清晰、职责分明的电话类图

这个类图看上去有点复杂了,完全满足了单一职责原则的要求,每个接口职责分明,结构清晰,但是我相信你在设计的时候肯定不会采用这种方式,一个手机类要把ConnectionManager和DataTransfer组合在一块才能使用。组合是一种强耦合关系,你和我都有共同的生命期,这样的强耦合关系还不如使用接口实现的方式呢,而且还增加了类的复杂性,多了两个类。经过这样的思考后,我们再修改一下类图,如图1-6所示。

这样的设计才是完美的,一个类实现了两个接口,把两个职责融合在一个类中。你会觉得这个Phone有两个原因引起变化了呀,是的,但是别忘记了我们是面向接口编程,我们对外公布的是接口而不是实现类。而且,如果真要实现类的单一职责,这个就必须使用上面的组合模式了,这会引起类间耦合过重、类的数量增加等问题,人为地增加了设计的复杂性。

通过上面的例子,我们来总结一下单一职责原则有什么好处:

● 类的复杂性降低,实现什么职责都有清晰明确的定义;

● 可读性提高,复杂性降低,那当然可读性提高了;

● 可维护性提高,可读性提高,那当然更容易维护了;

● 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

看过电话这个例子后,是不是想反思一下了,我以前的设计是不是有点问题了?不,不是的,不要怀疑自己的技术能力,单一职责原则最难划分的就是职责。一个职责一个接口,但问题是“职责”没有一个量化的标准,一个类到底要负责那些职责?这些职责该怎么细化?细化后是否都要有一个接口或类?这些都需要从实际的项目去考虑,从功能上来说,定义一个IPhone接口也没有错,实现了电话的功能,而且设计还很简单,仅仅一个接口一个实现类,实际的项目我想大家都会这么设计。项目要考虑可变因素和不可变因素,以及相关的收益成本比率,因此设计一个IPhone接口也可能是没有错的。但是,如果纯从“学究”理论上分析就有问题了,有两个可以变化的原因放到了一个接口中,这就为以后的变化带来了风险。如果以后模拟电话升级到数字电话,我们提供的接口IPhone是不是要修改了?接口修改对其他的Invoker类是不是有很大影响?

注意 单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。1.3 我单纯,所以我快乐

对于接口,我们在设计的时候一定要做到单一,但是对于实现类就需要多方面考虑了。生搬硬套单一职责原则会引起类的剧增,给维护带来非常多的麻烦,而且过分细分类的职责也会人为地增加系统的复杂性。本来一个类可以实现的行为硬要拆成两个类,然后再使用聚合或组合的方式耦合在一起,人为制造了系统的复杂性。所以原则是死的,人是活的,这句话很有道理。

单一职责原则很难在项目中得到体现,非常难,为什么?在国内,技术人员的地位和话语权都比较低,因此在项目中需要考虑环境,考虑工作量,考虑人员的技术水平,考虑硬件的资源情况,等等,最终妥协的结果是经常违背单一职责原则。而且,我们中华文明就有很多属于混合型的产物,比如筷子,我们可以把筷子当做刀来使用,分割食物;还可以当叉使用,把食物从盘子中移动到口中。而在西方的文化中,刀就是刀,叉就是叉,你去吃西餐的时候这两样肯定都是有的,刀就是切割食物,叉就是固定食物或者移动食物,分工很明晰。这种文化的差异很难一步改造过来,但是我相信随着技术的深入,单一职责原则必然会深入到项目的设计中,而且这个原则是那么的简单,简单得不需要我们更加深入地思考,单从字面上大家都应该知道是什么意思,单一职责嘛!

单一职责适用于接口、类,同时也适用于方法,什么意思呢?一个方法尽可能做一件事情,比如一个方法修改用户密码,不要把这个方法放到“修改用户信息”方法中,这个方法的颗粒度很粗,比如图1-7中所示的方法。图1-7 一个方法承担多个职责

在IUserManager中定义了一个方法changeUser,根据传递的类型不同,把可变长度参数changeOptions修改到userBO这个对象上,并调用持久层的方法保存到数据库中。在我的项目组中,如果有人写了这样一个方法,我不管他写了多少程序,花了多少工夫,一律重写!原因很简单:方法职责不清晰,不单一,不要让别人猜测这个方法可能是用来处理什么逻辑的。比较好的设计如图1-8所示。

通过类图可知,如果要修改用户名称,就调用changeUserName方法;要修改家庭地址,就调用changeHomeAddress方法;要修改单位电话,就调用changeOfficeTel方法。每个方法的职责非常清晰明确,不仅开发简单,而且日后的维护也非常容易,大家可以逐渐养成这样的习惯。图1-8 一个方法承担一个职责

所以,如果对接口、类、方法使用了单一职责原则,那么快乐的就不仅仅是你了,还有你的项目组成员,大家可以轻松而又愉快地进行开发;还有你的老板,减少了因为变更引起的工作量,减少了无谓的人员和资金消耗。当然,最快乐的也许就是你了,因为加官晋爵可能等着你哟!1.4 最佳实践

阅读到这里,可能有人会问我,你写的是类的设计原则吗?你通篇都在说接口的单一职责,类的单一职责你都违背了呀!呵呵,这个还真是的,我的本意是想把这个原则讲清楚,类的单一职责嘛,这个很简单,但当我回头写的时候,发觉并不是这么回事,翻看了以前的一些设计和代码,基本上拿得出手的类设计都是与单一职责相违背的。静下心来回忆,发觉每一个类这样设计都是有原因的。我查阅了Wikipedia、OODesign等几个网站,专家和我也有类似的经验,基本上类的单一职责都用了类似的一句话来说"This is sometimes hard to see",这句话翻译过来就是“这个有时候很难说”。是的,类的单一职责确实受非常多因素的制约,纯理论地来讲,这个原则是非常优秀的,但是现实有现实的难处,你必须去考虑项目工期、成本、人员技术水平、硬件情况、网络情况甚至有时候还要考虑政府政策、垄断协议等因素。比如,2004年我就做过一个项目,做加密处理的,甲方就甩过来一句话,你什么都不用管,调用这个API就可以了,不用考虑什么传输协议、异常处理、安全连接等。所以,我们就直接使用了JNI与加密厂商提供的API通信,什么单一职责原则,根本就不用考虑,因为对方不公布通信接口和异常判断。

对于单一职责原则,我的建议是接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。第2章 里氏替换原则2.1 爱恨纠葛的父子关系

在面向对象的语言中,继承是必不可少的、非常优秀的语言机制,它有如下优点:

● 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;

● 提高代码的重用性;

● 子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;

● 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;

● 提高产品或项目的开放性。

自然界的所有事物都是优点和缺点并存的,即使是鸡蛋,有时候也能挑出骨头来,继承的缺点如下:

● 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;

● 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;

● 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。

Java使用extends关键字来实现继承,它采用了单一继承的规则,C++则采用了多重继承的规则,一个子类可以继承多个父类。从整体上来看,利大于弊,怎么才能让“利”的因素发挥最大的作用,同时减少“弊”带来的麻烦呢?解决方案是引入里氏替换原则(Liskov Substitution Principle,LSP),什么是里氏替换原则呢?它有两种定义:

● 第一种定义,也是最正宗的定义:If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.(如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。)

● 第二种定义:Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.(所有引用基类的地方必须能透明地使用其子类的对象。)

第二个定义是最清晰明确的,通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。2.2 纠纷不断,规则压制

里氏替换原则为良好的继承定义了一个规范,一句简单的定义包含了4层含义。

1.子类必须完全实现父类的方法

我们在做系统设计时,经常会定义一个接口或抽象类,然后编码实现,调用类则直接传入接口或抽象类,其实这里已经使用了里氏替换原则。我们举个例子来说明这个原则,大家都打过CS吧,非常经典的FPS类游戏,我们来描述一下里面用到的枪,类图如图2-1所示。图2-1 CS游戏中的枪支类图

枪的主要职责是射击,如何射击在各个具体的子类中定义,手枪是单发射程比较近,步枪威力大射程远,机枪用于扫射。在士兵类中定义了一个方法killEnemy,使用枪来杀敌人,具体使用什么枪来杀敌人,调用的时候才知道,AbstractGun类的源程序如代码清单2-1所示。

代码清单2-1 枪支的抽象类public abstract class AbstractGun { //枪用来干什么的?杀敌! public abstract void shoot();}

手枪、步枪、机枪的实现类如代码清单2-2所示。

代码清单2-2 手枪、步枪、机枪的实现类public class Handgun extends AbstractGun { //手枪的特点是携带方便,射程短 @Override public void shoot() { System.out.println("手枪射击..."); }}public class Rifle extends AbstractGun{ //步枪的特点是射程远,威力大 public void shoot(){ System.out.println("步枪射击..."); }}public class MachineGun extends AbstractGun{ public void shoot(){ System.out.println("机枪扫射..."); }}

有了枪支,还要有能够使用这些枪支的士兵,其源程序如代码清单2-3所示。

代码清单2-3 士兵的实现类public class Soldier { //定义士兵的枪支 private AbstractGun gun; //给士兵一支枪 public void setGun(AbstractGun _gun){ this.gun = _gun; } public void killEnemy(){ System.out.println("士兵开始杀敌人..."); gun.shoot(); }}

注意粗体部分,定义士兵使用枪来杀敌,但是这把枪是抽象的,具体是手枪还是步枪需要在上战场前(也就是场景中)前通过setGun方法确定。场景类Client的源代码如代码清单2-4所示。

代码清单2-4 场景类public class Client { public static void main(String[] args) { //产生三毛这个士兵 Soldier sanMao = new Soldier(); //给三毛一支枪 sanMao.setGun(new Rifle()); sanMao.killEnemy(); }}

有人,有枪,也有场景,运行结果如下所示。

士兵开始杀敌人...

步枪射击...

在这个程序中,我们给三毛这个士兵一把步枪,然后就开始杀敌了。如果三毛要使用机枪,当然也可以,直接把sanMao.setGun(new Rifle())修改为sanMao.setGun(new MachineGun())即可,在编写程序时Solider士兵类根本就不用知道是哪个型号的枪(子类)被传入。

注意 在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。

我们再来想一想,如果我们有一个玩具手枪,该如何定义呢?我们先在类图2-1上增加一个类ToyGun,然后继承于AbstractGun类,修改后的类图如图2-2所示。图2-2 枪支类图

首先我们想,玩具枪是不能用来射击的,杀不死人的,这个不应该写在shoot方法中。新增加的ToyGun的源代码如代码清单2-5所示。

代码清单2-5 玩具枪源代码public class ToyGun extends AbstractGun { //玩具枪是不能射击的,但是编译器又要求实现这个方法,怎么办?虚构一个呗! @Override public void shoot() { //玩具枪不能射击,这个方法就不实现了 }}

由于引入了新的子类,场景类中也使用了该类,Client稍作修改,源代码如代码清单2-6所示。

代码清单2-6 场景类public class Client { public static void main(String[] args) { //产生三毛这个士兵 Soldier sanMao = new Soldier(); sanMao.setGun(new ToyGun()); sanMao.killEnemy(); }}

修改了粗体部分,把玩具枪传递给三毛用来杀敌,代码运行结果如下所示:

士兵开始杀敌人...

坏了,士兵拿着玩具枪来杀敌人,射不出子弹呀!如果在CS游戏中有这种事情发生,那你就等着被人爆头吧,然后看着自己凄惨地倒地。在这种情况下,我们发现业务调用类已经出现了问题,正常的业务逻辑已经不能运行,那怎么办?好办,有两种解决办法:

● 在Soldier类中增加instanceof的判断,如果是玩具枪,就不用来杀敌人。这个方法可以解决问题,但是你要知道,在程序中,每增加一个类,所有与这个父类有关系的类都必须修改,你觉得可行吗?如果你的产品出现了这个问题,因为修正了这样一个Bug,就要求所有与这个父类有关系的类都增加一个判断,客户非跳起来跟你干架不可!你还想要客户忠诚于你吗?显然,这个方案被否定了。

● ToyGun脱离继承,建立一个独立的父类,为了实现代码复用,可以与AbastractGun建立关联委托关系,如图2-3所示。图2-3 玩具枪与真实枪分离的类图

例如,可以在AbstractToy中声明将声音、形状都委托给AbstractGun处理,仿真枪嘛,形状和声音都要和真实的枪一样了,然后两个基类下的子类自由延展,互不影响。

在Java的基础知识中都会讲到继承,Java的三大特征嘛,封装、继承、多态。继承就是告诉你拥有父类的方法和属性,然后你就可以重写父类的方法。按照继承原则,我们上面的玩具枪继承AbstractGun是绝对没有问题的,玩具枪也是枪嘛,但是在具体应用场景中就要考虑下面这个问题了:子类是否能够完整地实现父类的业务,否则就会出现像上面的拿枪杀敌人时却发现是把玩具枪的笑话。

注意 如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。

2.子类可以有自己的个性

子类当然可以有自己的行为和外观了,也就是方法和属性,那这里为什么要再提呢?是因为里氏替换原则可以正着用,但是不能反过来用。在子类出现的地方,父类未必就可以胜任。还是以刚才的关于枪支的例子为例,步枪有几个比较“响亮”的型号,比如AK47、AUG狙击步枪等,把这两个型号的枪引入后的Rifle子类图如图2-4所示。图2-4 增加AK47和AUG后的Rifle子类图

很简单,AUG继承了Rifle类,狙击手(Snipper)则直接使用AUG狙击步枪,源代码如代码清单2-7所示。

代码清单2-7 AUG狙击枪源码代码public class AUG extends Rifle { //狙击枪都携带一个精准的望远镜 public void zoomOut(){ System.out.println("通过望远镜察看敌人..."); } public void shoot(){ System.out.println("AUG射击..."); }}

有狙击枪就有狙击手,狙击手类的源代码如代码清单2-8所示。

代码清单2-8 AUG狙击手类的源码代码public class Snipper { public void killEnemy(AUG aug){ //首先看看敌人的情况,别杀死敌人,自己也被人干掉 aug.zoomOut(); //开始射击 aug.shoot(); }}

狙击手,为什么叫Snipper?Snipe翻译过来就是鹬,就是“鹬蚌相争,渔人得利”中的那只鸟,英国贵族到印度打猎,发现这个鹬很聪明,人一靠近就飞走了,没办法就开始伪装、远程精准射击,于是乎Snipper就诞生了。

狙击手使用狙击枪来杀死敌人,业务场景Client类的源代码如代码清单2-9所示。

代码清单2-9 狙击手使用AUG杀死敌人public class Client { public static void main(String[] args) { //产生三毛这个狙击手 Snipper sanMao = new Snipper(); sanMao.setRifle(new AUG()); sanMao.killEnemy(); }}

狙击手使用G3杀死敌人,运行结果如下所示:

通过望远镜察看敌人...

AUG射击...

在这里,系统直接调用了子类,狙击手是很依赖枪支的,别说换一个型号的枪了,就是换一个同型号的枪也会影响射击,所以这里就直接把子类传递了进来。这个时候,我们能不能直接使用父类传递进来呢?修改一下Client类,如代码清单2-10所示。

代码清单2-10 使用父类作为参数public class Client { public static void main(String[] args) { //产生三毛这个狙击手 Snipper sanMao = new Snipper(); sanMao.setRifle((AUG)(new Rifle())); sanMao.killEnemy(); }}

显示是不行的,会在运行期抛出java.lang.ClassCastException异常,这也是大家经常说的向下转型(downcast)是不安全的,从里氏替换原则来看,就是有子类出现的地方父类未必就可以出现。

3.覆盖或实现父类的方法时输入参数可以被放大

方法中的输入参数称为前置条件,这是什么意思呢?大家做过Web Service开发就应该知道有一个“契约优先”的原则,也就是先定义出WSDL接口,制定好双方的开发协议,然后再各自实现。里氏替换原则也要求制定一个契约,就是父类或接口,这种设计方法也叫做Design by Contract(契约设计),与里氏替换原则有着异曲同工之妙。契约制定了,也就同时制定了前置条件和后置条件,前置条件就是你要让我执行,就必须满足我的条件;后置条件就是我执行完了需要反馈,标准是什么。这个比较难理解,我们来看一个例子,我们先定义一个Father类,如代码清单2-11所示。

代码清单2-11 Father类源代码public class Father { public Collection doSomething(HashMap map){ System.out.println("父类被执行..."); return map.values(); }}

这个类非常简单,就是把HashMap转换为Collection集合类型,然后再定义一个子类,源代码如代码清单2-12所示。

代码清单2-12 子类源代码public class Son extends Father { //放大输入参数类型 public Collection doSomething(Map map){ System.out.println("子类被执行..."); return map.values(); }}

请注意粗体部分,与父类的方法名相同,但又不是覆写(Override)父类的方法。你加个@Override试试看,会报错的,为什么呢?方法名虽然相同,但方法的输入参数不同,就不是覆写,那这是什么呢?是重载(Overload)!不用大惊小怪的,不在一个类就不能是重载了?继承是什么意思,子类拥有父类的所有属性和方法,方法名相同,输入参数类型又不相同,当然是重载了。父类和子类都已经声明了,场景类的调用如代码清单2-13所示。

代码清单2-13 场景类源代码public class Client { public static void invoker(){ //父类存在的地方,子类就应该能够存在 Father f = new Father(); HashMap map = new HashMap(); f.doSomething(map); } public static void main(String[] args) { invoker(); }}

代码运行后的结果如下所示:

父类被执行...

根据里氏替换原则,父类出现的地方子类就可以出现,我们把上面的粗体部分修改为子类,如代码清单2-14所示。

代码清单2-14 子类替换父类后的源代码public class Client { public static void invoker(){ //父类存在的地方,子类就应该能够存在 Son f =new Son(); HashMap map = new HashMap(); f.doSomething(map); } public static void main(String[] args) { invoker(); }}

运行结果还是一样,看明白是怎么回事了吗?父类方法的输入参数是HashMap类型,子类的输入参数是Map类型,也就是说子类的输入参数类型的范围扩大了,子类代替父类传递到调用者中,子类的方法永远都不会被执行。这是正确的,如果你想让子类的方法运行,就必须覆写父类的方法。大家可以这样想,在一个Invoker类中关联了一个父类,调用了一个父类的方法,子类可以覆写这个方法,也可以重载这个方法,前提是要扩大这个前置条件,就是输入参数的类型宽于父类的类型覆盖范围。这样说可能比较难理解,我们再反过来想一下,如果Father类的输入参数类型宽于子类的输入参数类型,会出现什么问题呢?会出现父类存在的地方,子类就未必可以存在,因为一旦把子类作为参数传入,调用者就很可能进入子类的方法范畴。我们把上面的例子修改一下,扩大父类的前置条件,源代码如代码清单2-15所示。

代码清单2-15 父类的前置条件较大public class Father { public Collection doSomething(Map map){ System.out.println("父类被执行..."); return map.values(); }}

把父类的前置条件修改为Map类型,我们再修改一下子类方法的输入参数,相对父类缩小输入参数的类型范围,也就是缩小前置条件,源代码如代码清单2-16所示。

代码清单2-16 子类的前置条件较小public class Son extends Father { //缩小输入参数范围 public Collection doSomething(HashMap map){ System.out.println("子类被执行..."); return map.values(); }}

在父类的前置条件大于子类的前置条件的情况下,业务场景的源代码如代码清单2-17所示。

代码清单2-17 子类的前置条件较小public class Client { public static void invoker(){ //有父类的地方就有子类 Father f= new Father(); HashMap map = new HashMap(); f.doSomething(map); } public static void main(String[] args) { invoker(); }}

代码运行结果如下所示:

父类被执行...

那我们再把里氏替换原则引入进来会有什么问题?有父类的地方子类就可以使用,好,我们把这个Client类修改一下,源代码如代码清单2-18所示。

代码清单2-18 采用里氏替换原则后的业务场景类public class Client { public static void invoker(){ //有父类的地方就有子类 Son f =new Son(); HashMap map = new HashMap(); f.doSomething(map); } public static void main(String[] args) { invoker(); }}

代码运行后的结果如下所示:

子类被执行...

完蛋了吧?!子类在没有覆写父类的方法的前提下,子类方法被执行了,这会引起业务逻辑混乱,因为在实际应用中父类一般都是抽象类,子类是实现类,你传递一个这样的实现类就会“歪曲”了父类的意图,引起一堆意想不到的业务逻辑混乱,所以子类中方法的前置条件必须与超类中被覆写的方法的前置条件相同或者更宽松。

4. 覆写或实现父类的方法时输出结果可以被缩小

这是什么意思呢,父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说,要么S和T是同一个类型,要么S是T的子类,为什么呢?分两种情况,如果是覆写,父类和子类的同名方法的输入参数是相同的,两个方法的范围值S小于等于T,这是覆写的要求,这才是重中之重,子类覆写父类的方法,天经地义。如果是重载,则要求方法的输入参数类型或数量不相同,在里氏替换原则要求下,就是子类的输入参数宽于或等于父类的输入参数,也就是说你写的这个方法是不会被调用的,参考上面讲的前置条件。

采用里氏替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑,非常完美!2.3 最佳实践

在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了,把子类当做父类使用,子类的“个性”被抹杀——委屈了点;把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离——缺乏类替换的标准。第3章 依赖倒置原则3.1 依赖倒置原则的定义

依赖倒置原则(Dependence Inversion Principle,DIP)这个名字看着有点别扭,“依赖”还“倒置”,这到底是什么意思?依赖倒置原则的原始定义是:

High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.

翻译过来,包含三层含义:

● 高层模块不应该依赖低层模块,两者都应该依赖其抽象;

● 抽象不应该依赖细节;

● 细节应该依赖抽象。

高层模块和低层模块容易理解,每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块,原子逻辑的再组装就是高层模块。那什么是抽象?什么又是细节呢?在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化,也就是可以加上一个关键字new产生一个对象。依赖倒置原则在Java语言中的表现就是:

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载