一、设计模式概述
设计模式分很多种,每种一般都用于解决某个软件开发过程中的问题。许多人认为设 计模式有23种,其实,对于这个数字也没必要那么教条,当然还有更多的设计模式种类,只 不过是这23种比较经典而已。甚至可以说,如果你有很丰富的程序设计经验,那么你发明 自己的设计模式也没问题。
模式,是指事物的标准样式或者针对特定问题的可重用解决方案。换句话说,遇到这类 问题,用这种解决方法;遇到那类问题,用那种解决方法;遇到不同的问题,用不同的解决 方法——这就是模式。
再回到“设计模式”这个词上来,这个词的英文是Design Pattern,以下几种对“设计模 式”的解释都可以
- 设计模式,是一套被反复使用的代码设计经验的总结,是经过提炼的出色的设计 方法。
- 设计模式,是程序员在长期的开发实践中总结出的一套提高开发效率的编程方法。
- · 设计模式,代表了一些解决常见问题的通用做法,体现着人们尝试解决某些问题时
的智慧。所以,它是一种强大的管理复杂度的工具。 · 设计模式,是在特定问题发生时的可重用解决方案。 - 一个设计模式用来描述几个模块或类对象之间关系、职责,以及它们之间如何进行 分工与合作。 一个甚至几个设计模式共同配合来解决软件设计中面对的实际问题。
- 设计模式在比编程语言惯用手法更高的层面来描述解决特定类型问题的途径。
- 设计模式用来描述在软件系统中如何通过管理代码的相互依赖性来管理复杂性。
使用设计模式的主要目的是在设计大型项目时,保证所设计的模块之间的代码灵活性 和可复用性,但是毫无疑问,这两个特性都以增加复杂性作为代价。
- 灵活性(可扩展性/低耦合性)
- 谈到灵活性这件事,可以想象一下3D 网络游戏里面的人物,刚出现在游戏世界中的时 候,人物形象可能是光着膀子;
- 当人物达到5级的时候,玩家给人物买了一件布衣,穿上之 后,人物的形象发生了改变;当人物达到10级的时候,玩家给人物买了一件皮衣,穿上之 后,人物的形象又发生了改变,这就是人物形象灵活性的体现。
- 在制作游戏中人物的时候,先做一个裸体的人物模型,衣服也单独做成模型,做一件 布衣模型,做一件皮衣模型。
- 当人物穿布衣的时候,通过编写程序,把布衣模型贴到人物模型上。当人物改穿皮 衣的时候,依旧通过编写程序,把布衣模型从人物模型上摘下来,把皮衣模型贴到人 物模型上去。从而实现了只要变换衣服模型就可以改变整个人物外观的需求。
- 反之,如果把人物模型和衣服模型做到一起,成为一个整体的话,那就面临着人物+布 衣要做一个模型,人物+皮衣要做一个模型……这不但劳民伤财,而且人物模型载入(显示 到屏幕)的时间一般会比较长,可能造成游戏卡顿,影响玩家体验。所以,除非有十分必要的 理由,否则应该尽可能选择这种具有灵活性的解决方案。
总结一下,所谓灵活性可以理解为两点:
- 修改现有的某部分内容不会影响其他部分的内容(影响面尽可能窄或者说尽量将修 改的代码集中在一起,不希望大范围修改代码)。
- 增加新内容的时候尽量少甚至不需要改动系统现有的内容。
- 可复用性
刚才谈到了《设计模式——可复用面向对象软件的基础》一书,从书名中,可以提取出来 两个非常重要的词:可复用和面向对象。
- 可复用:可以重复使用,可以到处用(可以被很多地方调用)。
- 面向对象有三大特性———封装性、继承性、多态性。其中的多态性对于学好设计模式非 常重要,读者一定要好好理解,可以仔细阅读《C++ 新经典:对象模型》 ——其中对多态性有 相当深入的解释。
C++支持很多种风格的程序设计方法,或者说C++支持很多种编程模型:
- (1)面向过程;
- (2)基于对象;
- (3)面向对象(基于对象的编程模型融入继承性和多态性后形成的);
- (4)泛型编程等。通常情况下,设计模式是指面向对象这种编程模型下的设计模式,组合使用各种设计模式来进行面向对象程序设计。但是,设计模式并不等价于面向对象的设 计模式,也就是说,脱离面向对象这个概念,设计模式的概念也可以单独存在。这里不多谈, 读者在日后工作中遇到类似的问题时可以再进行深入的研究。
灵活性和可复用性两者是相辅相成的,没必要分开理解,可复用也意味着很灵活,灵活 性也意味着可复用。设计模式也被称为微架构(Micro-Architecture), 各种设计模式的组合 运用可以生成各种新的架构。
1.1 设计模式中的抽象思维
下面先解释一下“耦合”和“解耦合”这两个概念。
- 耦合:两个模块相互依赖,修改其中的一个模块,则另外一个也要修改,这两者之间 存在的互相影响关系就叫作两个模块之间存在耦合关系。
- 解耦合:通过修改程序代码,切断两个模块之间的依赖关系,使得对于任意一个模 块的更改都不会影响另外一个模块,这就叫两个模块之间已经解耦合了。
回归到“抽象思维”这个话题中来——抽象思维在设计模式中的运用非常重要,抽象思 维强调对象的本质属性,主要应用于一些软件设计中的解耦合过程。
1. 抽象思维的概念
什么叫抽象思维呢?对于一个事物,所谓的抽象思维,是指能从这个事物中抽取出或者 说提炼出一些本质的、共性的内容,把这些共性的内容组合到一起(封装)。
例如狗、猫、猪,针对这3种动物,看一看怎样把它们的一些共性的内容提取出来做个抽 象(抽象成一个基类或者说基类本身就是一种抽象)。
- (1)它们都是动物,它们都要吃,都要喝,这是它们的共性,所以可以抽象出一个叫作动 物的类,吃、喝都可以作为动物这个类成员函数。
class Animal
{
public:
void eat()
{
}
void drink()
{
}
};
- (2)当然,狗、猫、猪还有各自不同的特点,例如狗可以看家,猫可以抓老鼠,猪可以养肥 吃肉,等等,不过这些内容不在抽象思维这个话题中,故不多探讨。
上面举出的这个抽象思维的小范例比较简单和显而易见,所以比较好理解,但是在一个 大型的复杂项目中,要利用抽象思维把一些事务的共性提取出来,并不是一件容易的事。从 编程思维的角度来思考,下面两种解决问题复杂性的方法可以借鉴。 - 分解法:把一个复杂的事物分解成若干比较简单的事物。因为人们能够更轻松地 理解多个简单的事物而不是一个复杂的事物。
- 抽象法:从每个简单的事物中,抽象出本质的内容并封装起来。抽象思维的能力因人而异,跟传统教育有关系,跟后天的培养也有关系,总之是一个复 杂的事情。
毫无疑问,设计模式是很依赖抽象思维的——尽量尝试把一个事物的本质内容抽取出 来。所以,学习设计模式的过程,也是一个不断提高自己抽象思维能力的过程。
- 抽象思维的目的
那么,为什么要利用抽象思维把事务中一些本质、共性的内容提取出来?这样做有什么 好处呢?
假设,读者是一个农场主,农场里目前有3种动物,狗、猫、猪,现在要对这3种动物进行 管理。从写程序的角度来讲,可以对这3种动物分别创建类:狗、猫、猪。对于这3种动物 类,每个类都要实现一些成员函数(方法),例如狗,要实现的成员函数有———吃、喝、看家; 猫,要实现的成员函数有——吃、喝、捉老鼠;猪,要实现的成员函数有——吃、喝、被屠宰。
除了对这3种动物分别创建3个类之外,是否还有更好的程序写法呢?
仔细想想,不难 发现,在狗、猫、猪这3种动物的类中,都有吃、喝这两个成员函数。但是,每个类又分别有不 同的成员函数,例如狗的专有成员函数是“看家”,而猫的专有成员函数是“捉老鼠”,猪的专 有成员函数是“被屠宰”。有了这些想法,就可以重新设计这些类,设计的原则就是减少代码 的重复性,方便代码的扩展( 例如日后可以灵活地加入新的动物品种)。
- 吃、喝这两个成员函数既然每种动物都有,那么可以专门创建一个动物类(Animal),
把吃、喝这两个成员函数放进去。这就不用在狗、猫、猪类里面都写吃、喝这两个成 员函数,从而减少了代码的重复性。这里,就抽象出了动物类,做抽象的原则是把比 较稳定的、不怎么变化的内容作为一个模块,单独定义出来。 - 分别定义狗、猫、猪这3个类,将这3个类定义为继承自动物类,那么这3个子类就 自动拥有了“吃""喝”这两个成员函数。
- 在父类(动物类)中定义一个“用途”成员函数,这个成员函数应该设计为一个虚函 数 。之后,在狗、猫、猪这3个子类中分别实现“用途”成员函数,例如,狗的用途是看 家,猫的用途是捉老鼠,猪的用途是被屠宰,等等。换句话说,不同种类的动物,它们 的用途是不同的、是变化的,所以要把变化的这部分内容放到每个子类中去实现。
请读者想一想:这种设计方式是不是更好?是否起到了减少代码重复性,方便代码扩 展性的目的呢?
- 抽象思维的检验
对于一个项目,把哪些内容抽象出来封装成一个类,是一件很主观的事情,没有统一的 设计标准,不同的人有不同的做法。那么怎么检验某种抽象是否合适?
面对一个项目时,如果项目的某些需求发生更改(实际上软件领域的需求变更是经 常发生的),不更改现有的代码,通过增加新代码应对需求变更。例如,农场里增加 一种新动物——公鸡,公鸡的主要用途是报晓,这个时候,可以直接创建一个公鸡 类,同样继承自动物类,然后也在其中实现“用途”成员函数,在这个成员函数中实现 报晓这个功能。
这里进一步提一下继承,继承是面向对象的特性之一,用于表达类与类之间的父子 关系,这种父子关系一般用于表达两种意思:(1)抽象机制———就是上面谈到的,抽取出本质的、共性的内容放到基类中;
(2)可重用机制——基类中的一些内容,直接拿过来使用。
当一个类中内容太多时,就要考虑是否可以将该类进一步拆分。尤其注意不要把一 些毫不相关的内容都写到一个类中。面向对象程序设计有一个设计原则叫“单一职 责原则”,意思就是说一个类只干好一个事情,承担好一种责任,不然就会牵扯太多, 不管哪块需求变化了都需要修改这个类,那就麻烦了。这跟人一样,你又当语文课 代表,又当数学课代表,还兼任班长,那就麻烦了,一会儿班主任找你,一会儿语文老 师找你,一会儿数学老师找你,你就忙不过来了。
1.3 学习设计模式普遍存在的问题
设计模式是一把双刃剑,若用得好,能提高系统的灵活性、可复用性等,但乱用或错用也 会造成系统适用性的降低。很多读者都学习过设计模式,学习之后,普遍存在一些共性 问 题 。
听得懂但不会用
讲解的内容能听懂,能理解,但不知道在具体工作中如何应用或者说无法得心应手地使 用。换句话说,就是知道代码是这样写的,但不知道为什么要这样写代码。
设计模式相关知识的产生,是因为人们进行项目设计时,随着代码规模的逐步增大,逐 步遇到代码灵活性不够、牵一发而动全身的窘况,此时的代码变得越来越难以修改和维护, 为了应对这个问题,人们不得不采取一些必要的手段对代码进行重新设计,提炼其中的设计 规律,最终总结成各种设计模式。
学习设计模式的有效之法是忘记设计模式,先面对具体要解决的问题,只有经历了“遇 到难题 →用了一个很笨重的方法解决,效果很不理想 →采用设计模式解决,效果比较理想” 的步骤,才能对设计模式印象深刻并做到学以致用。学完了之后到处滥用
设计模式是用来解决大型项目设计时遇到的各种代码问题的,换句话说,就是先有问 题,再有设计模式。
但很多程序开发者往往本末倒置, 一个本来没有多少行代码的小项目在编写代码的时 候非要往设计模式上套(上来就用设计模式),非要让代码中充斥各种设计模式,似乎只有这 样才能体现出程序设计者的高、大、上,这是非常错误的做法,也容易导致出现一些很差劲的 设计(差劲的设计是指这种设计的用途非常有限,却因为引入过多的类而大大增加了他人理 解代码的难度和学习时间,把简单问题复杂化了)。当然,这并不是说小项目就不需要设计, 小项目也同样需要先做设计,再动手编码。
设计项目的时候需要遵守几个原则:
- 不要过度设计,把未来十年八年的事都考虑进来,这没必要,能够支撑未来1~2年 的扩展就行了,其实对于一个小项目如果一开始就引入设计模式,往往会过度设计。
- 设计是一件很主观的事,也不是一步到位的事,可能需要不断地调整和修改。因为 随着个人的成长,想法、见识会不断提高,设计也就自然会不断改进(重构)。
- 不要为了用模式而用模式,使用模式之前,必须要考虑该模式是否适合,往往代码的 实用性和易读性更重要。
3. 设计模式无用论
一般来讲,这种观点属于无稽之谈。
在实际工作中,绝大多数的开发者面临的都是几千到几万行代码的项目。这个时候其 实只要代码写得过得去,总是可以把项目完成,确实也不太需要用到设计模式。但是, 一旦 代码达到十几万行甚至二十万行,读者千万不要简单地以为无非就是代码多了10倍而已, 绝不是这样简单计算的。编写2万行代码,确实用不上设计模式也能写得很愉快,但是,编 写20万行代码,如果不用设计模式,到了开发的后期开发者可能会疯掉,那种难以扩展、难 以维护的感觉,会非常让人纠结和痛苦。换句话说,驾驭几十万行代码的项目,需要的是掌 握设计模式这种开发技术(开发技巧)。
其实,很多开发者在编写代码时总会在无意之间就使用到某种设计模式,只不过自己没 有意识到而已。
1.1.4 设计模式的缺点
灵活性和可复用性,显然是设计模式的两大核心优点,但是引入设计模式,也会让程序 员付出代价,对这种代价也应该有所了解。
- 增加程序书写的复杂性:遵从设计模式书写的程序代码往往与普通的程序代码不 太一样,但是经过一定的学习,程序员还是能够渐渐熟悉这种程序书写方式。
- 增加了学习和理解上的负担:为了代码编写的灵活性,在解决一个问题的时候,往 往要引入多个类(而以往可能只需要一个类就能解决问题),这些类之间配合起来实 现指定的功能,这无疑给代码的学习者和阅读者增加了理解负担。所以设计模式的 使用必须要小心谨慎,尽量不要引入不必要的复杂性,从管理的角度上有一句俗话 叫作“用户想要一杯茶,就不要为他创建一个能煮沸海洋的系统”。
- 设计模式的引入会在一定程度上降低程序的运行效率,这是显然的。因为一个功能 要几个类配合起来实现,显然会增加额外的调用类成员函数的时间成本(例如以往 调用一个成员函数实现,现在要调用5个成员函数来实现)。当然, 一般来讲,相比 于设计模式带来的好处,这种时间成本的付出是值得的(除非真正面临效率瓶颈问 题)。如果在一些资源贫瘠又极度注重效率的系统上,那么在开发的时候是否引入 设计模式,就是一个值得考虑的问题了。
1.1.5 设计模式在实际工作中的应用和学习方法
这个问题要分两方面看。
- 日常工作中的小项目
在日常的小项目开发中,用到设计模式知识的机会并不多。主要原因有如下几点:
- 所开发的项目规模偏小,可能只有几千行代码,当需求变动时,可以方便地修改 代 码 。
- 项目的逻辑功能比较单一,只用于解决眼下问题,不需要将编写的代码提供给他人 复 用 。
- 即便是这样的小项目,开发者可能也在无意中使用了一些设计模式,只不过开发者 没意识到使用的是哪种设计模式。
- 大型应用项目和框架类项目
一旦应用项目较大,例如达到5万行甚至10万行代码,向其中增加新的代码和新的功 能变得越发困难,项目变得越来越笨重,如果不精心设计项目的结构,就会导致出现大量的 冗余代码,灵活性尽失。
对于这种被诸多项目使用的框架类项目,就更需要具备极高的灵活性和可复用性,适应 各种不同的环境、不同的操作系统平台。 框架可以理解成多种设计模式的综合运用所生成 的半成品,开发人员需要向其中增加更多的实现代码以最终形成成品应用程序。
所以,在大型项目和框架类项目中,才是设计模式真正发挥重要作用的舞台。 那么,对于设计模式的学习和掌握,建议采用如下步骤和方法:
- (1)掌握设计模式的基本概念和该设计模式要解决的具体问题,这样当碰到类似问题 时,就能够快速识别并运用对应的设计模式。
- (2)动手实际编写相关的测试代码并进一步体验该模式的工作过程。这一步对深入扎 实地掌握该模式将起到不可替代的作用, 一定不要略过。
- (3)在编码过程中要不断思考和总结设计经验,对于设计不合理的部分及时调整和更改。
- (4)在实际项目中,细致大胆地采用设计模式进行实战,尤其注意采用多个设计模式解 决问题时模式之间的关联和配合,不要怕出错。其实,程序设计中的最好、最通用、最正确之 说都是相对的,没有绝对的。
二、大型项目的软件开发思想
- 基本思想
对于大型项目的开发工作,需要注意如下几点。
(1)前期做细致的需求分析以及架构设计是非常重要的,应该多花时间去撰写相关的 需求文档、规划设计文档,安排合理的进度,文档应该为未来的项目规模增长提供一定的伸 缩性。力求让项目有一个良好的开端,为程序员提供开发参照和指导,尽量减少后期调整和 改动的成本。
(2)对于设计的层次来讲,不要一上来就设计类,而应当先划分成各个模块(子系统)。 在对模块进行明确划分的前提下,再进行类的划分,确定好类的接口。有些模块只需要单独 一个类即可实现,而有些模块需要多个类之间协作才能实现。对于每个类的设计应该有清 晰的认识,包括为何进行这样的设计、设计的具体细节等内容。设计是一种迭代的过程,不 可能一蹴而就。
模块之间交互时要限制与当前模块交互的其他模块数量。错综复杂的交互会让设计显得特别凌乱。
- (3)大型项目往往最让人头疼的问题是可维护性和可扩展性,如果模块之间的耦合度特 别高(紧耦合),那么随着项目代码的增多,那种牵一发而动全身的功能扩展让程序开发人员如 入泥沼,极度无奈。所以,尽可能降低模块之间的耦合度(解耦),其实,从某种角度上来说,学 习设计模式的本质就是寻求模块之间的解耦,耦合度越低,就越容易专注解决一小块问题。
- 微服务架构设计模式与设计模式的区别
微服务这个概念,读者多多少少都听说过,也是在2010年之后开始逐渐流行起来。为什 么这个概念会流行起来呢?主要是因为互联网业务规模的不断变大,用户越来越多,传统的开 发方式一般都是所谓的单体架构(理解成一个单一的可执行程序),如果想增加一个业务,就要 向这个可执行程序中不断增加代码,显然,这个可执行程序一定会变得越来越复杂。当它复杂 到一定程度的时候,就会面临着可靠性、可维护性等都变差的问题。就像一个人,如果这个人 200斤还可以接受,如果1000斤,那这个人就麻烦了,他可能连独自站起来都做不到了。
微服务解决的就是这种单独一个可执行程序过度复杂、过度庞大的问题,把这个可执行 程序进行拆分,拆分的角度是从功能上进行,也就是说,从功能上拆分成多个小的程序,彼此 之间通过一些架构方式,配合起来共同实现业务需求。
微服务架构设计模式与本书所讲解的设计模式之间的差别:
① 单独一个可执行程序来实现业务也好,或者是采用微服务这种用多个可执行程序配 合来实现业务也罢,都存在首先要把一个一个单独的可执行程序写好的问题;
② 同时,并不是所有业务都能用微服务来解决。
所以,读者切不可因为微服务的存在就放弃了对写出高质量代码的追求。本书所指的 设计模式研究探讨的是针对一个单独的可执行程序内部的各个模块之间如何做到高灵活 性、高可复用性以及高可扩展性的问题,这一点,请读者明确。
三、设计模式分类
- 设计模式分类
常见设计模式大概有二十多种,通常分为三大类,见表1.1。
设计模式名称 | 设计模式分类 |
模板方法(Template Method)模式 | 行为型模式 |
策略(Strategy)模式 | 行为型模式 |
观察者(Observer)模式 | 行为型模式 |
命令(Command)模式 | 行为型模式 |
迭代器(Iterator)模式 | 行为型模式 |
状态(State)模式 | 行为型模式 |
中介者(Mediator)模式 | 行为型模式 |
备忘录(Memento)模式 | 行为型模式 |
职责链(Chain Of Responsibility)模式 | 行为型模式 |
访问者(Visitor)模式 | 行为型模式 |
解释器(Interpreter)模式 | 行为型模式 |
工厂模式:①简单工厂(Simple Factory)模式 ②工厂方法(Factory Method)模式 ③抽象工厂(Abstract Factory)模式 | 创建型模式 |
原型(Prototype)模式 | 创建型模式 |
建造者(Builder)模式 | 创建型模式 |
单件(Singleton)模式 | 创建型模式 |
装饰(Decorator)模式 | 创建型模式 |
外观(facade)模式 | 结构型模式 |
组合(Composite)模式 | 结构型模式 |
享元(Flyweight)模式 | 结构型模式 |
代理(Proxy)模式 | 结构型模式 |
适配器(Adapter)模式 | 结构型模式 |
桥接(Bridge)模式 | 结构型模式 |
- (1)行为型模式。这种模式关注的是对象的行为或者交互方面内容,主要涉及算法和 对象之间的职责分配。通过使用对象组合,行为型模式可以描述一组对象应该如何协作来完成一个整体任务。
- (2)创建型模式。这种模式关注的是如何创建对象,其核心思想是要把对象的创建和 使用相分离(解耦)以取代传统对象创建方式可能导致的代码修改和维护上的问题。
- (3)结构型模式。这种模式关注的是对象之间的关系,主要涉及如何组合各种对象以 便获得更加灵活的结构,例如,继承机制提供了最基本的子类扩展父类的功能,但结构型模 式不仅仅简单地使用继承,而是更多地通过各种关系组合以获得更加灵活的程序结构,达到 简化设计的目的。