设计模式之道:解构结构型设计模式的核心原理

解构常见的三种结构型设计模式的核心原理

  • 一、引言:如何学习设计模式
  • 二、责任链模式
    • 2.1、代码结构
    • 2.2、符合的设计原则
    • 2.3、案例分析:nginx 阶段处理
    • 2.4、小结
  • 三、装饰器模式
    • 3.1、代码结构
    • 3.2、符合的设计原则
    • 3.3、小结
  • 四、组合模式
    • 4.1、代码结构
    • 4.2、符合的设计原则
    • 4.3、小结
  • 五、总结

一、引言:如何学习设计模式

学习设计模式最主要要抓住一点:就是怎么分析这个稳定点和变化点。自己实现一个框架,或者是实现一个具体的小功能,本质上分析问题的思路都是一样的,首先要去把稳定点给它抽象出来,然后针对这个变化点想着怎么去扩展它。所以这里还是要反复的介绍怎么分析这个稳定点和变化点;具体不同的设计模式是怎么来处理这个扩展(就是扩展的问题);稳定点它是怎么处理的;用C++的语言特性是怎么去解决这些问题的;沿着这个思路去学习。

前面已经介绍了设计模式当中的模板方法、观察的模式、以及策略模式,这里再次强调以下学习、掌握设计模式的学习步骤。

  • 首先,需要来了解设计模式解决了什么问题。本质上是分析它的稳定点和变化点,实际在做具体功能开发的时候也需要去抽象具体的稳定点以及想办法去扩展变化点,这样在实际开发过程当中,尽量写少量的代码去应对未来需求的变化。
  • 第二点,设计模式的代码结构是什么。需要培养一个看代码、看一些框架或者看项目代码结构的时候马上能够反应出来使用了什么设计模式,或者它符合什么设计原则,从而可以推断出代码具体的意图。熟悉实现具体设计模式的代码结构能够帮助我们对一个代码有一个敏感度,以便能够快速的进行推断和反应。
  • 第三点,看这些设计模式符合了哪些设计原则。因为设计模式是由设计原则推导过来的,所以可以按照这一个设计模式的产生的流程重新去思考这一个问题,能够帮助我们去很好的去设计我们的代码。相信很多人在具体的工作当中都有自己不同的一些设计方式,它不一定符合某一些设计模式,未来大家应对的某些需求也会自己去设计一个框架,所以可以思考它符合哪些设计原则。
  • 第四点,如何在上面扩展代码。尤其是对于初学者或刚刚参加工作的朋友们,对这个扩展代码一定要非常的清楚,就是如果在这个设计模式的基础上要修改哪些代码,这个是必须要掌握的。
  • 第五点,按照自己的需求或者自己的项目以及自己的工作场景进行一个联系,哪些需求变化可以使用设计模式;在看开源框架的时候也可以去看一下它是怎么解决这一个问题的。记住几个关键设计模式的一些典型应用场景能够帮助我们快速的反应;当具体需求来了知道该怎么使用某一些设计模式
学习步骤
设计模式解决什么问题
稳定点
变化点
设计模式的代码结构是什么
设计模式符合哪些设计原则
如何在上面扩展代码
设计模式有哪些典型应用场景
联系工作场景
开源框架

这个就是设计模式具体的学习步骤。

二、责任链模式

开源框架Nginx也会用到责任链,先了解一下什么是责任链模式责任链模式定义:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止

定义解析:“请求的发送者和接收者之间的一个耦合关系”是请求的发送由多个接收者(或处理对象)来进行处理的,并且这些处理对象是连成了一个链条,具体的请求会沿着链条的顺序依次进行传递,直到有一个对象处理为止。假设有一个对象处理了,则它处理完就直接返回了,后面的对象就不会处理了,这个就是责任链的定义,它描述的一个功能。

