秒懂设计模式

978-7-115-54936-5
作者: 刘韬
译者:
编辑: 武晓燕

图书目录:

详情

本书共计 25 章,以轻松、幽默、浅显易懂的文风从面向对象编程、面向对象三大特性的理论基础开篇,然后系统且详细地讲解了单例、原型、工厂方法、抽象工厂、建造者、门面、组合、装饰器、适配器、享元、代理、桥接、 模板方法、迭代器、责任链、策略、状态、备忘录、中介、命令、访问者、观察者及解释器等 23 种设计模式的概念及结构机理,最后以六大设计原则收尾,全面地解析归纳了软件设计准则,参透设计模式的本质。 本书引入了很多贴近生活的真实范例,并配有大量生动形象的插图,再结合相关例程代码实战演练,循序渐进、深入浅出,引导读者探究设计模式的哲学真谛。 本书面向广大的软件设计工作者,包括但不限于各个层次从事面向对象编程语言开发的软件开发、设计、架构等工程技术人员,也可以作为大专院校相关专业教学用书和培训学校的教材。

图书摘要

版权信息

书名:秒懂设计模式

ISBN:978-7-115-54936-5

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


著    刘 韬

责任编辑 武晓燕

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


本书共计25章,以轻松、幽默、浅显易懂的方式从面向对象编程、面向对象三大特性的理论基础开篇,接着系统且详细地讲解了单例、原型、工厂方法、抽象工厂、建造者、门面、组合、装饰器、适配器、享元、代理、桥接、模板方法、迭代器、责任链、策略、状态、备忘录、中介、命令、访问者、观察者及解释器共23种设计模式的概念及结构机理,最后以六大设计原则总结收尾,全面地解析归纳了软件设计准则,参透设计模式的本质。

本书引入了很多贴近生活的真实范例,并配有大量生动形象的插图,再结合相关例程代码实战演练,循序渐进、深入浅出,引导读者探究设计模式的哲学真谛。

本书面向广大的软件设计工作者,包括但不限于各个层次从事面向对象编程语言的软件研发、设计、架构等工程技术人员,也可以作为大专院校相关专业教学用书和培训学校的教材。


相信软件开发工作者都听过一句名言:“不要重复造轮子。”从某种意义上讲,程序中如果出现大量重复的代码,则意味着这是一个缺乏设计的软件项目。面向对象编程语言的初学者写代码时往往是“东一榔头、西一棒槌”,想到哪里写到哪里,缺乏软件架构的全局观,最终造成系统中充斥大量的冗余代码,缺乏模块化的设计,更谈不上代码的复用性。代码量大并不能代表系统功能多么完备,更不能代表程序员多么努力与优秀,反之,作为有思想高度的开发者一定要培养“偷懒”意识,想方设法以最少的代码量实现最强的功能,这样才是优秀的设计。

设计模式主要研究的是“变”与“不变”,以及如何将它们分离、解耦、组装,将其中“不变”的部分沉淀下来,避免“重复造轮子”,而对于“变”的部分则可以用抽象化、多态化等方式,增强软件的兼容性、可扩展性。如果将编写代码比喻成建筑施工,那么设计模式就像是建筑设计。这就像乐高积木的设计理念一样,圆形点阵式的接口具有极强的兼容性,能够让任意组件自由拼装、组合,形成一个全新的物件。

有一定项目经验的开发人员都会有这样的体会,随着需求的增加与变动,软件项目版本不断升级,维护也变得越来越难,修改或添加一个很简单的功能往往要耗费大量的时间与精力,牵一发而动全身,严重时甚至会造成整个系统的崩溃。优秀的系统不单单在于其功能有多么强大,更应该将各个模块划分清楚,并且拥有一套完备的框架,像开放式平台一样兼容对各种插件的扩展,让功能变动或新增变得异常简单,一劳永逸,这离不开对各种设计模式的合理运用。

设计模式并不局限于某种特定的编程语言,它是从更加宏观的思想高度上展开的一种格局观,是一套基于前人经验总结出的软件设计指导思想,所以很多初学者觉得设计模式晦涩难懂,无从下手。本书秉承简约与现实的风格,帮助读者进行一场思想升华,将各种概念与理论化繁为简,以通俗易懂、更贴近生活的实例与源码详细解析每种模式的结构与机理。此外,文中配有大量生动形象的漫画与图表,幽默轻松的风格使原本刻板的知识鲜活起来,让读者能在轻松愉悦的学习氛围中领悟设计模式的思想真谛。

