你好,我是Chrono。

今天,我们进入最后的“总结”单元,把前面学到的这些知识上升到“理论结合实践”的高度,做个归纳整理。我们先来了解一下设计模式和设计原则,然后再把理论“落地”,综合利用所有知识点,设计并开发出一个实际的服务器应用。

你可能会问了:我们这是个C++的课程,为什么还要专门来讲设计模式呢?

我觉得,设计模式是一门通用的技术,是指导软件开发的“金科玉律”,它不仅渗透进了C++语言和库的设计(当然也包括其他编程语言),而且也是成为高效C++程序员必不可缺的“心法”和“武器”。

掌握了它们,理解了语言特性、库和工具后面的设计思想,你就可以做到“知其然,更知其所以然”,然后以好的用法为榜样,以坏的用法为警示,扬长避短,从而更好地运用C++。

所以,我把我这些年的实践经验进行了提炼和总结,糅合成了两节课,帮你快速掌握,并且用好设计模式,写出高效、易维护的代码。这节课,我会先讲一讲学好设计模式的核心方法,下节课,我们再讲在C++里具体应用了哪些设计模式。

为什么要有设计模式?

虽然C++支持多范式编程,但面向对象毕竟还是它的根基,而且,面向对象编程也通用于当前各种主流的编程语言。所以,学好、用好面向对象,对于学好C++来说,非常有用。

但是,想要得到良好的面向对象设计,并不是一件容易的事情。

因为每个人自身的能力、所在的层次、看问题的角度都不同,仅凭直觉“对现实建模”,很有可能会生成一些大小不均、职责不清、关系混乱的对象,最后搭建出一个虽然可以运行,但却难以理解、难以维护的系统。

所以,设计模式就是为此而生的。

它系统地描述了一些软件开发中的常见问题、应用场景和对应的解决方案,给出了专家级别的设计思路和指导原则

按照设计模式去创建面向对象的系统,就像是由专家来“手把手”教你,不能说绝对是“最优解”,但至少是“次优解”。

而且,在应用设计模式的过程中,你还可以从中亲身体会这些经过实际证明的成功经验,潜移默化地影响你自己思考问题的方式,从长远来看,学习和应用设计模式能够提高你的面向对象设计水平。

学习、理解设计模式,才能用好面向对象的C++

经典的《设计模式》一书里面介绍了23个模式,并依据设计目的把它们分成了三大类:创建型模式、结构型模式和行为模式。

这三类模式分别对应了开发面向对象系统的三个关键问题:如何创建对象、如何组合对象,以及如何处理对象之间的动态通信和职责分配。解决了这三大问题,软件系统的“架子”也就基本上搭出来了。

23个模式看起来好像不是很多,但它们的内涵和外延都是非常丰富的,不然也不会有数不清的论文、书刊研究它们了,所以,我们要从多角度、多方面去评价、审视模式。

那该怎么做才好呢?

你可以看一下《设计模式》的原书,它用了一个很全面的体例来描述模式,包括名称、别名、动机、结构、示例、效果、相关模式,等等。

虽然显得有点琐碎、啰唆,但我们必须要承认,这种严谨、甚至是有些刻板的方式能够全方位、无死角地介绍模式,强迫你从横向、纵向、深层、浅层、抽象、具体等各个角度来研究、思考。只有在这个过程中,你才能真正掌握设计模式的内核。

模式里的结构和实现方式直接表现为代码,可能是最容易学习的部分,但我认为,其实这些反而是最不重要的。

你更应该去关注它的参与者、设计意图、面对的问题、应用的场合、后续的效果等代码之外的部分,它们通常比实现代码更重要

因为代码是“死”的,只能限定由某几种语言实现,而模式发现问题、分析问题、解决问题的思路是“活”的,适用性更广泛,这种思考“What、Where、When、Why、How”并逐步得出结论的过程,才是设计模式专家经验的真正价值。

理解了这些内容,我们就可以应用在C++面向对象编程里了。下节课,我会具体给你讲一讲在C++里,这些该怎么用。

学习、理解设计原则,才能用好多范式的C++

可能你在学习设计模式的时候还是有些困惑,设计模式是专家经验的总结不假,但专家们是如何察觉、发现、探索出这些模式的呢?

而且模式真的完全只是“模式”、固定的“套路”,有没有什么更一般的思想来指导我们呢?换句话说,有没有“设计‘设计模式’的模式”呢?

嗯,这个真的有(笑)。

其实,这些更高层次的指导思想你可能也听说过,它们被通称为“设计原则”。

最常用有5个原则,也就是常说的“SOLID”。

  1. SRP,单一职责(Single ResponsibilityPrinciple);
  2. OCP,开闭(Open Closed Principle);
  3. LSP,里氏替换(Liskov Substitution Principle);
  4. ISP,接口隔离(Interface-Segregation Principle);
  5. DIP,依赖反转,有的时候也叫依赖倒置(Dependency Inversion Principle)。

不过可能是因为我最先接触、研究的是设计模式,所以后来再看到这些原则的时候,“认同感”就没有那么强烈了。

虽然它们都说得很对,但没有像设计模式那样给出完整、准确的论述。所以,我觉得它们有点“飘”,缺乏可操作性,在实践中不好把握使用的方式。