定义特别复杂,只要来分析它解决的问题,从稳定点和变化点来进行分析。

  • 稳定点:处理流程是稳定的,第一个稳定点是请求按照链条传递,第二个稳定点是可打断的。一个请求者对多个接收者,这个接受者是由多个对象构成的,请求会沿着这个链条依次进行处理,因此可以看到请求会沿着这个链条进行传递是一个稳定点。请求是由多个接受者来进行处理的,如果被其中一个处理了,就可以打断,后面的就不会被处理了,这个称之为可打断,这也是一个稳定点。
  • 变化点:处理节点的个数、处理条件、处理顺序。一个请求沿着处理链条依次进行处理,这个链条可能会新增加一个处理流程,这是第一个变化点,可能会增加一些功能,或者要去减少一些功能;第二个变化点就是可能会去进行调整处理流程节点的顺序(即调整他们请求处理的顺序)化点。

2.1、代码结构

先来看这一个具体的案例,具体的请假流程:

请求流程,1 天内需要主程序批准,3 天内需要项目经理批准,3 天以上需要老板批准。

说明:有一个这样的规定,一天内需要主程序来去批准,如果主程序批准了,那么项目经理就不需要看这个请假流程了,那么老板更加不需要知道了;如果是两天的请假,主程序把握不住,就把这个流程传递给下一个人去处理,项目经理就根据目前的项目进度来看一看能不能请两天假,如果可以请两天假,在这里处理完就结束了,后面的人都不需要看了(即不需要让老板知道,老板就不需要关注这个事情);如果要请一个月假,项目经理把握不住了,需要让更上层的老板来进行处理了,老板可能就看这个员工最近在公司表现怎么样(KPI可不可以),如果是一个人才,那么还是批了,要是经常KPI很低,然后经常浑水摸鱼,那么当然老板直接就可能…。这个就是处理的流程的背景,这个背景显然就是责任链模式一样的。

那么来看一下它的稳定点和变化点,同样的,来分析这个稳定点和变化点跟责任链的需求到底是怎么对应上的,首先来看一下它符不符合稳定点(请求是不是按照链条传递),流程是按照主程序、项目经理、老板依次进行传递,而且是可以打断的(主程序处理了,那么项目经理不需要知道这个事情了,项目经理处理了,那么老板就不需要知道这个事情了,可打断)。

那么再来看一下这个变化点。可能主程序就很抱怨,为什么总是有请假的事情来干扰他写代码,那么前面能不能增加一个前台,前台来负责处理一下半天以内的(比如请假一个小时也来打扰一下,那么主程序就不用干活了),让主程序来休息一下,不需要关注这么多小事情,这个就是新增流程节点(比如增加一个前台的处理流程),这是第一个变化需求。第二个变化需求是这个时间条件,比如主程序觉得这个一天不合适,能不能把他的条件改成三天,或者改为十天以内再来麻烦项目经理,又或者说一个月以上的请假才来麻烦老板,就是处理条件也是一个变化点。

分析具体需求的时候,一定要把它的稳定点和变化点来分析出来,稳定点要通过抽象的流程来解决,让它变得更稳定,变化点要想办法能不能够通过扩展的方式去扩展它,扩展的方式有两种思路:第一是看看能不能通过继承的方式来去复写它,本质上就是多态;第二是能不能够通过组合的方式(比如说接口组合的方式)来实现扩展功能。

解释完这个案例背景,接下来看一下它的代码结构。先来看不使用设计模式是怎么来处理的:

#include <string>

class Context {
public:
    std::string name;
    int day;
};

class LeaveRequest {
public:
    bool HandleRequest(const Context &ctx) {
        if (ctx.day <= 1)
            HandleByMainProgram(ctx);
        else if (ctx.day <= 3)
            HandleByProjMgr(ctx);
        else
            HandleByBoss(ctx);
    }

private:
    bool HandleByBeaty(const Context &ctx) {
        
    }
    bool HandleByMainProgram(const Context &ctx) {
        
    }
    bool HandleByProjMgr(const Context &ctx) {
        
    }
    bool HandleByBoss(const Context &ctx) {

    }
};

