引言--- 为何需要设计模式?
增加代码的可维护性, 可扩展性.
在重构中学习设计模式, 在空余时间中对代码进行持续重构.
保证测试通过的情况下进行安全的,小步骤的重构.要保持代码清晰,必须持续地去除重复,简化和澄清代码. 采取更小、更安全的步骤比采取更大的步骤更能快速达到目标
学习设计模式不应当只是学习设计模式的结果, 而应当学习设计模式的重构经过. 这样才能在工作中不断地重构自己地代码。
--- 不能为了使用设计模式而使用设计模式,设计模式也是不断地重构出来的
学习设计模式不如掌握设计原则 (根据原则慢慢重构自己的设计模式)
高层模块 (稳定) 不应该依赖低层模块 (变化) , 两者都依赖抽象 (稳定)
抽象(稳定)不依赖于具体实现 (变化), 具体实现依赖于抽象 (稳定).
如何理解: 降低耦合性, 让高层模块和底层模块解耦合.
如何解释这个问题.
具体的类是不稳定的,是变化的. 高层模块一般是稳定的, 稳定的模块不应该依赖一个变化的模块.
依赖倒置原则是最常用, 也是最容易使用的设计原则 (一般在看见稳定依赖低层模块(变化)的代码的时候) 我们完全可以考虑将这个具体类中的特征抽象出来一个抽象层, 接口类(稳定点)
eg: 尝试着写一下.
模拟实现这样一个场景: A是客户. B 是一个具体的play玩耍项目.
没有使用依赖倒置原则的时候的代码.
class PlayPingPong { public: void Play() { cout << "一起打乒乓吧" << endl; } //...其他节目特性 void Eat() { cout << "一起来吃吃吃吧" << endl; } }; class PlayGame { public: void Play() { cout << "一起玩游戏吧" << endl; } void GameOver() { cout << "游戏结束了" << endl; } }; class Client { public: void Run(PlayGame* game) { game->Play(); } }; int main() { Client cli; cli.Run(new PlayGame); return 0; }
上述这个代码可以不可以, 是可以的, 但是Client类强依赖于PlayGame这个具体的类. 两者之间是强耦合的,而PlayGame这个具体的类是不稳定的. 一旦Play做出变化, 修改都会直接影响到Client类。 (这种强耦合性,不利于类的扩展)
然后我们再看一看下面这款代码:
class Game { public: virtual void Play() = 0; }; class PlayPingPong : public Game { public: virtual void Play() { cout << "一起打乒乓吧" << endl; } //...其他节目特性 void Eat() { cout << "一起来吃吃吃吧" << endl; } }; class PlayGame : public Game { public: virtual void Play() { cout << "一起玩游戏吧" << endl; } void GameOver() { cout << "游戏结束了" << endl; } }; class Client { public: void Run(Game* game) { game->Play(); } }; int main() { Client cli; cli.Run(new PlayGame); cli.Run(new PlayPingPong); return 0; }
上述这款代码的可扩展性. 已经耦合性就降低了很多. 有人可能会说,还不是都依赖抽象层了,没错是依赖抽象层(抽象接口了)。但是抽象接口一定是稳定的(稳定点)。稳定代表着 可靠不会轻易改变。这样就是好的一种设计。 (扩展性好, 稳定,跟具体类耦合度低)
画一画这个依赖图 (前后对比):
这种代码也一样的违背了开放封闭原则, (对于功能的扩展一样不友好)
然后是如下的加入依赖倒置的代码. (运行时绑定), 虽然都依赖抽象,但是抽象接口是稳定的, 而且具备很好的可扩展性.
- 开放封闭原则
⼀个类应该对扩展开放,对修改关闭。何为扩展. 常见的扩展方式是什么
扩展也就是无需对原有的体系结构做出大的修改, 而是进行扩充.
常见的扩展方式:
- 虚函数重写
- 基类指针作为成员对象
为何基类指针作为成员对象是一种扩展, 因为这个基类指针可以指向由这个基类派生出来的所有类对象. 所以也是一种扩展方式 (而且相对更推荐这种扩展方式, 因为相较于继承的虚函数重写这种扩展方式的耦合度更低, 更符合设计原则)
面向接口编程
不将变量类型申明为某一个具体的对象, 而是声明为某个接口 (扩展性更强)
上述相较之下, 第二种base相当于是接口. 扩展性更强. 包括C++中的多态也是遵循的这样的原则.
减少系统中各部分的依赖关系,从而实现高内聚、松耦合的类型设计方案
封装变化点(核心原则) 抽象稳定点,扩展变化点
将稳定点和变化点分离, 扩展修改变化点, 让稳定点和变化点层次分离 (设计模式的核心所在)
要使用设计模式,就必须明确程序中的稳定点和变化点是什么, 将稳定点和变化点进行封装隔离,用稳定点去调变化点. 使用虚函数重写来扩展变化点.
单一职责原则
一个类应该仅有一个引起它变化的原因
里氏替换原则
子类替换掉父类之后,一定要能够完成父类的职责
接口隔离原则
将复杂的方法,和细节的方法放入到私有和保护中, 仅仅将最简单易用的接口提供给用户使用
对象组合优于类继承
继承耦合度高, 组合耦合度低
什么情况下使⽤设计模式?
- 存在稳定点, 能够明确区分变化点,稳定点
- 熟悉需求可能的变化方向
- 明确依赖关系, 封装分离变化点,稳定点.
重构中获取设计模式
- 静态转换为动态
- 早绑定(编译时绑定)转换为晚绑定(运行时绑定.多态)
- 继承转换为组合
- 编译时依赖转换为运行时依赖,个人感觉跟上面早晚绑定一个意思
- 紧耦合转换为松耦合
学习设计模式的步骤
- 深入体会设计原则
- 学习具体的设计模式,找出其中的变化点,稳定点, 遵循的设计原则
- 在复杂的需求中按照时间的紧凑程度安排业务实现,然后再不断地在日常中持续重构,最终开发自己地设计模式
开篇之最简单的模板方法设计模式
定义 : 定义一个操作中的算法骨架,而将一些步骤延迟到子类中. (运行时绑定, 晚绑定C++中的虚函数重写)
大白话 : 为啥说这个是最简单的设计模式,因为它真的很容易理解. 说白了就是在抽象基类,稳定抽象类中定义好一个操作流程. 算法骨架。然后将具体的步骤实现,细节实现通过虚函数重写延迟到子类中.
结构图
代码示例:
代码场景引入, 存在这样一个类 Person类. 提供固定的人一天的流程。然后一些具体的人来依赖使用这个Person类, 调用Person类. 扩展这个Person类。先简单的实现一下这个功能.
//1, 3, 5都是固定的人必须流程 class Person { public: void Step1() { cout << "早上起床了吃个饭" << endl; } void Step3() { cout << "中午吃饭了" << endl; } void Step5() { cout << "晚上睡觉了" << endl; } }; class Student : public Person { public: void Step2() { cout << "学习学习学习" << endl; } void Step4() { cout << "午休,玩耍,刷短视频 再学习" << endl; } }; class Wocker : public Person { public: void Step2() { cout << "上班上班上班" << endl; } void Step4() { cout << "午休了去打一把麻将" << endl; } }; int main() { //固定的流程 Wocker w; w.Step1(); w.Step2(); w.Step3(); w.Step4(); w.Step5(); Student s; s.Step1(); s.Step2(); s.Step3(); s.Step4(); s.Step5(); return 0; }
如上的代码: 有没有实现需要的功能, 实现了,可以完全没有遵循任何的设计原则。
没有遵循依赖倒置原则:
可扩展性极差. 如果有些人有特殊的癖好,对于Person的模板不想遵循,想要做出特殊的更改. Student就需要从新写一个新的步骤, 这样存在二义性的问题, 或者就需要直接更改Person类的Step, 又或是是Person的Step出现了变化也会直接影响到Student, Wocker类
没有遵循开放封闭原则,上述说清楚了,完全没有遵循对扩展开放,对修改封闭的设计原则
没有做到良好的复用. 没有做到封装变化点.
而且没有做到接口隔离原则。接口使用对用户及其不方便友好.
模板方法引入如下:
现在我们看一看下面这一版模板方法的代码. 如何做到封装变化点. 抽象稳定点.
首先稳定点何在: Step1, Step2, Step3, Step4, Step5这个固定的算法骨架。模板是一个稳定点. 固定的Step1, Step3, Step5, 正常来讲是稳定的固定流程, Step2 Step4 我们留给子类扩展变化.
class Person { public: //固定的算法骨架 void TemplateMethod() { Step1(); Step2(); Step3(); Step4(); Step5(); } //虚析构函数 virtual ~Person() { } protected: void Step1() { cout << "早上起床了吃个饭" << endl; } //步骤实现延迟到子类 virtual void Step2() = 0; void Step3() { cout << "中午吃饭了" << endl; } //步骤实现延迟到子类 virtual void Step4() = 0; void Step5() { cout << "晚上睡觉了" << endl; } }; class Student : public Person { protected: virtual void Step2() { cout << "学习学习学习" << endl; } virtual void Step4() { cout << "午休,玩耍,刷短视频 再学习" << endl; } }; class Wocker : public Person { protected: virtual void Step2() { cout << "上班上班上班" << endl; } virtual void Step4() { cout << "午休了去打一把麻将" << endl; } }; void Test(Person* p) { if (p != nullptr) { p->TemplateMethod(); delete p; } } int main() { //固定的流程 Person* p; Test(new Wocker); Test(new Student); return 0; }
分析: 上述的模板方法遵循了多少设计原则.
接口隔离原则. 将客户不需要的接口隔离, 影响细节. 而是对用户提供简单易用的必要接口TemplateMethod
依赖倒置原则. 利用晚绑定. 将编译时依赖转变为运行时依赖. 不再是依赖具体的类,而是依赖于抽象.
开闭原则, 对扩展开发,对修改封闭.
逆向调用原则,反向调用原则, 父类调用重写的子类方法. 调用子类重写后的虚函数.
子类可以复写父类的子流程,从而使得父类的大流程更加丰富
至此,各位兄弟朋友们, 小杰相当于和大家一起对于设计模式的学习开了个头. 设计模式的学习绝对不是学习既定的设计模式,去死记硬背如何写,一味的强行使用设计模式,生拉硬拽的往固定的设计模式上去靠。 这个绝对不是学习设计模式的正确姿势
设计原则高于设计模式, 抓住稳定点,变化点,分析缺失的设计原则,分析学习具体的设计模式所用到的设计原则,这样才对我们真正有用
在空余时间对于已有的既定代码进行持续重构,在重构的中研究实现自己的设计模式才是绝对正确的方式
祝兄弟们越来越好,大家一起加油,升职加薪,学业有成