图解设计模式,看完秒懂!!!

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

点击关注公众号,实用技术文章及时了解图解设计模式,看完秒懂!!!

在本公号连载的《设计模式是什么鬼》终于出书了,由人民邮电出版社出版,书名:秒懂设计模式,作者:刘韬,笔名:凸凹。以下节选自本书第一章“初探”,这里免费提供大家学习交流。

文末抽奖送5本《秒懂设计模式》书籍,欢迎大家参与,明天12点开奖!

图解设计模式,看完秒懂!!!
图解设计模式,看完秒懂!!!

第1章   初探

图解设计模式,看完秒懂!!!

在这个软件发展日新月异的时代,软件产品推陈出新应接不暇,软件需求更是变化莫测,难以预判。作为技术人员,我们在软件开发过程中常常会遇到代码重复的问题,这就不得不对系统进行改动、重构,所以良好稳固的软件架构就显得至关重要。设计模式正是为了解决这些问题,它针对各种场景提供了最适合的代码模块复用解决方案。

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

在学习设计模式之前我们得先搞清楚到底什么是面向对象。我们都生活在现实世界里,这里充满了各种对象,如本章封面图所示,其中的山川河流、花鸟鱼虫、高楼大厦、车水马龙,我们每天都要面对他们,与他们沟通、互动,这是对面向对象最简单的理解。为了将现实世界重现于计算机世界中,我们想了各种方法针对这些对象建立数字模型(类),但是理想很丰满而现实很残酷,我们永远也无法包罗万象这大千世界中的万事万物。而当人们在“造物”过程中发现其实类与类之间有着千丝万缕的关联,于是想到面向对象特有的编程方法,利用封装、继承、多态的方法去建模,大量的减少了重复代码、降低类间耦合,像拼积木一样组装了整个“世界”。“封装”、“继承”和“多态”就是面向对象的3大特性,这些都是设计模式的基石,我们必须要理解掌握。

1.1 封装

对于封装的概念,我们就以现实世界中的事物举例,比如胶囊对于各类药物混合的封装;钱包对于现金、身份证及银行卡的封装;电脑机箱对于主板、CPU及内存等零件的封装等等,封装可以说在我们生活中比比皆是、随处可见。我们举一个现实生活中最常见的例子,我们都用吸管喝过饮料吧,如图1-2所示。

图解设计模式,看完秒懂!!!
图1-1 饮料的封装

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

我们再来思考一下对电脑主机的封装,它必然是需要一个机箱外壳把各种配件封装进去的,比如主板、CPU、内存、显卡、硬盘等等。这样一方面是起到保护作用,防止乱七八糟的东西肆意进入内部破坏电路,比如老鼠昆虫什么的,另一方面,机箱也不是完全封闭,它一定对外预留有一些访问接口,比如各种开机、重启按钮等,这样用户才能够使用电脑,请参看电脑主机的类图,如图1-2所示。

图解设计模式,看完秒懂!!!
图1-2 电脑主机类图

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

图解设计模式,看完秒懂!!!
图1-3 弹药的发展

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

在Java编程语言中,一对大括号“{}”就是类的外壳、边界,它能很好地把类的各种属性以及行为包裹起来,封装在类内部固化下来并成为一个整体。在外部看来,封装好的类则被视为一个黑匣子,无法看到其内部的构造及运转机制,外部只需要访问其暴露出来的属性或方法即可。需要注意的是,我们千万不要过度设计、过度封装,更不要东拉西扯、乱攀亲戚,比如把台灯、轮子、茶杯等物品封装在一起,或者电脑主机里封装一个算盘这类荒谬的做法,把一些业务不相干的对象硬生生封装在一起,做大做全,结果代码变得臃肿不堪,类结构复杂而难于管理,过犹而不及,所以封装一定要适可而止。

1.2 继承

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

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

图解设计模式,看完秒懂!!!
图1-4 生物的遗传基因

儿子从父亲那继承下来一些东西,不需要自己再去拼搏了,避免重复造轮子,比如富二代继承家产,生来家财万贯;再比如天生一副好嗓子,因为妈妈是歌星。所以不管是身体上还是财富上,继承能让子孙后代得到父辈基业的传承。以我们最常见的动物举例,狗是人类最忠实的朋友,它们在一万多年的演变过程中不断繁衍,再加上人类的培育诞生了许多各色各样的品种,如图1-5所示。

图解设计模式,看完秒懂!!!
图1-5 犬类的继承