这个示例很简单,就是有一个请假流程,按照请假多少天来顺序传递,比如说有一个主程序处理一天的,项目经理处理三天以内的,否则就是老板去处理了。这个示例的处理流程里会有很多判断,未来如果还要加东西(加一个前台的处理)或可能还要修改条件(10天、一个月等)需要修改这个类,那么整个来说这个类就是不稳定,它的职责太多了,即它又有可能会改这个条件,又可能会增加节点,内容非常的不稳定,显然这不是设计模式,都不符合设计原则。

那符合设计原则的做法是怎么样的,对于稳定点而言,需要按照请求链条进行传递,首先想到了单链表,即这里会有一个链表关系;然后这个链表里面的节点都是一个个的处理对象,需要去处理它的变化点,也就是这些处理对象的节点的个数还会增加或减少,而且它里面的内容可能也会改变,这个处理条件本质上就是处理对象的内容,要消除处理对象的差异可以用接口(就是处理具体的对象)。这个接口就是它的职责(处理具体的请求),它只是跑到处理节点上来了,所以要把节点进行抽象。

代码实现如下:

#include <string>

class Context {
public:
    std::string name;
    int day;
};

// 稳定点 抽象  变化点 扩展 (多态)
//  从单个处理节点出发,我能处理,我处理,我不能处理交给下一个人处理
//  链表关系如何抽象

class IHandler {
public:
    virtual ~IHandler() : next(nullptr) {}
    void SetNextHandler(IHandler *next) { // 链表关系
        next = next;
    }
    bool Handle(const Context &ctx) {
        if (CanHandle(ctx)) {
            return HandleRequest(ctx);
        } else if (GetNextHandler()) {
            return GetNextHandler()->Handle(ctx);
        } else {
            // err
        }
        return false;
    }
    // 通过函数来抽象 处理节点的个数  处理节点顺序
    static bool handler_leavereq(Context &ctx) {
        IHandler * h0 = new HandleByBeauty();
        IHandler * h1 = new HandleByMainProgram();
        IHandler * h2 = new HandleByProjMgr();
        IHandler * h3 = new HandleByBoss();
        h0->SetNextHandler(h1);
        h1->SetNextHandler(h2);
        h2->SetNextHandler(h3);
        return h0->Handle(ctx);
    }
protected:
    virtual bool HandleRequest(const Context &ctx) {return true};
    virtual bool CanHandle(const Context &ctx) {return true};
    IHandler * GetNextHandler() {
        return next;
    }
private:
    IHandler *next; // 组合基类指针
};

// 能不能处理,以及怎么处理
class HandleByMainProgram : public IHandler {
protected:
    virtual bool HandleRequest(const Context &ctx){
        //
        return true;
    }
    virtual bool CanHandle(const Context &ctx) {
        //
        if (ctx.day <= 10)
            return true;
        return false;
    }
};

class HandleByProjMgr : public IHandler {
protected:
    virtual bool HandleRequest(const Context &ctx){
        //
        return true;
    }
    virtual bool CanHandle(const Context &ctx) {
        //
        if (ctx.day <= 20)
            return true;
        return false;
    }
};
class HandleByBoss : public IHandler {
protected:
    virtual bool HandleRequest(const Context &ctx){
        //
        return true;
    }
    virtual bool CanHandle(const Context &ctx) {
        //
        if (ctx.day < 30)
            return true;
        return false;
    }
};

class HandleByBeauty : public IHandler {
protected:
    virtual bool HandleRequest(const Context &ctx){
        //
        return true;
    }
    virtual bool CanHandle(const Context &ctx) {
        //
        if (ctx.day <= 3)
            return true;
        return false;
    }
};