本书共有25章,包含从面向对象基础概念及特性到创建型、结构型、行为型设计模式的具体分析讲解,再到软件设计原则的归纳总结,由浅入深、由表及里。

面向对象

第1章,介绍了面向对象的概念及其三大特性,包括封装、继承、多态

创建型设计模式·第2~6章,包括单例模式、原型模式、工厂方法模式、抽象工厂模式、建造者模式

结构型设计模式

第7~13章,包括门面模式、组合模式、装饰器模式、适配器模式、享元模式、代理模式、桥接模式

行为型设计模式

第14~24章,包括模板方法模式、迭代器模式、责任链模式、策略模式、状态模式、备忘录模式、中介模式、命令模式、访问者模式、观察者模式、解释器模式

设计原则

第25章,归纳总结软件设计中的六大原则,包括单一职责原则、开闭原则、里氏替换原则、接口隔离原则、依赖倒置原则和迪米特法则

刘韬,笔名凸凹,现居西安,曾就读于西安电子科技大学和澳大利亚查理斯杜大学,先后在软通动力、中软国际、汇丰软件、艾默生科技资源等国内外知名企业承担软件设计及开发工作,至今已有15余年工作经验,主要研究方向为软件设计、数据库设计、Web应用程序设计、UI设计等,技术栈主要包括Java、C#、Spring框架、Micro Service架构及组件、Linux、Oracle、MySQL、JavaScript、JQuery、Angular等。

由于书中涉及知识点较多,难免有疏漏之处,欢迎广大读者批评、指正,并多提宝贵意见。作者的反馈邮箱为liewtao@vip.qq.com,本书责任编辑联系邮箱为wuxiaoyan@ptpress.com.cn。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

虽然作者和编辑尽最大努力确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可(见下图)。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发送电子邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以在线提交投稿,请联系邮箱wuxiaoyan@ptpress.com.cn。

如果您所在的学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近40年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


在这个计算机发展日新月异的时代,软件产品不断推陈出新、让人应接不暇,软件需求更是变幻莫测,难以捉摸。作为技术人员,我们在软件开发过程中常常会遇到代码重复的问题,从而不得不对系统进行大量改动,这不但带来很多额外工作,而且会给产品带来不必要的风险。因此,良好、稳固的软件架构就显得至关重要。设计模式正是为了解决这些问题,它针对各种场景提供了适合的代码模块的复用及扩展解决方案。

设计模式最早于1994年由Gang Of Four(四人小组)提出,并以面向对象语言C++作为示例,如今已大量应用于Java、C#等面向对象语言所开发的程序中。其实设计模式和编程语言并不是密切相关的,因为编程语言只是人与计算机沟通的媒介,它们可以用自己的方式去实现某种设计模式。从某种意义上讲,设计模式并不是指某种具体的技术,而更像是一种思想,一种格局。本书将以时下流行的面向对象编程语言Java为例,对23种设计模式逐一拆解、分析。

在学习设计模式之前,我们先得搞清楚到底什么是面向对象。我们生活的现实世界里充满了各种对象,如大自然中的山川河流、花鸟鱼虫,抑或是现代文明中的高楼大厦、车水马龙,我们每天都要面对它们,与它们沟通、互动,这是对面向对象最简单的理解。为了将现实世界重现于计算机世界中,我们想了各种方法针对这些对象建立数字模型,但是理想很“丰满”,而现实很“骨感”,我们永远无法包罗万象。人们在“造物”的过程中发现,各种模型并非孤立存在的,它们之间有着千丝万缕的关联,于是便出现了面向对象所特有的编程方法。我们利用封装、继承、多态的方式去建模,从而大量减少重复代码、降低模块间耦合,像拼积木一样组装了整个“世界”。这里提到的“封装”“继承”和“多态”便是面向对象的三大特性,它们是掌握设计模式不可或缺的先决条件与理论基础,我们必须要对其进行全面透彻的理解。

想要理解封装,我们可以先观察一下现实世界中的事物,比如胶囊对于各类混合药物的封装;钱包对于现金、身份证及银行卡的封装;计算机主机机箱对于主板、CPU及内存等配件的封装等。