但另一方面,这些原则也确实提炼出了软件设计里最本质、最基本的东西,就好像是欧几里得五公设、牛顿三定律一样,初看上去似乎很浅显直白,但仔细品品,就会发现,可以应用到任何系统里,所以了解它们还是很有必要的。

下面我就来讲讲对设计原则的一些理解和看法,再结合C++和设计模式,帮你来加深认识,进而在C++里实际用好它们。

第一个,单一职责原则,简单来说就是“不要做多余的事”,更常见的说法就是“高内聚低耦合”。在设计类的时候,要尽量缩小“粒度”,功能明确单一,不要设计出“大而全”的类。

使用单一职责原则,经常会得到很多“短小精悍”的对象,这时候,就需要应用设计模式来组合、复用它们了,比如,使用工厂来分类创建对象、使用适配器、装饰、代理来组合对象、使用外观来封装批量的对象。

单一职责原则的一个反例是C++标准库里的字符串类string(参见第11讲),它集成了字符串和字符容器的双重身份,接口复杂,让人无所适从(所以,我们应该只把它当作字符串,而把字符容器的工作交给vector<char>)。

第二个是开闭原则,它也许是最“模糊”的设计原则了,通常的表述是“对扩展开放,对修改关闭”,但没有说具体该怎么做,跟没说一样。

我觉得,你可以反过来理解这个原则,在设计类的时候问一下自己,这个类封装得是否足够好,是否可以不改变源码就能够增加新功能。如果答案是否定的(要改源码),那就说明违反了开闭原则。

应用开闭原则的关键是做好封装,隐藏内部的具体实现细节,然后开放足够的接口,这样外部的客户代码就可以只通过接口去扩展功能,而不必侵入类的内部。

你可以在一些结构型模式和行为模式里找到开闭原则的“影子”:比如桥接模式让接口保持稳定,而另一边的实现任意变化;又比如迭代器模式让集合保持稳定,改变访问集合的方式只需要变动迭代器。

C++语言里的final关键字(第5讲)也是实践开闭原则的“利器”,把它用在类和成员函数上,就可以有效地防止子类的修改。

第三个原则是里氏替换原则,意思是子类必须能够完全替代父类

这个原则还是比较好理解的,就是说子类不能改变、违反父类定义的行为。像在第5讲里说的正方形、鸟类的例子,它们就是违反了里氏替换原则。

不过,因为C++支持泛型编程,而且我也不建议多用继承,所以在C++里你只要了解一下它就好。

第四个是接口隔离原则,它和单一职责原则有点像,但侧重点是对外的接口而不是内部的功能,目标是尽量简化、归并给外界调用的接口,避免写出大而不当的“面条类”。

大多数结构型模式都可以用来实现接口隔离,比如,使用适配器来转换接口,使用装饰模式来增加接口,使用外观来简化复杂系统的接口。

第五个原则是依赖反转原则,个人觉得是一个比较难懂的原则,我的理解是上层要避免依赖下层的实现细节,下层要反过来依赖上层的抽象定义,说白了,大概就是“解耦”吧。

模板方法模式可以算是比较明显的依赖反转的例子,父类定义主要的操作步骤,子类必须遵照这些步骤去实现具体的功能。

如果单从“解耦”的角度来理解的话,存在上下级调用关系的设计模式都可以算成是依赖反转,比如抽象工厂、桥接、适配器。

除了SOLID这五个之外,我觉得还有两个比较有用:DRY(Don’t Repeate Yourself)和KISS(Keep It Simple Stupid)。

它们的含义都是要让代码尽量保持简单、简洁,避免重复的代码,这在C++里可以有很多方式去实现,比如用宏代替字面值,用lambda表达式就地定义函数,多使用容器、算法和第三方库。

小结

好了,今天就到这里吧,我从比较“宏观”的层面说了设计模式和设计原则。

其实这些就是对我们实际开发经验的高度浓缩和总结。理解掌握了这些经验,你就会始终保持着清醒的头脑,在写C++代码的过程中有意识地去发现、应用模式,设计出好的结构,对坏的代码进行重构。

小结一下这节课的要点:

  1. 面向对象是主流编程范式,使用设计模式可以比较容易地得到良好的面向对象设计;
  2. 经典的设计模式有23个,分成三大类:创建型模式、结构型模式和行为模式;
  3. 应该从多角度、多方面去研究设计模式,多关注代码之外的部分,学习解决问题的思路;
  4. 设计原则是设计模式之上更高层面的指导思想,适用性强,但可操作性弱,需要多在实践中体会;
  5. 最常用的五个设计原则是“SOLID”,此外,还有“DRY”和“KISS”。

不过,我还要特别提醒你,设计模式虽然很好,但它绝不是包治百病的“灵丹妙药”。如果不论什么项目都套上设计模式,就很容易导致过度设计,反而会增加复杂度,僵化系统。

对于我们C++程序员来说,更是要清楚地认识到这一点,因为在C++里,不仅有面向对象编程,还有泛型编程和函数式编程等其他范式,所以领会它的思想,在恰当的时候改用模板/泛型/lambda来替换“纯”面向对象,才是使用设计模式的最佳做法。

课下作业

最后是课下作业时间,给你留两个思考题:

  1. 你觉得使用设计模式有什么好处?
  2. 你是怎么理解SOLID设计原则的?哪个对你最有指导意义?

欢迎你在留言区写下你的思考和答案,如果觉得今天的内容对你有所帮助,也欢迎分享给你的朋友。我们下节课见。

评论