int main() {
    // 设置下一指针 
    Context ctx;
    if (IHander::handler_leavereq(ctx)) {
        cout << "请假成功";
    } else {
        cout << "请假失败";
    }
    
    return 0;
}

这里抽象了一个接口IHandler ,接口的作用是抽象稳定点,对于单个处理节点而言,稳定点是能处理就处理,不能处理就交给下一个人处理。那链表关系怎么构建的?链表关系如何,示例用一个静态的成员函数handler_leavereq来进行抽象,可以在这个地方去改对象就行了,这里面只处理对象或者顺序的改变。

接下来再来看到有一个总体的流程Handle,就是能处理就处理,不能处理就给下一个人处理,通过一个Handle函数来进行抽象,能处理就去处理;还有一个具体的处理的请求的流程HandleRequest去处理它。因为它是一个变化点,要进行扩展,在这里用了多态的方式进行扩展(能不能够处理?是怎么处理的?以及呢交给下一个人处理)。要注意到Handle是一个递归调用,递归调用交给下一个人去调用它的Handle

示例中是这么处理的:有一个主程序,要实现能不能够处理、条件是什么、是怎么处理的;还有一个项目经理,要实现能不能够处理、处理条件是什么、是怎么处理的,同样的还有一个具体的老板,要实现具体的处理的条件是什么、怎么处理的。假设要增加一个前台,只需要去扩展这个IHandler ,然后自己实现能不能处理、处理的条件是什么、怎么处理;接下来只需要在handler_leavereq这个地方去构建这个链表关系来处理它就行了。

使用就非常简单了,直接定义一个Context ,然后调用handler_leavereq就可以了。打断功能在Handle函数已经实现,处理了就直接打断了。

因此,代码结构是:从单节点出发,实现处理接口,实现一个构建链表关系的静态接口

扩展代码:

  • 实现处理接口。针对增加节点。
  • 修改静态接口。主要是调整顺序、增加节点、删除节点的处理。

2.2、符合的设计原则

  1. 组合优于继承。
  2. 面向接口编程
  3. 接口依赖。

2.3、案例分析:nginx 阶段处理

结构图:
在这里插入图片描述
调度图:
在这里插入图片描述

2.4、小结

要点:

  • 解耦请求方和处理方,请求方不知道请求是如何被处理,处理方的组成是由相互独立的子处理构成,子处理流程通过链表的方式连接,子处理请求可以按任意顺序组合。
  • 责任链请求强调请求最终由一个子处理流程处理;通过了各个子处理条件判断。
  • 责任链扩展就是功能链,功能链强调的是,一个请求依次经由功能链中的子处理流程处理。
  • 将职责以及职责顺序运行进行抽象,那么职责变化可以任意扩展,同时职责顺序也可以任意扩展。

本质:分离职责,动态组合。

结构图:
在这里插入图片描述
思维导图:
在这里插入图片描述

三、装饰器模式

定义:动态地给一个对象增加一些额外的职责。就增加功能而言,装饰器模式比生产子类更为灵活

装饰器它跟责任链不一样,责任链是一个一个的处理的接口,依次的传递,并且具备一个打断的功能;装饰器是在一个类的一个对象的,就是一个功能的基础上给他去扩展功能。定义可能听都听不懂,首先需要分析它的一个变化点和稳定点是什么,这是非常重要的。

  • 稳定点:顺序无关地增加职责
  • 变化点:增加

装饰器模式的稳定点是要给它进行增加职责,就是原来有一个类已经有职责了,现在在它的基础上增加职责,这个增加职责不要使用继承的方式,因为会生产子类,应该要通过组合的方式,组合优于继承。还有一个稳定点就是顺序无关。值得注意的是责任链也可以用来实现这个增加职责,但是责任链要求有顺序,而装饰器是跟顺序无关的,就跟装修一样,是先放电视机,还是先放沙发,它是没有什么顺序的,顺序上是无关的,可以在上面一直增加功能,并且是使用组合的方式,而不是使用继承的方式,因为继承的方式耦合度太高了。变化点就是增加。可能职责会一直不断的膨胀,一直会增加,关键的就是这个增加。