由此可见,封装在我们生活中随处可见。我们举一个现实生活中常见的例子。如图1-1所示,注意餐盘中的可乐杯,其中的饮料是被装在杯子里面的,杯子的最上面封上盖子,只留有一个孔用于插吸管,这其实就是封装。封装隐藏了杯子内部的饮料,也许还会有冰块,而对于杯子外部来说只留有一个“接口”用于访问。这样的做法是否多此一举?又会带来什么好处呢?首先是方便、快捷,只有这样我们才能拿着饮料杯四处行走,随吸随饮,而不至于把饮料洒得到处都是,因为零散的数据缺乏集中管理,难以引用、读取。其次是封装后的可乐更加干净、卫生,可以防止外部的灰尘落入,杯子里面以关键字“private”声明的可乐会成为内部的私有化对象,因此能防止外部随意访问,避免造成数据污染。最后,对外暴露的吸管接口带来了极大便利,顾客在喝可乐时根本不需要关心杯子的内部对象和工作机制,如杯子中的冰块如何让可乐降温、杯体内部的气压如何变化、气压差又是如何导致可乐流出等实现细节对顾客完全是不可见的,留给顾客的操作其实非常简单,只需调用“吸”这个公有方法就可以喝到冰爽的可乐了。

图1-1 饮料的封装

我们再来分析一下对计算机主机的封装,它必然需要一个机箱把各种配件封装进去,如主板、CPU、内存、显卡、硬盘等。一方面,机箱起到保护作用,防止异物(如老鼠、昆虫等)进入内部而破坏电路;另一方面,机箱也不是完全封闭的,它一定对外预留有一些访问接口,如开机按钮、USB接口等,这样用户才能够使用计算机,计算机主机的类结构如图1-2所示。

图1-2 计算机主机的类结构

封装的概念在历史发展中也非常多见,其实它就是随着时间的推移对前人经验和技术产物的逐渐堆叠和组合的结果。举个例子,早期的枪设计得非常简陋,打一发子弹需要很长时间去准备,装填时要先把火药倒入枪管内,然后装入铅弹,最后用棍子戳实后才能发射;而下一次发射还要再重复这一过程,耗时费力。为了解决这个问题,人们开始了思考,既然弹药装填如此困难,那么不如把弹头和火药组合后封装在弹壳里。这样只要撞击弹壳底部,弹头就会被爆炸的火药崩出去,装入枪膛的子弹便可发出,如图1-3所示。

从弹药到子弹的发展其实就是对弹药的“封装”,因此大大提高了装弹效率。其实一次装一发子弹还是不够高效,如果再进一步,在子弹外再封装一层弹夹的话则会更显著地提升效率。我们可以定义一个数据结构“栈”来模拟这个弹夹,保证最早压入(push)的子弹最后弹出(pop),这就是栈结构“先进后出,后进先出”的特点。如此一来,子弹打完后只需更换弹夹就可以了。至此,封装的层层堆叠又上了一个层次,在机枪被发明出来之后冷兵器时代就彻底结束了。

图1-3 弹药的发展

在Java编程语言中,一对大括号“{}”就是类的外壳、边界,它能很好地把类的各种属性及行为包裹起来,将它们封装在类内部并固化成一个整体。封装好的类如同一个黑匣子,外部无法看到内部的构造及运转机制,而只能访问其暴露出来的属性或方法。需要注意的是,我们千万不要过度设计、过度封装,更不要东拉西扯、乱攀亲戚,比如把台灯、轮子、茶杯等物品封装在一起,或者在计算机主机里封装一个算盘。如果把一些不相干的对象硬生生封装在一起,就会使代码变得莫名其妙,难于维护与管理,所谓“物极必反,过犹不及”,所以封装一定要适度。

继承是非常重要的面向对象特性,如果没有它,代码量会变得非常庞大且难以维护、修改。继承可以使父类的属性和方法延续到子类中,这样子类就不需要重复定义,并且子类可以通过重写来修改继承而来的方法实现,或者通过追加达到属性与功能扩展的目的。从某种意义上讲,如果说类是对象的模板,那么父类(或超类)则可以被看作模板的模板。

生物一代一代延续是靠什么来保持父辈的特征呢?没错,答案就是遗传基因DNA,如图1-4所示。正所谓“龙生龙凤生凤,老鼠的儿子会打洞”,如果没有这个遗传机制,代码量就会急剧增大,很多功能、资源都会出现重复定义的情况,这样就会造成极大的冗余和资源的浪费,所以受自然界的启发,面向对象就有了继承机制。

