正确的姿势学习设计模式,设计模式必知必会 --- 面试, 提升篇

简介: 正确的姿势学习设计模式,设计模式必知必会 --- 面试, 提升篇

引言--- 为何需要设计模式?

增加代码的可维护性, 可扩展性.


在重构中学习设计模式, 在空余时间中对代码进行持续重构.


保证测试通过的情况下进行安全的,小步骤的重构.要保持代码清晰,必须持续地去除重复,简化和澄清代码.   采取更小、更安全的步骤比采取更大的步骤更能快速达到目标


学习设计模式不应当只是学习设计模式的结果, 而应当学习设计模式的重构经过. 这样才能在工作中不断地重构自己地代码。                                      


---   不能为了使用设计模式而使用设计模式,设计模式也是不断地重构出来的

学习设计模式不如掌握设计原则 (根据原则慢慢重构自己的设计模式)

  • 依赖倒置原则

高层模块 (稳定) 不应该依赖低层模块 (变化) , 两者都依赖抽象 (稳定)


抽象(稳定)不依赖于具体实现 (变化), 具体实现依赖于抽象 (稳定).


如何理解:   降低耦合性, 让高层模块和底层模块解耦合.


如何解释这个问题.  


具体的类是不稳定的,是变化的. 高层模块一般是稳定的, 稳定的模块不应该依赖一个变化的模块.


依赖倒置原则是最常用, 也是最容易使用的设计原则 (一般在看见稳定依赖低层模块(变化)的代码的时候)  我们完全可以考虑将这个具体类中的特征抽象出来一个抽象层, 接口类(稳定点)  

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;
}

上述这款代码的可扩展性. 已经耦合性就降低了很多. 有人可能会说,还不是都依赖抽象层了,没错是依赖抽象层(抽象接口了)。但是抽象接口一定是稳定的(稳定点)。稳定代表着 可靠不会轻易改变。这样就是好的一种设计。     (扩展性好, 稳定,跟具体类耦合度低)

画一画这个依赖图 (前后对比):

这种代码也一样的违背了开放封闭原则, (对于功能的扩展一样不友好)

然后是如下的加入依赖倒置的代码.  (运行时绑定), 虽然都依赖抽象,但是抽象接口是稳定的, 而且具备很好的可扩展性.

  • 开放封闭原则

⼀个类应该对扩展开放,对修改关闭。何为扩展. 常见的扩展方式是什么

扩展也就是无需对原有的体系结构做出大的修改, 而是进行扩充.

常见的扩展方式:

  1. 虚函数重写
  2. 基类指针作为成员对象