3.1、代码结构

示例背景:

普通员工有销售奖金,累计奖金,部门经理除此之外还有团队奖金;后面可能会添加环比增长奖金,同时可能针对不同的职位产生不同的奖金组合。

计算出工资总共有多少钱,具体这个工资算法的先后顺序是没什么关系的,来看一下它是怎么来实现这个功能。

首先看一下没有使用设计模式的实现方式:

// 普通员工有销售奖金,累计奖金,部门经理除此之外还有团队奖金;后面可能会添加环比增长奖金,同时可能产生不同的奖金组合;
// 销售奖金 = 当月销售额 * 4%
// 累计奖金 = 总的回款额 * 0.2%
// 部门奖金 = 团队销售额 * 1%
// 环比奖金 = (当月销售额-上月销售额) * 1%
// 销售后面的参数可能会调整
class Context {
public:
    bool isMgr;
    // User user;
    // double groupsale;
};

class Bonus {
public:
    double CalcBonus(Context &ctx) {
        double bonus = 0.0;
        bonus += CalcMonthBonus(ctx);
        bonus += CalcSumBonus(ctx);
        if (ctx.isMgr) {
            bonus += CalcGroupBonus(ctx);
        }
        return bonus;
    }
private:
    double CalcMonthBonus(Context &ctx) {
        double bonus/* = */;
        return bonus;
    }
    double CalcSumBonus(Context &ctx) {
        double bonus/* = */;
        return bonus;
    }
    double CalcGroupBonus(Context &ctx) {
        double bonus/* = */;
        return bonus;
    }
};

int main() {
    Context ctx;
    // 设置 ctx
    Bonus *bonus = new Bonus;
    bonus->CalcBonus(ctx);
}

这个很简单,就不在说明了,主要问题就是实现类不稳定,发生变化时需要不断修改具体类。

再来看一下使用了设计模式的实现方式:

#include <iostream>
// 普通员工有销售奖金,累计奖金,部门经理除此之外还有团队奖金;后面可能会添加环比增长奖金,同时可能产生不同的奖金组合;
// 销售奖金 = 当月销售额 * 4%
// 累计奖金 = 总的回款额 * 0.2%
// 部门奖金 = 团队销售额 * 1%
// 环比奖金 = (当月销售额-上月销售额) * 1%
// 销售后面的参数可能会调整
using namespace std;
class Context {
public:
    bool isMgr;
    // User user;
    // double groupsale;
};


class CalcBonus {    
public:
    CalcBonus(CalcBonus * c = nullptr) : cc(c) {}
    virtual double Calc(Context &ctx) {
        return 0.0; // 基本工资
    }
    virtual ~CalcBonus() {}

protected:
    CalcBonus* cc;
};

class CalcMonthBonus : public CalcBonus {
public:
    CalcMonthBonus(CalcBonus * c) : CalcBonus(c) {}
    virtual double Calc(Context &ctx) {
        double mbonus /*= 计算流程忽略*/; 
        return mbonus + cc->Calc(ctx);
    }
};

class CalcSumBonus : public CalcBonus {
public:
    CalcSumBonus(CalcBonus * c) : CalcBonus(c) {}
    virtual double Calc(Context &ctx) {
        double sbonus /*= 计算流程忽略*/; 
        return sbonus + cc->Calc(ctx);
    }
};

class CalcGroupBonus : public CalcBonus {
public:
    CalcGroupBonus(CalcBonus * c) : CalcBonus(c) {}
    virtual double Calc(Context &ctx) {
        double gbnonus /*= 计算流程忽略*/; 
        return gbnonus + cc->Calc(ctx);
    }
};