图1-4 生物的遗传基因

举个例子,儿子从父亲那里继承了一些东西,就不需要通过别的方式获得了,如继承家产。再举个例子,我们知道,狗是人类忠实的朋友,它们在一万多年的进化过程中不断繁衍,再加上人类的培育,衍生出许多品种,如图1-5所示。

图1-5 犬类的继承

基于图1-5所示的继承关系,我们思考一下如何用代码来建模,倘若为每个犬类品种都定义一个类并封装各自的属性和方法,这显然不行,因为类一多势必会造成代码泛滥。其实,不管是什么犬类品种,它们都有某些共同的特征与行为,如吠叫行为等,所以我们需要把犬类共有的基因抽离出来,并封装到一个犬类祖先中以供后代继承,请参看代码清单1-1。

代码清单1-1 犬类的祖先Dog

1.  public class Dog {
2.    protected String breeds;//品种
3.    protected boolean sex;//性别
4.    protected String color;//毛色
5.    protected int age;//年龄
6.
7.    public Dog(String breeds) { 
8.      this.age = 0; //初始化为0岁
9.      this.breeds = breeds; //初始化犬类品种
10.   }
11.
12.   public void bark(){//吠叫
13.     System.out.println("汪汪汪");
14.   }
15.
16.   public String getBreeds() {
17.     return breeds;
18.   }
19.
20.   /*假设自出生后就不可以变种了,那么此处不应暴露setBreeds方法
21.   public void setBreeds(String breeds) {
22.     this.breeds = breeds;
23.   }
24.   */
25.   public boolean isSex() {
26.     return sex;
27.   }
28.
29.   public void setSex(boolean sex) {
30.     this.sex = sex;
31.   }
32.
33.   public String getColor() {
34.     return color;
35.   }
36.
37.   public void setColor(String color) {
38.     this.color = color;
39.   }
40.
41.   public int getAge() {
42.     return age;
43.   }
44.
45.   public void setAge(int age) {
46.     this.age = age;
47.   }
48. }

如代码清单1-1所示,我们为犬类定义了品种、性别、毛色、年龄这4个属性,并且带有相应的setter方法和getter方法。第12行的吠叫方法是犬类的共有行为,理所当然能被子类继承。需要注意的是,倘若我们把犬类属性的访问权限由“protected”改为“private”,就意味着子类不能再直接访问这些属性了,但这并无大碍,最终子类依旧可以通过继承而来的并且声明为“public”的getter方法和setter方法去间接访问它们。好了,接下来我们用子类哈士奇类来说明如何继承,请参看代码清单1-2。

代码清单1-2 哈士奇类Husky

1.  public class Husky extends Dog {
2.
3.    public Husky() {
4.      super("哈士奇");
5.    }
6.
7.    public void sleighRide() {//拉雪橇
8.      System.out.println("拉雪橇");
9.    }
10.
11. }

如代码清单1-2所示,为了延续父类的基因,哈士奇类在第一行的类定义后用“extends”关键字声明了对父类Dog的继承。第4行以“super”关键字调用了父类的构造方法,并初始化了狗的品种breeds为“哈士奇”,当然年龄一并会被父类初始化为0岁。我们可以看到哈士奇类的代码已经变得特别简单了,既没有定义任何getter方法或setter方法,又没有定义吠叫方法,而当我们调用这些方法时却能神奇般地得到结果,这是因为它继承了父类的方法,不需要我们重新定义。只是能够单单地继承父类是不够的,哈士奇类还应该有自己的特色,这就要增加其自己的属性、方法,在代码第7行中我们增加了哈士奇类所特有的“拉雪橇”行为,这是父类所不具有的。除此之外,哈士奇吠叫起来比较特殊,这可能是基因突变或者是返祖现象所致,这时我们甚至可以重写吠叫方法以让它发出狼的叫声。其他子类的继承也可以各尽其能,比如贵宾犬可以作揖,藏獒可以看家护院等,读者可以自己发挥。总之,继承的目的并不只是全盘照搬,而是可以基于父类的基因灵活扩展。

 

 扩展阅读

我们知道任何类都有一个toString()方法,但我们根本没有声明它,这是为什么呢?其实这是从Object类继承的方法,因为Object是一切类的祖先类。

 