我们思考一下如何用代码来建模,倘若为每个犬类品种都定义一个类并封装各自的属性方法的话,不言而喻这必然会造成代码泛滥。我们就以最典型常用的类继承来举例,不管是什么犬类品种,它们必然都有某些共同的特征与行为,比如性别属性啊,吠叫行为啊等等,所以我们需要把犬类公有的基因给抽离出来,并封装到一个犬类祖先中以供后代继承,请参看代码清单1-1。

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


public class Dog {
  protected String breeds;//品种
  protected boolean sex;//性别
  protected String color;//毛色
  protected int age;//年龄

  public Dog(String breeds) 
    this.age = 0//初始化为0岁
    this.breeds = breeds; //初始化犬类品种
  }

  public void bark(){//犬吠
    System.out.println("汪汪汪");
  }

  public String getBreeds() {
    return breeds;
  }

  /*假设自诞生后就不可以变种了,所以此处不应暴露setBreeds方法。
  public void setBreeds(String breeds) {
    this.breeds = breeds;
  }
  */

  public boolean isSex() {
    return sex;
  }

  public void setSex(boolean sex) {
    this.sex = sex;
  }

  public String getColor() {
    return color;
  }

  public void setColor(String color) {
    this.color = color;
  }

  public int getAge() {
    return age;
  }

  public void setAge(int age) {
    this.age = age;
  }
}

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

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

public class Husky extends Dog {

  public Husky() {
    super("哈士奇");
  }

  public void sleighRide() {//拉雪橇
    System.out.println("拉雪橇");
  }

}

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

扩展阅读

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

1.3 多态

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

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

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

Dog dog; //定义父类引用
dog = new Dog();//父类引用指向父类对象(狗是犬类)
dog = new Husky()//父类引用指向子类对象(哈士奇是犬类)

Husky husky = new Dog();//错误:子类引用指向父类对象(犬类是哈士奇)

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

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

图解设计模式,看完秒懂!!!
图1-6 有机食物的多态性

虽然说食材形态各异,但是万变不离其宗,它们都是自然界生长出来的有机生物。而作为人类,我们可以食用哪些东西?显而易见人类只可以食用机食物类,对于金属、塑料等是不能消化的。所以正如人类与食物的关系类图1-7所描述的那样,我们的食物一定可以是蔬菜、水果、肉等有机食品的多形态表现,而决不能食用金属类物质。

图解设计模式,看完秒懂!!!
图1-7 人类与食物类图

我们在设计的时候,对多态的把握一定要适可而止,切勿无限放大,否则通通用Object作为引用不就更加灵活,多态性更加丰富吗?所以说物极必反,过于灵活的自由散漫会有失规范性,无规矩则不成方圆,越泛化不代表越好,适可而止才是最好的。

1.4 电脑与外设

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

图解设计模式,看完秒懂!!!
图1-8 老式电脑

越是古老的电脑越是高度集成,恨不得把所有配件都一体化,配件之间的耦合性极大,如胶似漆难以拆分。这种过度封装的电脑为什么会退出历史舞台?试想,某天显示器坏掉了麻烦就来了,我们只能去送修,然后把整个壳子拆开进行更换,如果显示器是焊接在主板上的那就更惨了,缺少接口的设计造成了极大的耦合度,而更糟的是这种显示器已经停产了根本买不到新的来匹配,结果只能整机换新。

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

图解设计模式,看完秒懂!!!
图1-9 现代电脑设计

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

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

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

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

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

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

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

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

Computer电脑:“你自己想办法,要顺应时代潮流,与时俱进。”

PS2鼠标:“……”

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

图解设计模式,看完秒懂!!!
图1-10 新型电脑类图

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

然而,我们都知道有一种设备叫转换器,它能轻松地将老旧的接口设备调制适配到新的接口达到兼容的目的,这就是“适配器模式”了。当然,这些设计模式之后都会被讲到,我们不要想一口就吞个胖子,学习需要由浅入深一步一个脚印才能将它们逐个攻破,并且理论与实践相结合,最终融会贯通于实际项目中去,这样才能设计出更加优雅、健壮、灵活的应用程序。

书籍目录

图解设计模式,看完秒懂!!!

另外,随机送出5本,抽奖即可,明天中午12点开奖,中奖者把收货信息及时写入,三天内发出!

图解设计模式,看完秒懂!!!

对本书有兴趣,扫码可以直接购买!

图解设计模式,看完秒懂!!!

原文始发于微信公众号(Java知音):图解设计模式,看完秒懂!!!