class CalcCycleBonus : public CalcBonus {
public:
    CalcCycleBonus(CalcBonus * c) : CalcBonus(c) {}
    virtual double Calc(Context &ctx) {
        double gbnonus /*= 计算流程忽略*/; 
        return gbnonus + cc->Calc(ctx);
    }
};

int main() {
    // 1. 普通员工
    Context ctx1;
    CalcBonus *base = new CalcBonus();
    CalcBonus *cb1 = new CalcMonthBonus(base);//依赖注入
    CalcBonus *cb2 = new CalcSumBonus(cb1);


    cb2->Calc(ctx1);
    // 2. 部门经理
    Context ctx2;
    CalcBonus *cb3 = new CalcGroupBonus(cb1);
    cb3->Calc(ctx2);
}

首先实现一个这样的接口CalcBonus,一个算工资的接口,它采用protected来组合自己,扩展功能。通过组合的方式组合基类指针的方式,它是有职责的,通常这个基类就是基本工资,基类通常还会实现一些基本功能(即一些基本职责),然后通过继承扩展,但是扩展这个计算工资的这个接口是一种多态组合,继承这个计算的接口,然后调用自己的计算的流程。即自己实现Calc,然后加上前面组合的部分。

装饰器的使用是这样的,示例中有一个普通员工算工资,先算基本工资,然后基本工资算完之后把这个基本工资通过依赖注入的方式注入到下一个,这里又有累计奖金,又有月度奖金,要把它带进去,那么最终就调用这个cb2->Calc(ctx1)来实现,去调用计算所有的工资,这就是普通员工的所有工资;还有一个部门经理,直接把前面计算好的传进来,部门经理就会把base、1、2、3全部都带进来了,那么就实现了计算工资。

这个就是装饰器模式,和顺序无关,可以任意调整。

代码结构就是:通过组合基类指针的方式,所有子类都继承基类扩展功能,使用依赖注入累加功能。

扩展代码:只需要写一个子类继承基类,然后现自己的职责,然后在掉用的地方new,最后调用职责接口。

3.2、符合的设计原则

3.3、小结

要点:

  • 通过采用组合而非继承的手法, 装饰器模式实现了在运行时动态扩展对象功能的能力,而且可以根据需要扩展多个功能。 避免了使用继承带来的“灵活性差”和“多子类衍生问题”。
  • 不是解决“多子类衍生问题”问题,而是解决“父类在多个方向上的扩展功能”问题。
  • 装饰器模式把一系列复杂的功能分散到每个装饰器当中,一般一个装饰器只实现一个功能,实现复用装饰器的功能。

什么时候使用?
不影响其他对象的情况下,以动态、透明的方式给对象添加职责;每个职责都是完全独立的功能,彼此之间没有依赖。

本质:动态组合

结构图:
在这里插入图片描述
思维导图:
在这里插入图片描述

四、组合模式

定义:将对象组合成树型结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

组合模式就是部分和整体的关系,非常的简单。稳定点就是部分和整体的层次关系,并且要使用这个对象跟组合对象具有一致性;也就是说对象之间的关系是稳定的,并且对它的使用也是具有一致性的。它的变化点就是可能会去增加一些对象,这里面的对象显然在这个地方就是树状关系,它主要解决了对象之间是树状层次关系的(有叶子节点、有组合节点,有这种组合节点就是指树状结构),相信在开发过程当中都会碰到这种场景:对象关系是属于树状关系,不同的对象一个是叶子节点对象,一个是组合对象;现在第一步就要消除他们两者之间的差异,同样是用接口来消除他们的差异。

  • 稳定点:“具备层次关系”是稳定的和对象和组合对象可统一使用。
  • 变化点:对象的职责变更和组合对象里对象数量的变更。

4.1、代码结构

class IComponent
{
public:
    IComponent(/* args */);
    ~IComponent();
    virtual void Execute() = 0;
    virtual void AddChild(IComponent *ele) {}
    virtual void RemoveChild(IComponent *ele) {}
};