众所周知,在我们创建对象的时候通常会再定义一个引用指向它,以便后续进行对象操作,而这个引用的类型则决定着其能够指向哪些对象,用犬类定义的引用绝不能指向猫类对象,所以对于父类定义的引用只能指向本类或者其子类实例化而来的对象,这就是一种多态。除此之外,还有其他形式的多态,例如抽象类引用指向子类对象,接口引用指向实现类的对象,其本质上都别无二致。

我们继续以1.2节中的犬类继承为例。如果以犬类Dog作为父类,那么哈士奇、贵宾犬、藏獒、吉娃娃等都可以作为其子类。如果我们定义犬类引用dog,那么它就可以指向犬类的对象,或者其任意子类的对象,也就是“哈士奇是犬类,藏獒是犬类……”。下面我们用代码来表示,请参看代码清单1-3。

代码清单1-3 犬类多态构造示例

1.  Dog dog; //定义父类引用
2.  dog = new Dog();//父类引用指向父类对象(狗是犬类)
3.  dog = new Husky()//父类引用指向子类对象(哈士奇是犬类)
4.
5.  Husky husky = new Dog();//错误:子类引用指向父类对象(犬类是哈士奇)

如代码清单1-3所示,前3行没有任何问题,犬类引用可以指向犬类的对象,也可以指向哈士奇类的对象,这让dog引用变得更加灵活、多变,可以引用任何本类或子类的对象。然而第5行代码则会出错,因为让哈士奇类的引用指向犬类Dog的对象就行不通了,这就好像说“犬类就是哈士奇”一样,逻辑不通。

再进一步讲,多态其实是利用了继承(或接口实现)这个特性体现出来的另一番景象。我们以食物举例,中华美食博大精深,菜品众多且色香味俱全,这都离不开各种各样的食材,如图1-6所示。

图1-6 有机食物的多态性

虽然食材形态各异,但是万变不离其宗,它们都是自然界生长出来的有机生物。而作为人类,我们可以食用哪些食物呢?显而易见,人类只可以食用有机食物,对于金属、塑料等是不能消化的。所以正如图1-7所展示的那样,人类所能接受的食物对象可以是番茄、苹果、牛肉等有机食物的多形态表现,而不能是金属类物质。

图1-7 人类与食物的关系类结构

也许有人会提出疑问,全部用Object类作为引用不是更加灵活,多态性更加丰富吗?其实,任何事物都有两面性,一方面带来了灵活性,而另一方面造成了破坏性。

为了更透彻地理解面向对象的特性,以及设计模式如何巧妙利用面向对象的特性来组织各种模块协同工作,我们就以计算机这个既形象又贴切的例子来切入实战部分。如图1-8所示,相信很多年轻的读者都没有见过这种早期的个人计算机,它的键盘、主机和显示器等都是集成为一体的。

图1-8 老式计算机

越是老式的计算机,其集成度越高,甚至把所有配件都一体化,配件之间的耦合度极高,难以拆分。这种过度封装的计算机为什么会退出历史舞台呢?试想,某天显示器坏了,我们只能把整个机器拆开更换显示器。如果显示器是焊接在主板上的,情况就更糟糕了。缺少接口的设计造成了极高的耦合度,而更糟的是,如果这种显示器已经停产了,那么结果只能是整机换新。

为解决这个问题,设计人员提出了模块化的概念,各种外设如雨后春笋般涌现,如鼠标、键盘、摄像头、打印机、外接硬盘……但这时又出现一个问题,如果每种设备都有一种接口,那么计算机主机上得有多少种接口?这些接口包括串口、并口、PS2接口……接口泛滥将是一场灾难,采用标准化的接口势在必行,于是便有了现在的USB接口。USB提供了一种接口标准:电压5V,双工数据传输,最重要的是其物理形态上的统一规范,只要是USB标准,设备就可以进行接驳,最终计算机发展成为图1-9所示的样子。

图1-9 现代计算机

我们每天都在接触计算机,对于这种设计可能从未思考过。为了便于理解,我们让计算机和各种外设鲜活起来,下面是它们之间展开的一场精彩对话,其中的角色包括一台计算机,一个USB接口,还有几个USB设备,故事就这样开始了。

计算机:“我宣布,从现在开始USB接口晋升为我的秘书,我只接收它传递过来的数据,谁要找我沟通必须通过它。”

USB接口:“我不关心要接驳我的设备是什么,但我规定你必须实现我定义的readData()这个方法,但具体怎样实现我不管,总之我会调用你的这个方法把数据读取过来。”

