设计模式的C++实现(0)——初识设计模式
《设计模式的C++实现》为作者对系列博客的又一次尝试:在开发过C++基于多设计模式下的同步&异步⽇志系统🔗后,作者接触到了《设计模式》这本书,颇有感触,于是决定写博客来记录读后的灵感,然而随着博客内容的不断丰富,单篇的博客已经承载不下如此多样的内容,各种各样的设计模式便是一个典例,所以我决定将
设计模式
相关的博客拆成一个系列博客
目录:
TODO
知识基础
本系列博客着重于经典设计模式的介绍和运用C++类和对象技术将其实现,所以要求读者已经能够熟练掌握C++类和对象技术。并且当我们提及抽象类
,多态
,或者继承
,虚函数的声明与实现
等概念时,你应当已经对这些概念了然于心,而不是一知半解,还要查资料复习
设计模式从何而来
设计模式描述了在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。设计模式捕获了随时间进化与发展的问题的求解方法,因此它们往往并不是人们从一开始就采用的设计方案,它们反映了不为人知的重新设计和重新编码的成果,而这些都来自软件开发者为了设计出灵活可复用的软件而长时间进行的艰苦努力。设计模式所做的事,就是捕获这些解决方案,并用简洁易用的方式表达出来。
学习目标
设计模式针对的是使用标准的面向对象语言实现,所以并不要求使用独特的语言特性,所以我们既用C++
实现设计模式,也可以用其它面向对象语言实现,这里使用C++实现仅仅是为了更加具体和直观,同时对C++类和对象有更深入的了解和掌握
以上是对语言的要求,那么设计模式要学到什么地步呢?我们希望在学习了设计模式后,能够在实际开发中对设计模式有一种"Aha!"
而不是"Huh?"
的应用技术和体验。在此之后,你将能用一种非同寻常的方式思考面向对象设计。你将拥有一种深刻的洞察力,以帮助你设计出更加灵活的、模块化的、可复用的和易理解的软件。优雅永不过时
什么是设计模式
Christopher Alexander说过:“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”
尽管Alexander所指的是城市和建筑模式,但他的思想也适用于面向对象设计模式。设计模式就是这样一种注重解决实际开发中重复出现的问题,提供一种简洁而优雅的设计方案的经验总结。为什么它能帮助我们减少不必做的重复劳动?因为一个优秀的设计者/开发者能够在遇到问题时,善于找出其中与已解决的问题中重复的部分,优先复用以往的成功经验,而不是每个新问题都要创造一个新的解决方法,一方面是低效,不光是动手开发,往往还要在决策方面耗费不少时间;另一方面是不可靠,新的方法与成熟的解决方案相比,难以对所有情况考虑周全,迭代次数也相应地较少。
所以学好设计模式,可以大大减少在设计决策方面的代价。使程序员能够在设计合理的前提下,更专注于代码实现。
设计模式的构成
一般而言,一个模式有四个基本要素
- 模式名称(
pattern name
) : 一个助记名,它能用一两个词来描述模式的问题、解决方案和效果。 - 问题(
problem
) :描述了应该在何时使用模式。 - 解决方案(
solution
) :描述了设计的组成成分,它们之间的相互关系及各自的职责和协作方式。因为模式就像一个模板,所以有时解决方案可能比较抽象,而不是某种具体实现 - 效果(
consequences
) :描述了模式应用的效果及使用模式应权衡的问题。我们几乎可以认为,在计算机领域中没有完美的方案。凡事都是需要权衡得失的
出发点的不同会产生对什么是模式而什么不是模式的理解不同。一个人的模式对另一个人来说可能只是基本构造部件。
我们这里对设计模式的定义为:对被用来在特定场景下解决一般设计问题的类和互相通信的对象的描述
设计模式的六大原则
- 单一职责原则(Single Responsibility Principle)
解释
:类的职责应该单一,一个方法只做一件事。职责划分清晰明了,每次改动划分到最小单位的方法或类具体使用建议
:两个完全不一样的功能不应该放在一个类中。一个好的类中应该是一组相关性很高的函数和数据的封装- 用例:网络聊天中要实现
网络通信
&聊天
,在这一层次上应分割为网络通信类
和聊天类
,像网络通信类
还能接着划分出套接字类
等下一级的分割
- 用例:网络聊天中要实现
- 开闭原则(Open Closed Pinciple)
解释
:对扩展开放,对修改单一使用建议
:对软件实体的改动,最好用扩展而非修改的方式- 用例:
- 类设计上,对已投入使用的
类A
,要扩展时,不是把类A
修改成类X
,而是继承类A
并修改派生类类A_Plus
- 超市卖货促销–不是修改商品原来的价格,而是新增促销价格
- 类设计上,对已投入使用的
- 用例:
- 里氏替换原则(Liskov Substitution Principle)
- 通俗点讲,就是只要父类能出现的地方,子类就可以出现,而且替换为子类也不会产生任何错误或异常
- 在继承类时,务必重写⽗类中所有的⽅法,尤其需要注意 ⽗类的
protected
⽅法,⼦类尽量不要暴露⾃⼰的public
⽅法供外界调⽤。 - 使用建议:子类必须完全实现父类的方法,孩⼦类可以有⾃⼰的个性。覆盖或实现⽗类的⽅法时,输⼊参数可以被放⼤,输出可以缩⼩
- 用例:父类跑步运动员类-
会跑步
,子类长跑运动员-会跑步
且擅长长跑
,另一个子类短跑运动员-会跑步
且擅长短跑
- 依赖倒置(Dependence Inversion Principle)
解释
:⾼层模块不应该依赖低层模块,两者都应该依赖其抽象. 不可分割的原⼦逻辑就是低层模式,原⼦逻辑组装成的就是⾼层模块- 模块间依赖通过抽象(接⼝)发⽣,具体类之间不直接依赖
- 使⽤建议:每个类都尽量有抽象类,任何类都不应该从具体类派⽣。尽量不要重写基类的⽅法。结合⾥⽒替换原则使⽤。
- 迪米特法则(Law of Demeter) 又称:”最少知道法则”
解释
:尽量减少对象之间的交互,从而减小类之间的耦合。⼀个对象应该对其他对象有最少的了解。对类的低耦合提出了明确的要求:- 只和直接的朋友交流, 朋友之间也是有距离的。⾃⼰的就是⾃⼰的(如果⼀个⽅法放在本类中,既不增加类间关系,也对本类不产⽣负⾯影响,那就放置在本类中)。
用例
:⽼师让班⻓点名–⽼师给班⻓⼀个名单,班⻓完成点名勾选,返回结果,⽽不是班⻓点名,⽼师勾选
- 接口隔离原则(Interface Segregation Principle)
解释
:客⼾端不应该依赖它不需要的接⼝,类间的依赖关系应该建⽴在最⼩的接⼝上使⽤建议
:接⼝设计尽量精简单⼀,但是不要对外暴露没有实际意义的接⼝。⽤例
:修改密码,不应该提供修改⽤⼾信息接⼝,⽽就是单⼀的最⼩修改密码接⼝,更不要暴露数据库操作
从整体上来理解六⼤设计原则,可以简要的概括为⼀句话,⽤抽象构建框架,⽤实现扩展细节,具体到每⼀条设计原则,则对应⼀条注意事项:
单⼀职责原则
告诉我们实现类要职责单⼀⾥⽒替换原则
告诉我们不要破坏继承体系依赖倒置原则
告诉我们要⾯向接⼝编程接⼝隔离原则
告诉我们在设计接⼝的时候要精简单⼀迪⽶特法则
告诉我们要降低耦合开闭原则
是总纲,告诉我们要对扩展开放,对修改关闭
怎样选择设计模式
设计模式可以多达几十种,分成创建型
、结构型
、行为型
三大类,但要从中找出一个针对特定设计问题的模式可能还是很困难的,尤其是当面对一组新模式,你还不怎么熟悉它的时候。所以对设计模式的熟悉程度和开发经验很重要,但经验也是从无到有基类出来的。这里给出几个不同的方法,希望有助于发现时候手头问题的设计模式:
- 考虑设计模式是怎样解决设计问题的
- 浏览模式的意图(
intent
)部分,从模式的目的角度考察其关联性。 - 研究模式怎样互相关联,有时模式之间会互相关联协作
- 研究目的相似的模式。通过对细节的辨析,加深对模式的理解
- 检查重新设计的原因。实际上设计模式也会面临重新设计的问题,可以与自己遇到的问题比较是否有相似性
- 考虑你的设计中哪些是可变的。这一点与上一点想法,这点着重于考虑你想要什么变化却又不会引起重新设计。可以思考这一要求与哪个设计模式的想法相近
怎样使用设计模式
选择了设计模式后该怎么使用它呢?这里给出一个有效应用设计模式的循序渐进的方法。
- 大致浏览一边模式 特别注意其适用性部分和效果部分,确定它适合你的问题
- 回头研究结构部分、参与者部分和协作部分 设计模式之间往往不是孤立的,要确保你理解这个模式的类和对象以及它们是怎样关联的
- 看代码示例部分,看看这个模式代码形式的具体例子 研究具体的代码也有助于着手实现模式
- 选择模式参与者的名字,使它们在应用上下文中有意义 这一点更倾向于从代码规范的角度出发,设计模式参与者的名字通常过于抽象而不会直接出现在应用中,此时通过命名更有利于在形式上增强系统的关联性
- 定义类 声明它们的接口,建立它们的继承关系,组织好类内成员。
- 定义模式中专门用于应用的操作名称 这里再一次体现出,接口的名字一般依赖于应用。
- 实现执行模式中责任和协作的操作 到这一步就要把设计模式的具体实现整合到整个项目代码中,规划如何调用接口。
设计模式的使用没有标准范式,这里只是作指导作用,探索出适合自己的使用方法才是最好的。
这里再强调一次使用设计模式所造成的得失问题,必须要权衡到位
- 引入设计模式所带来的灵活性和可变性是否刚好符合要求,还是太多余或者还不够
- 引入设计模式使得设计变得更复杂,是否使项目过于难理解了
- 引入设计模式所造成的性能牺牲是否在可接受的范围
所以一定权衡好,一个设计模式所带来的灵活性,是否是你真正需要的