class Leaf : public IComponent
{
public:
    virtual void Execute() {
        cout << "leaf exxcute" << endl;
    }
};

class Composite : public IComponent
{
private:
    std::list<IComponent*> _list;
public:
    virtual void AddChild(IComponent *ele) {
        // ...
    }
    virtual void RemoveChild(IComponent *ele) {
        // ...
    }
    virtual void Execute() {
        for (auto iter = _list.begin(); iter != _list.end(); iter++) {
            iter->Execute();
        }
    }
};

这就是具体的一个的整体和部分的一个抽象,示例中抽象的一个具体的一个组件对象IComponent,这个组件对象因为要消除叶子节点和组合对象的差异,有添加节点AddChild,还有一个具体执行的接口Execute,还有一个移除节点RemoveChild,添加节点和移除节点是组合节点,Execute是叶子节点执行具体的功能。叶子节点和组合节点都继承这个IComponent 接口,但是叶子节点只有执行的职责,只是去执行。组合对象也是继承IComponent 接口,因为要使单个对象和组合对象的使用具有一致性,所以要用接口来进行一个统一,因此它需要实现三种功能,但是具体功能职责的实现需要去委托下面的叶子节点去实现,所以要用一个容器来进行组合所有的对象,在组合对象中会把具体执行某一个功能的职责一直传递到叶子节点去执行它相对应的功能。

这个就是组合模式组合模式使用的非常的频繁,尤其是在游戏开发过程当中,游戏开发当中会有一个用户玩家,它会有很多的系统(比如签到的系统、跑马灯系统等等),这些系统都会绑定在这个用户身上,都会统一继承同一个接口,比如说跑灯功能又会有分为很多的分支,有一些子的功能通过这种组合模式把所有的功能进行一个组合,为什么我们要通过组合的方式来实现这个功能呢?因为它的变化点是可能会增加,就比如说某一个组合节点可能还会要增加一些child,这个是它的变化点,可能还要移除一些变化点,某一些功能已经不想实现了,那么要remove掉它;还有就是这个具体的组合对象,它是把它的功能进行委托的,可能具体的叶子它是执行具体某一些功能的,那么它的职责也会发生变更,通过这样子呢就进行了一个解耦具体的功能,在叶子节点当中去修改它,这个是一个变化点;第二个变化点就是具体的组合对象的叶子节点的一个变更,会通过add和remove来添加职责、移除职责。

代码结构:通过一个接口,消除叶子节点和组合节点的差异。

  • 接口用于整合整体和部分的差异。
  • 叶子节点用于实现具体的职责。
  • 组合节点职责委托叶子节点实现,同时具备组合叶子节点职责。

4.2、符合的设计原则

4.3、小结

什么时候使用组合模式

  • 如果你想表示对象的部分-整体层次结构,可以选用组合模式,把整体和部分的操作统一起来,使得层次结构实现更简单,从外部来使用这个层次结构也容易;
  • 如果你希望统一地使用组合结构中的所有对象,可以选用组合模式,这正是组合模式提供的主要功能。

怎么实现?
将叶子节点当成特殊的组合对象看待,从而统一叶子对象和组合对象。

结构图:
在这里插入图片描述
思维导图:
在这里插入图片描述

五、总结

本文探讨了责任链模式装饰器模式组合模式的设计原则、代码结构以及它们在实际案例中的应用。这些设计模式提供了灵活性和可维护性,帮助开发人员构建可扩展的软件系统。

责任链模式是一种行为设计模式,它通过将请求沿着处理链传递,使多个对象都有机会处理请求。本文介绍了责任链模式的代码结构,并讨论了如何设计符合开闭原则和单一职责原则的责任链。通过分析 nginx 阶段处理的案例,展示了责任链模式在实际中的应用。