USB键盘:“我有readData(data Data)这个方法,我已经实现好了,传过去的是用户输入的字符。”

USB鼠标:“我也一样,但传过去的是鼠标移动或点击数据。”

USB摄像头:“没错,我也实现了这个方法,只是我的数据是视频流相关的。”

USB接口:“不管你们是什么类型的数据,只要传过来转换成Data就行了,我接收你们的接驳请求,除了PS2鼠标。”

PS2鼠标:“@计算机,老大,这怎么办?你找来的这个USB接口太霸道了,我们根本无法沟通,你们不能尊重一下老人吗?”

计算机:“你自己想办法,要顺应时代潮流,与时俱进。”

PS2鼠标:……

通过这场对话,我们对计算机和外设以及它们之间的关系有了更深刻的认识。计算机中装了一个USB接口,这就是“封装”,而键盘、鼠标及摄像头都是USB接口的实现类,从广义上理解这就是一种“继承”,所以计算机的USB接口就能接驳各种各样的USB设备,这就是“多态”。我们来看它们的类结构,如图1-10所示。

图1-10 现代计算机的类结构

通过对计算机接口的抽象化、标准化,我们对各个模块重新分类、规划,并合理封装,最终实现计算机与外设的彻底解耦。多态化的外设使计算机功能更加强大、灵活、可扩展、可替换。其实这就是设计模式中非常重要的一种“策略模式”,接口的定义是解决耦合问题的关键所在。但对于一些老旧的接口设备模块,我们暂时还无法使用,正如同上面故事里那个可怜的PS2鼠标。

我们都知道有一种设备叫转换器,它能轻松地将老旧的接口设备调制适配到新的接口,以达到兼容的目的,这就是“适配器模式”。这些设计模式后续都会被讲到,我们会由浅入深、一步一个脚印地逐个解析。读者一定要边学边思考,理论一定要与实践结合才能举一反三、融会贯通,如此才能合理有效地利用设计模式设计出更加优雅、健壮、灵活的应用程序。



单例模式(Singleton)是一种非常简单且容易理解的设计模式。顾名思义,单例即单一的实例,确切地讲就是指在某个系统中只存在一个实例,同时提供集中、统一的访问接口,以使系统行为保持协调一致。singleton一词在逻辑学中指“有且仅有一个元素的集合”,这非常恰当地概括了单例的概念,也就是“一个类仅有一个实例”。

盘古开天,造日月星辰。从“夸父逐日”到“后羿射日”,太阳对于我们的先祖一直具有着神秘的色彩与非凡的意义。随着科学的不断发展,我们逐渐揭开了太阳系的神秘面纱。我们可以把太阳系看作一个庞大的系统,其中有各种各样的对象存在,丰富多彩的实例造就了系统的美好。这个系统里的某些实例是唯一的,如我们赖以生存的恒星太阳,如图2-1所示。

图2-1 太阳系

与其他行星或卫星不同的是,太阳是太阳系内唯一的恒星实例,它持续提供给地球充足的阳光与能量,离开它地球就不会有今天的勃勃生机,但倘若天上有9个太阳,那么将会带来一场灾难。太阳东升西落,循环往复,不多不少仅此一例。

既然太阳系里只有一个太阳,我们就需要严格把控太阳实例化的过程。我们从最简单的开始,先来写一个Sun类。请参看代码清单2-1。

代码清单2-1 太阳类Sun

1.  public class Sun {
2.   
3.  }

如代码清单2-1所示,太阳类Sun中目前什么都没有。接下来我们得确保任何人都不能创建太阳的实例,否则一旦程序员调用代码“new Sun()”,天空就会出现多个太阳,便又需要“后羿”去解决了。有些读者可能会疑惑,我们并没有写构造器,为什么太阳还可以被实例化呢?这是因为Java可以自动为其加上一个无参构造器。为防止太阳实例泛滥将世界再次带入灾难,我们必须禁止外部调用构造器,请参看代码清单2-2。

代码清单2-2 太阳类Sun

1.  public class Sun {
2.  
3.      private Sun(){//构造方法私有化
4.      
5.      }
6.  
7.  }

如代码清单2-2所示,我们在第3行将太阳类Sun的构造方法设为private,使其私有化,如此一来太阳类就被完全封闭了起来,实例化工作完全归属于内部事务,任何外部类都无权干预。既然如此,那么我们就让它自己创建自己,并使其自有永有,请参看代码清单2-3。