为何基类指针作为成员对象是一种扩展, 因为这个基类指针可以指向由这个基类派生出来的所有类对象. 所以也是一种扩展方式   (而且相对更推荐这种扩展方式, 因为相较于继承的虚函数重写这种扩展方式的耦合度更低, 更符合设计原则

面向接口编程

不将变量类型申明为某一个具体的对象, 而是声明为某个接口      (扩展性更强)

上述相较之下, 第二种base相当于是接口. 扩展性更强. 包括C++中的多态也是遵循的这样的原则.

减少系统中各部分的依赖关系,从而实现高内聚、松耦合的类型设计方案

封装变化点(核心原则) 抽象稳定点,扩展变化点

将稳定点和变化点分离, 扩展修改变化点, 让稳定点和变化点层次分离    (设计模式的核心所在)

要使用设计模式,就必须明确程序中的稳定点和变化点是什么, 将稳定点和变化点进行封装隔离,用稳定点去调变化点. 使用虚函数重写来扩展变化点.

单一职责原则

一个类应该仅有一个引起它变化的原因

里氏替换原则

子类替换掉父类之后,一定要能够完成父类的职责

接口隔离原则

将复杂的方法,和细节的方法放入到私有和保护中, 仅仅将最简单易用的接口提供给用户使用

对象组合优于类继承

继承耦合度高, 组合耦合度低

什么情况下使⽤设计模式?

  1. 存在稳定点, 能够明确区分变化点,稳定点
  2. 熟悉需求可能的变化方向
  3. 明确依赖关系, 封装分离变化点,稳定点.

重构中获取设计模式

  1. 静态转换为动态
  2. 早绑定(编译时绑定)转换为晚绑定(运行时绑定.多态)
  3. 继承转换为组合
  4. 编译时依赖转换为运行时依赖,个人感觉跟上面早晚绑定一个意思
  5. 紧耦合转换为松耦合

学习设计模式的步骤

  1. 深入体会设计原则
  2. 学习具体的设计模式,找出其中的变化点,稳定点, 遵循的设计原则
  3. 在复杂的需求中按照时间的紧凑程度安排业务实现,然后再不断地在日常中持续重构,最终开发自己地设计模式

开篇之最简单的模板方法设计模式

定义 : 定义一个操作中的算法骨架,而将一些步骤延迟到子类中.  (运行时绑定, 晚绑定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


依赖倒置原则. 利用晚绑定. 将编译时依赖转变为运行时依赖. 不再是依赖具体的类,而是依赖于抽象.


开闭原则, 对扩展开发,对修改封闭.


逆向调用原则,反向调用原则, 父类调用重写的子类方法.  调用子类重写后的虚函数.


子类可以复写父类的子流程,从而使得父类的大流程更加丰富


至此,各位兄弟朋友们, 小杰相当于和大家一起对于设计模式的学习开了个头. 设计模式的学习绝对不是学习既定的设计模式,去死记硬背如何写,一味的强行使用设计模式,生拉硬拽的往固定的设计模式上去靠。 这个绝对不是学习设计模式的正确姿势


设计原则高于设计模式, 抓住稳定点,变化点,分析缺失的设计原则,分析学习具体的设计模式所用到的设计原则,这样才对我们真正有用


在空余时间对于已有的既定代码进行持续重构,在重构的中研究实现自己的设计模式才是绝对正确的方式


祝兄弟们越来越好,大家一起加油,升职加薪,学业有成


相关文章
|
4月前
|
存储 JSON 前端开发
No206.精选前端面试题,享受每天的挑战和学习
No206.精选前端面试题,享受每天的挑战和学习
No206.精选前端面试题,享受每天的挑战和学习
|
13天前
|
监控 安全 Java
【多线程学习】深入探究阻塞队列与生产者消费者模型和线程池常见面试题
【多线程学习】深入探究阻塞队列与生产者消费者模型和线程池常见面试题
|
18天前
|
设计模式 存储 前端开发
Java从入门到精通:2.2.1学习Java Web开发,了解Servlet和JSP技术,掌握MVC设计模式
Java从入门到精通:2.2.1学习Java Web开发,了解Servlet和JSP技术,掌握MVC设计模式
|
2月前
|
网络协议
跟着动画学习TCP三次握手和四次挥手,及全部面试题
跟着动画学习TCP三次握手和四次挥手,及全部面试题
41 0
|
4月前
|
设计模式 Java 关系型数据库
BAT等大厂年薪30W+面试清单:JVM\MySQL\设计模式\分布式\微服务
疫情影响下招聘名额缩减不少,但阿里、腾讯、抖音、快手等互联网公司却加快了人才招聘的节奏。这里根据自身的实际经历,整理了一份面试这些大厂的清单,希望能帮助到大家查漏补缺,攻克面试难关。
|
4月前
|
存储 前端开发 JavaScript
No204.精选前端面试题,享受每天的挑战和学习
No204.精选前端面试题,享受每天的挑战和学习
|
4月前
|
前端开发 UED
No203.精选前端面试题,享受每天的挑战和学习
No203.精选前端面试题,享受每天的挑战和学习
|
4月前
|
前端开发 JavaScript
No201.精选前端面试题,享受每天的挑战和学习
No201.精选前端面试题,享受每天的挑战和学习
|
1月前
|
存储 安全 Java
大厂面试题详解:java中有哪些类型的锁
字节跳动大厂面试题详解:java中有哪些类型的锁
60 0
|
3天前
|
Java
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
13 0