装饰器模式是一种结构设计模式,它允许在不改变现有对象的结构的情况下,动态地向对象添加新的功能。本文探讨了装饰器模式的代码结构,并讨论了如何设计符合开闭原则和单一职责原则的装饰器。通过示例代码,展示了装饰器模式如何增强对象的功能。

组合模式是一种结构设计模式,它允许将对象组合成树形结构,以表示“整体-部分”层次关系。本文介绍了组合模式的代码结构,并讨论了如何设计符合开闭原则和单一职责原则的组合模式。通过使用组合模式,可以简化处理复杂结构的代码。

在这里插入图片描述


http://www.niftyadmin.cn/n/5234221.html

相关文章

windows+deepin v23 linux 双系统 安装前后 与 删除后 的硬盘efi分区情况,deepin v23 beta2的一些体验

知乎版&#xff1a;https://zhuanlan.zhihu.com/p/669429404 windows下安装deepin v23 beta2 电脑8GB内存&#xff0c;一个256GB固态硬盘&#xff0c;已经安装windows11。 安装双系统前分区情况&#xff1a;主要包含 windows EFI分区 和 系统分区&#xff0c;并预留了64GB给d…

无线物理层安全学习

文章目录 3.17到3.203.85到3.88 3.17到3.20 3.85到3.88

GO学习之 条件变量 sync.Cond

GO系列 1、GO学习之Hello World 2、GO学习之入门语法 3、GO学习之切片操作 4、GO学习之 Map 操作 5、GO学习之 结构体 操作 6、GO学习之 通道(Channel) 7、GO学习之 多线程(goroutine) 8、GO学习之 函数(Function) 9、GO学习之 接口(Interface) 10、GO学习之 网络通信(Net/Htt…

笔记-基于CH579M模块通过网线直连电脑进行数据收发(无需网络)

刚学习&#xff0c;做个记录。 基于CH579M模块通过网线直连电脑进行数据收发(无需网络) 目录 一、工具1、CH579模块2、 网线3、电脑以及网络调试工具 二、操作步骤1、TCP/UDP等程序下载以及设置以太网IP2、网络断开3、检查以太网是否正常显示并稳定4、打开网络调试助手进行测试…

np.array无法直接用matplotlib画图,因为需要借用np.squeeze先转化

文章目录 前言一、使用步骤1.没使用np.squeeze转化2.使用np.squeeze转化 前言 实际工作中&#xff0c;时而难免会遇见np.array无法直接用matplotlib画图的情况&#xff0c;这个时候&#xff0c;是因为在画图之前少了一个步骤&#xff0c;需要先借用np.squeeze先转化 一、使用步…

ctfhub技能树_web_web前置技能_HTTP

目录 一、HTTP协议 1.1、请求方式 1.2、302跳转 1.3、Cookie 1.4、基础认证 1.5、响应包源代码 一、HTTP协议 1.1、请求方式 注&#xff1a;HTTP协议中定义了八种请求方法。这八种都有&#xff1a;1、OPTIONS &#xff1a;返回服务器针对特定资源所支持的HTTP请求方法…

mysql原理--重新认识MySQL

1.MySQL请求处理 1.1.查询缓存 MySQL 服务器程序处理查询请求时&#xff0c;会把刚刚处理过的查询请求和结果缓存起来&#xff0c;如果下一次有一模一样的请求过来&#xff0c;直接从缓存中查找结果就好了&#xff0c;就不用再傻呵呵的去底层的表中查找了。这个查询缓存可以在不…

Go语言实现深度学习的正向传播和反向传播

文章目录 开发前言开发理论图解理论数据类型数学函数数据节点统一抽象变量数据节点常量数据节点单目运算封装双目运算封装算子节点统一抽象基础算子加法算子减法算子乘法算子除法算子指数算子对数算子正切算子正弦算子余弦算子数据流图正向传播反向传播运行示例开发总结 开发前…