代码清单2-3 太阳类Sun

1.  public class Sun {
2.      
3.      private static final Sun sun = new Sun();//自有永有的单例
4.  
5.      private Sun(){//构造方法私有化
6.      
7.      }
8.  
9.  }

如代码清单2-3所示,代码第3行中“private”关键字确保太阳实例的私有性、不可见性和不可访问性;而“static”关键字确保太阳的静态性,将太阳放入内存里的静态区,在类加载的时候就初始化了,它与类同在,也就是说它是与类同时期且早于内存堆中的对象实例化的,该实例在内存中永生,内存垃圾收集器(Garbage Collector,GC)也不会对其进行回收;“final”关键字则确保这个太阳是常量、恒量,它是一颗终极的恒星,引用一旦被赋值就不能再修改;最后,“new”关键字初始化太阳类的静态实例,并赋予静态常量sun。这就是“饿汉模式”(eager initialization),即在初始阶段就主动进行实例化,并时刻保持一种渴求的状态,无论此单例是否有人使用。

单例的太阳对象写好了,可一切皆是私有的,外部怎样才能访问它呢?正如同程序入口的静态方法main(),它不需要任何对象引用就能被访问,我们同样需要一个静态方法getInstance()来获取太阳的单例对象,同时将其设置为“public”以暴露给外部使用,请参看代码清单2-4。

代码清单2-4 太阳类Sun

1.  public class Sun {
2.  
3.      private static final Sun sun = new Sun();//自有永有的太阳单例
4.  
5.      private Sun(){//构造方法私有化
6.      
7.      }
8.  
9.      public static Sun getInstance(){//阳光普照,方法公开化
10.         return sun;
11.     }
12.  
13. }

如代码清单2-4所示,太阳单例类的雏形已经完成了,对外部来说只要调用Sun.getInstance()就可以得到太阳对象了,并且不管谁得到,或是得到几次,得到的都是同一个太阳实例,这样就确保了整个太阳系中恒星太阳的唯一合法性,他人无法伪造。当然,读者还可以添加其他功能方法,如发光和发热等,此处就不再赘述了。

至此,我们已经学会了单例模式的“饿汉模式”,让太阳一开始就准备就绪,随时供应免费日光。然而,如果始终没人获取日光,那岂不是白造了太阳,一块内存区域被白白地浪费了?这正类似于商家货品滞销的情况,货架上堆放着商品却没人买,白白浪费空间。因此,商家为了降低风险,规定有些商品必须提前预订,这就是“懒汉模式”(lazy initialization)。沿着这个思路,我们继续对太阳类进行改造,请参看代码清单2-5。

代码清单2-5 太阳类Sun

1.  public class Sun {
2.  
3.      private static Sun sun;//这里不进行实例化
4.  
5.      private Sun(){//构造方法私有化
6.      
7.      }
8.  
9.      public static Sun getInstance() {
10.         if (sun == null) {//如果无日才造日
11.             sun = new Sun();
12.         }
13.         return sun;
14.     }
15.  
16. }

如代码清单2-5所示,可以看到我们一开始并没有造太阳,所以去掉了关键字final,只有在某线程第一次调用第9行的getInstance()方法时才会运行对太阳进行实例化的逻辑代码,之后再请求就直接返回此实例了。这样的好处是如无请求就不实例化,节省了内存空间;而坏处是第一次请求的时候速度较之前的饿汉初始化模式慢,因为要消耗CPU资源去临时造这个太阳(即使速度快到可以忽略不计)。

这样的程序逻辑看似没问题,但其实在多线程模式下是有缺陷的。试想如果是并发请求的话,程序第10行的判空逻辑就会同时成立,这样就会多次实例化太阳,并且对sun进行多次赋值(覆盖)操作,这违背了单例的理念。我们再来改良一下,把请求方法加上synchronized(同步锁)让其同步,如此一来,某线程调用前必须获取同步锁,调用完后会释放锁给其他线程用,也就是给请求排队,一个接一个按顺序来,请参看代码清单2-6。

代码清单2-6 太阳类Sun

1.  public class Sun {
2.  
3.      private static Sun sun;//这里不进行实例化
4.  
5.      private Sun(){//构造方法私有化
6.      
7.      }
8.  
9.      public static synchronized Sun getInstance() {//此处加入同步锁
10.         if (sun == null) {//如果无日才造日
11.             sun = new Sun();
12.         }
13.         return sun;
14.     }
15.  
16. }

如代码清单2-6所示,我们将太阳类Sun中第9行的getInstance()改成了同步方法,如此可避免多线程陷阱。然而这样的做法是要付出一定代价的,试想,线程还没进入方法内部便不管三七二十一直接加锁排队,会造成线程阻塞,资源与时间被白白浪费。我们只是为了实例化一个单例对象而已,犯不上如此兴师动众,使用synchronized让所有请求排队等候。所以,要保证多线程并发下逻辑的正确性,同步锁一定要加得恰到好处,其位置是关键所在,请参看代码清单2-7。

代码清单2-7 太阳类Sun

1.  public class Sun {
2.  
3.      private volatile static Sun sun;
4.  
5.      private Sun(){//构造方法私有化
6.      
7.      } 
8.  
9.      public static Sun getInstance() {//华山入口
10.         if (sun == null) {//观日台入口
11.             synchronized(Sun.class){//观日者进行排队
12.                if (sun == null) {
13.                    sun = new Sun();//只有排头兵造了太阳,旭日东升
14.                }
15.             }
16.         }
17.         return sun; //……阳光普照,其余人不必再造日
18.     }
19. }

如代码清单2-7所示,我们在太阳类Sun中第3行对sun变量的定义不再使用final关键字,这意味着它不再是常量,而是需要后续赋值的变量;而关键字volatile对静态变量的修饰则能保证变量值在各线程访问时的同步性、唯一性。需要特别注意的是,对于第9行的getInstance()方法,我们去掉了方法上的关键字synchronized,使大家都可以同时进入方法并对其进行开发。请仔细阅读每行代码的注释,有些人(线程)起早就是为了观看日出,那么这些人会通过第10行的判空逻辑进入观日台。而在第11行我们又加上了同步块以防止多个线程进入,这就类似于观日台是一个狭长的走廊,大家排队进入。随后在第12行我们又进行一次判空逻辑,这就意味着只有队伍中的第一个人造了太阳,有幸看到了日出的第一缕阳光,而后面的人则统统离开,直到第17行得到已经造好的太阳,如图2-2所示。

图2-2 观日台

随后发生的事情我们就可以预见了,太阳高高升起,实例化操作完毕,起晚的人们都无须再进入观日台,直接获取太阳实例就可以了,阳光普照大地,将温暖洒向人间。

大家注意到没有,我们一共用了2个嵌套的判空逻辑,这就是懒加载模式的“双检锁”:外层放宽入口,保证线程并发的高效性;内层加锁同步,保证实例化的单次运行。如此里应外合,不仅达到了单例模式的效果,还完美地保证了构建过程的运行效率,一举两得。

相比“懒汉模式”,其实在大多数情况下我们通常会更多地使用“饿汉模式”,原因在于这个单例迟早是要被实例化占用内存的,延迟懒加载的意义并不大,加锁解锁反而是一种资源浪费,同步更是会降低CPU的利用率,使用不当的话反而会带来不必要的风险。越简单的包容性越强,而越复杂的反而越容易出错。我们来看单例模式的类结构,如图2-3所示。单例模式的角色定义如下。

图2-3 单例模式的类结构

除了“饿汉”与“懒汉”这2种单例模式,其实还有其他的实现方式。但万变不离其宗,它们统统都是由这2种模式发展、衍生而来的。我们都知道Spring框架中的IoC容器很好地帮我们托管了业务对象,如此我们就不必再亲自动手去实例化这些对象了,而在默认情况下我们使用的正是框架提供的“单例模式”。诚然,究其代码实现当然不止如此简单,但我们应该追本溯源,抓住其本质的部分,理解其核心的设计思想,再针对不同的应用场景做出相应的调整与变动,结合实践举一反三。


相关图书

YOLO目标检测
YOLO目标检测
深入Activiti流程引擎:核心原理与高阶实战
深入Activiti流程引擎:核心原理与高阶实战
Serverless核心技术和大规模实践
Serverless核心技术和大规模实践
深入浅出Windows API程序设计:编程基础篇
深入浅出Windows API程序设计:编程基础篇
Spring Cloud微服务架构实战
Spring Cloud微服务架构实战
读源码学架构:系统架构师思维训练之道
读源码学架构:系统架构师思维训练之道

相关文章

相关课程