揭示C++设计模式中的实现结构及应用——结构型设计模式

简介

结构型模式(Structural Pattern)描述如何将类或者对象结合在一起形成更大的结构,就像搭积木,可以通过 简单积木的组合形成复杂的、功能更为强大的结构。

结构型模式可以分为类结构型模式和对象结构型模式:

  • 类结构型模式:关心类的组合,由多个类可以组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系
  • 对象结构型模式:关心类与对象的组合,通过关联关系使得在一 个类中定义另一个类的实例对象,然后通过该对象调用其方法

根据“ 合成复用原则 ”,在系统中尽量使用关联关系来替代继 承关系,因此大部分结构型模式都是对象结构型模式。

其包含以下几个模式:

  • 适配器模式:让接口不兼容的对象能够相互合作。
  • 桥接模式:可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构,从而能在开发时分别使用。
  • 组合模式:你可以使用它将对象组合成树状结构,并且能像使用独立对象一样使用它们。
  • 装饰模式:允许你将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。
  • 外观模式:能为程序库、框架或其他复杂类提供一个简单的接口。
  • 享元模式:摒弃了再每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,让你能在优先的内存容量中载入更多对象。
  • 代理模式:让你能够提供对象的替代品或其占位符。代理控制着对于原对象的访问,并允许在将请求提交给对象前后进行一些处理。

适配器模式(Adapter)

前言

假如你正在开发一款股票市场监测程序,它会从不同来源下载 XML 格式的股票数据,然后向用户呈现出美观的图表。

在开发过程中, 你决定在程序中整合一个第三方智能分析函数库。但是遇到了一个问题, 那就是分析函数库只兼容JSON 格式的数据。

在这里插入图片描述

你可以修改程序库来支持XML。但是,这可能需要修改部分依赖该程序库的现有代码。甚至还有更糟糕的情况,你可能根本没有程序库的源代码,从而无法对其进行修改。

你可以创建一个适配器。这是一个特殊的对象,能够转换对象接口,使其能与其他对象进行交互。

适配器模式通过封装对象将复杂的转换过程隐藏于幕后。被封装的对象甚至察觉不到适配器的存在。例如,你可以使用一个将所有数据转换为英制单位(如英尺和英里)的适配器封装运行于米和千米单位制中的对象。

适配器不仅可以转换不同格式的数据,其还有助于采用不同接口的对象之间的合作。它的运作方式如下:

  • 适配器实现与其中一个现有对象兼容的接口。
  • 现有对象可以使用该接口安全地调用适配器方法。
  • 适配器方法被调用后将以另一个对象兼容的格式和顺序将请求传递给该对象。

在这里插入图片描述

实现结构

实现时使用了构成原则:适配器实现了其中一个对象的接口,并对另一个对象进行封装。所有流行的编程语言都可以实现适配器。

在这里插入图片描述

  1. 客户端(Client)是包含当前程序业务逻辑的类。
  2. 客户端接口(Client Interface) 描述了其他类与客户端代码合作时必须遵循的协议。
  3. 服务(Service)中有一些功能类(通常来自第三方或遗留系统)。客户端与其接口不兼容,因此无法直接调用其功能。
  4. 适配器(Adapter) 是一个可以同时与客户端和服务交互的类:它在实现客户端接口的同时封装了服务对象。适配器接受客户端通过适配器接口发起的调用,并将其转换为适用于被封装服务对象的调用。
  5. 客户端代码只需通过接口与适配器交互即可,无需与具体的适配器类耦合。因此,你可以向程序中添加新类型的适配器而无需修改已有代码。这在服务类的接口被更改或替换时很有用:你无需修改客户端代码就可以创建新的适配器类。

适用场景及优缺点

  • 当你希望使用某个类,但是其接口与其他代码不兼容时,可以使用适配器类。

适配器模式允许你创建一个中间层类,其可作为代码与遗留类、第三方类或提供怪异接口的类之间的转换器。

优点

  • 单一职责原则你可以将接口或数据转换代码从程序主要业务逻辑中分离。
  • 开闭原则。只要客户端代码通过客户端接口与适配器进行交互,你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。

缺点:代码整体复杂度增加,因为你需要新增一系列接口和类。有时直接更改服务类使其与其他代码兼容会更简单。

实例演示

考虑经典的“方钉和圆孔”的问题,我们要做的是让方钉适配圆孔。

适配器让方钉假扮成一个圆钉(RoundPeg),其半径等于方钉(SquarePeg)横截面对角线的一半(即能容纳方钉的最小外接圆的半径)。

ClientInterface.h:

#ifndef CLIENT_INTERFACE_H_
#define CLIENT_INTERFACE_H_

// 圆钉: 客户端接口, 在C++中定义成抽象基类
class RoundPeg {
 public:
    RoundPeg() {}
    virtual int get_radius() = 0;
};

#endif  // CLIENT_INTERFACE_H_

Adapter.h:

#ifndef ADAPTER_H_
#define ADAPTER_H_

#include <cmath>
#include "Service.h"
#include "ClientInterface.h"

// 方钉适配器: 该适配器能让客户端将方钉放入圆孔中
class SquarePegAdapter : public RoundPeg {
 public:
    explicit SquarePegAdapter(SquarePeg* sp) : square_peg_(sp) {}
    int get_radius() override {
        return square_peg_->get_width() * sqrt(2) / 2;
    }

 private:
    SquarePeg* square_peg_;
};

#endif  // ADAPTER_H_

Service.h:

#ifndef SERVICE_H_
#define SERVICE_H_

// 方钉: 适配者类, 即和客户端不兼容的类
class SquarePeg {
 public:
    explicit SquarePeg(int w) : width_(w) {}
    int get_width() {
        return width_;
    }

 private:
    int width_;
};

#endif  // SERVICE_H_

Client.h:

#ifndef CLIENT_H_
#define CLIENT_H_

#include "ClientInterface.h"

// 圆孔: 客户端类
class RoundHole {
 public:
    explicit RoundHole(int r) : radius_(r) {}
    int get_radius() {
        return radius_;
    }
    bool isFit(RoundPeg* rp) {
        return radius_ >= rp->get_radius();
    }

 private:
    int radius_;
};

#endif  // CLIENT_H_

main.cpp:

#include <iostream>
#include "Client.h"
#include "Adapter.h"

int main() {
    // 半径为10的圆孔
    RoundHole* hole = new RoundHole(10);

    // 半径分别为5和20的大小方钉 + 它们的适配器
    SquarePeg* samll_square_peg = new SquarePeg(5);
    SquarePeg* large_square_peg = new SquarePeg(20);
    SquarePegAdapter* small_square_peg_adapter = new SquarePegAdapter(samll_square_peg);
    SquarePegAdapter* large_square_peg_adapter = new SquarePegAdapter(large_square_peg);

    // hole->isFit(samll_square_peg);  // 编译报错
    // hole->isFit(large_square_peg);  // 编译报错
    if (hole->isFit(small_square_peg_adapter)) {
        std::cout << "small square peg fits the hole" << std::endl;
    } else {
        std::cout << "small square peg don't fit the hole" << std::endl;
    }
    if (hole->isFit(large_square_peg_adapter)) {
        std::cout << "large square peg fits the hole" << std::endl;
    } else {
        std::cout << "large square peg don't fit the hole" << std::endl;
    }
}

结果展示

small square peg fits the hole
large square peg don't fit the hole

桥接模式(Bridge)

前言

假如你有一个几何形状(Shape)类, 从它能扩展出两个子类: 圆形(Circle)和方形(Square)。你希望对这样的类层次结构进行扩展以使其包含颜色, 所以你打算创建名为红色(Red)和蓝色(Blue)的形状子类。但是,由于你已有两个子类,所以总共需要创建四个类才能覆盖所有组合,例如蓝色圆形(BlueCircle)和红色方形(RedSquare)。

在这里插入图片描述

在层次结构中新增形状和颜色将导致代码复杂程度指数增长。例如添加三角形状,你需要新增两个子类,也就是每种颜色一个;此后新增一种新颜色需要新增三个子类,即每种形状一个。如此以往,情况会越来越糟糕。

问题的根本原因是我们试图在两个独立的维度——形状与颜色——上扩展形状类。这在处理类继承时是很常见的问题。

桥接模式通过将继承改为组合的方式来解决这个问题。具体来说,就是抽取其中一个维度并使之成为独立的类层次,这样就可以在初始类中引用这个新层次的对象,从而使得一个类不必拥有所有的状态和行为。

在这里插入图片描述

根据该方法,我们可以将颜色相关的代码抽取到拥有红色和蓝色两个子类的颜色类中,然后在形状类中添加一个指向某一颜色对象的引用成员变量。现在,形状类可以将所有与颜色相关的工作委派给连入的颜色对象。这样的引用就成为了形状和颜色之间的桥梁。此后,新增颜色将不再需要修改形状的类层次,反之亦然。

实现结构

在这里插入图片描述

  1. 抽象部分(Abstraction)提供高层控制逻辑, 依赖于完成底层实际工作的实现对象。
  2. 实现部分(Implementation)为所有具体实现声明通用接口。抽象部分仅能通过在这里声明的方法与实现对象交互。抽象部分可以列出和实现部分一样的方法,但是抽象部分通常声明一些复杂行为,这些行为依赖于多种由实现部分声明的原语操作。
  3. 具体实现(Concrete Implementations)中包括特定于平台的代码。
  4. 精确抽象(Refined Abstraction)提供控制逻辑的变体。与其父类一样,它们通过通用实现接口与不同的实现进行交互。
  5. 通常情况下,客户端(Client)仅关心如何与抽象部分合作。但是,客户端需要将抽象对象与一个实现对象连接起来。

适用场景及优缺点

  • 如果你想要拆分或重组一个具有多重功能的庞杂类(例如能与多个数据库服务器进行交互的类),可以使用桥接模式。

类的代码行数越多,弄清其运作方式就越困难,对其进行修改所花费的时间就越长。一个功能上的变化可能需要在整个类范围内进行修改,而且常常会产生错误,甚至还会有一些严重的副作用。桥接模式可以将庞杂类拆分为几个类层次结构。

  • 如果你希望在几个独立维度上扩展一个类,可使用该模式。

桥接建议将每个维度抽取为独立的类层次。初始类将相关工作委派给属于对应类层次的对象,无需自己完成所有工作。

  • 如果你需要在运行时切换不同实现方法,可使用桥接模式。

当然并不是说一定要实现这一点,桥接模式可替换抽象部分中的实现对象,具体操作就和给成员变量赋新值一样简单。

优点:

  • 你可以创建与平台无关的类和程序。
  • 客户端代码仅与高层抽象部分进行互动,不会接触到平台的详细信息。
  • 开闭原则。你可以新增抽象部分和实现部分,且它们之间不会相互影响。
  • 单一职责原则。抽象部分专注于处理高层逻辑,实现部分处理平台细节。

缺点:对高内聚的类使用该模式可能会让代码更加复杂。

实例演示

Abstraction.h:

#ifndef ABSTRACTION_H_
#define ABSTRACTION_H_

#include <string>
#include "Implementation.h"

// 抽象类: Pen
class Pen {
 public:
    virtual void draw(std::string name) = 0;
    void set_color(Color* color) {
        color_ = color;
    }

 protected:
    Color* color_;
};

#endif  // ABSTRACTION_H_

RefinedAbstraction.h:

#ifndef REFINED_ABSTRACTION_H_
#define REFINED_ABSTRACTION_H_

#include <string>
#include "Abstraction.h"

// 精确抽象类: BigPen
class BigPen : public Pen {
 public:
    void draw(std::string name) {
        std::string pen_type = "大号钢笔绘制";
        color_->bepaint(pen_type, name);
    }
};

// 精确抽象类: SmallPencil
class SmallPencil : public Pen {
 public:
    void draw(std::string name) {
        std::string pen_type = "小号铅笔绘制";
        color_->bepaint(pen_type, name);
    }
};

#endif  // REFINED_ABSTRACTION_H_

Implementation.h:

#ifndef IMPLEMENTATION_H_
#define IMPLEMENTATION_H_

#include <string>
#include <iostream>

// 实现类接口: 颜色
class Color {
 public:
    virtual void bepaint(std::string pen_type, std::string name) = 0;
};

#endif  // IMPLEMENTATION_H_

ConcreteImplementation.h:

#ifndef CONCRETE_IMPLEMENTATION_H_
#define CONCRETE_IMPLEMENTATION_H_

#include <string>
#include "Implementation.h"

// 具体实现类: Red
class Red : public Color {
 public:
    void bepaint(std::string pen_type, std::string name) override {
        std::cout << pen_type << "红色的" << name << "." << std::endl;
    }
};

// 具体实现类: Green
class Green : public Color {
 public:
    void bepaint(std::string pen_type, std::string name) override {
        std::cout << pen_type << "绿色的" << name << "." << std::endl;
    }
};


#endif  // CONCRETE_IMPLEMENTATION_H_

main.cpp:

#include "ConcreteImplementation.h"
#include "RefinedAbstraction.h"

int main() {
    // 客户端根据运行时参数获取对应的Color和Pen
    Color* color = new Red();
    Pen* pen = new SmallPencil();

    pen->set_color(color);
    pen->draw("太阳");

    delete color;
    delete pen;
}

输出结果

小号铅笔绘制红色的太阳.

组合模式(Composite)

前言

例如你有两类对象:产品和盒子。一个盒子中可以包含多个产品或者几个较小的盒子。这些小盒子中同样可以包含一些产品或者更小的盒子。

假设你希望在这些类的基础上开发一个订购系统。订单中可以包含无包装的简单产品,也可以包含装满产品的盒子。此时你会如何计算每张订单的总价格呢?

在这里插入图片描述

从上图可以看出,订单中可能包括各种产品,这些产品放置在盒子中,然后又被放入一层又一层更大的盒子总。整个结构看上去像是一棵倒过来的树。

组合模式建议使用一个通用结构来与「产品」和「盒子」进行交互,并且在该接口中声明一个计算总价的方法:

  • 对于一个具体产品,该方法直接返回其价格
  • 对于一个盒子,该方法遍历盒子中的所有项目,返回该盒子的总价格

该方法的最大优点在于你无需了解构成树状结构的对象的具体类。你也无需了解对象是简单的产品还是复杂的盒子。你只需要调用通用接口以相同的方式对其进行处理即可。当你调用该方法后,对象会将请求沿着树结构传递下去。

实现结构

在这里插入图片描述

  1. 组件(Component)接口描述了树中简单项目和复杂项目所共有的操作。
  2. 叶节点(Leaf)是树的基本结构,它不包含子项目。一般情况下,叶节点最终会完成大部分的实际工作,因为它们无法将工作指派给其他部分。
  3. 容器(Container)是包含叶节点或其他容器等子项目的单位。容器不知道其子项目所属的具体类,它只通过通用的组件接口与其子项目交互。容器接收到请求后会将工作分配给自己的子项目,处理中间结果,然后将最终结果返回给客户端。
  4. 客户端(Client)通过组件接口与所有项目交互。因此, 客户端能以相同方式与树状结构中的简单或复杂项目交互。

适用场景及优缺点

  • 如果你需要实现树状对象结构,可以使用组合模式。

组合模式为你提供了两种共享公共接口的基本元素类型:简单叶节点和复杂容器。容器中可以包含叶节点和其他容器。这使得你可以构建树状嵌套递归对象结构。

  • 如果你希望客户端代码以相同方式处理简单和复杂元素,可 以使用该模式。

组合模式中定义的所有元素共用同一个接口。在这一接口的帮助下,客户端不必在意其所使用的对象的具体类。

优点:

  • 你可以利用多态和递归机制更方便地使用复杂树结构。
  • 开闭原则。无需更改现有代码,你就可以在应用中添加新元素,使其成为对象树的一部分。

缺点:对于功能差异较大的类,提供公共接口或许会有困难。在特定情况下,你需要过度一般化组件接口,使其变得令人难以理解。

实例演示

本例将借助组合模式帮助你在图形编辑器中实现一系列的几何图形。

组合图形(CompoundGraphic)是一个容器,它可以由多个包括容器在内的子图形构成。组合图形和简单图形拥有相同的方法。但是组合图形自身并不完成具体工作,而是将请求递归地传递给自己的子项目,然后“汇总”结果。

通过所有图形类所共有的接口,客户端代码可以与所有图形互动。因此,客户端不知道与其交互的是简单图形还是组合图形。客户端可以与非常复杂的对象结构进行交互,而无需与组成该结构的实体类紧密耦合。

Component.h:

#ifndef COMPONENT_H_
#define COMPONENT_H_

// 组件接口会声明组合中简单和复杂对象的通用操作, C++中实现成抽象基类。
class Graphic {
 public:
    virtual void move2somewhere(int x, int y) = 0;
    virtual void draw() = 0;
};

#endif  // COMPONENT_H_

Leaf.h:

#ifndef LEAF_H_
#define LEAF_H_

#include <cstdio>
#include "Component.h"

// 叶节点类代表组合的中断对象。叶节点对象中不能包含任何子对象。
// 叶节点对象通常会完成实际的工作, 组合对象则仅会将工作委派给自己的子部件。

// 点
class Dot : public Graphic {
 public:
    Dot(int x, int y) : x_(x), y_(y) {}
    void move2somewhere(int x, int y) override {
        x_ += x;
        y_ += y;
        return;
    }
    void draw() override {
        printf("在(%d,%d)处绘制点\n", x_, y_);
        return;
    }

 private:
    int x_;
    int y_;
};

// 圆
class Circle : public Graphic {
 public:
    explicit Circle(int r, int x, int y) : radius_(r), x_(x), y_(y) {}
    void move2somewhere(int x, int y) override {
        x_ += x;
        y_ += y;
        return;
    }
    void draw() override {
        printf("以(%d,%d)为圆心绘制半径为%d的圆\n", x_, y_, radius_);
    }

 private:
    // 半径与圆心坐标
    int radius_;
    int x_;
    int y_;
};

#endif  // LEAF_H_

Composite.h:

#ifndef COMPOSITE_H_
#define COMPOSITE_H_

#include <map>
#include "Component.h"

// 组合类表示可能包含子项目的复杂组件。组合对象通常会将实际工作委派给子项目,然后“汇总”结果。
class CompoundGraphic : public Graphic {
 public:
    void add(int id, Graphic* child) {
        childred_[id] = (child);
    }
    void remove(int id) {
        childred_.erase(id);
    }
    void move2somewhere(int x, int y) override {
        for (auto iter = childred_.cbegin(); iter != childred_.cend(); iter++) {
            iter->second->move2somewhere(x, y);
        }
    }
    void draw() override {
        for (auto iter = childred_.cbegin(); iter != childred_.cend(); iter++) {
            iter->second->draw();
        }
    }

 private:
    // key是图表id, value是图表指针
    std::map<int, Graphic*> childred_;
};

#endif  // COMPOSITE_H_

main.cpp:

#include "Composite.h"
#include "Leaf.h"

int main() {
    // 组合图
    CompoundGraphic* all = new CompoundGraphic();

    // 添加子图
    Dot* dot1 = new Dot(1, 2);
    Circle *circle = new Circle(5, 2, 2);
    CompoundGraphic* child_graph = new CompoundGraphic();
    Dot* dot2 = new Dot(4, 7);
    Dot* dot3 = new Dot(3, 2);
    child_graph->add(0, dot2);
    child_graph->add(1, dot3);

    // 将所有图添加到组合图中
    all->add(0, dot1);
    all->add(1, circle);
    all->add(2, child_graph);

    // 绘制
    all->draw();

    delete all;
    delete dot1;
    delete dot2;
    delete dot3;
    delete circle;
    return 0;
}

输出结果

(1,2)处绘制点
以(2,2)为圆心绘制半径为5的圆
在(4,7)处绘制点
在(3,2)处绘制点

装饰模式(Decorator)

前言

假设你正在开发一个提供通知功能的库,其他程序可使用它向用户发送关于重要事件的通知。

库的最初版本基于通知器Notifier 类,其中只有很少的几个成员变量,一个构造函数和一个send 发送方法。该方法可以接收来自客户端的消息参数,并将该消息发送给一系列的邮箱,邮箱列表则是通过构造函数传递给通知器的。作为客户端的第三方程序仅会创建和配置通知器对象一次,然后在有重要事件发生时对其进行调用。

在这里插入图片描述

此后某个时刻,你会发现库的用户希望使用除邮件通知之外的功能。许多用户会希望接收关于紧急事件的手机短信,还有些用户希望在微信上接收消息, 而公司用户则希望在QQ上接收消息。

在这里插入图片描述

但是很快有人会问:“为什么不同时使用多种通知形式呢?如果房子着火了,你大概会想在所有渠道中都收到相同的消息吧。”

你可以尝试创建一个特殊子类来将多种通知方法组合在一起以解决该问题。但这种方式会使得代码量迅速膨胀,不仅仅是程序库代码,客户端代码也会如此。

在这里插入图片描述
你必须找到其他方法来规划通知类的结构,否则它们的数量会极其庞大。但是你不能使用继承,因为可能引发的几个严重问题:

  • 继承是静态的。你无法在运行时更改已有对象的行为,只能使用由不同子类创建的对象来替代当前的整个对象。
  • 子类只能有一个父类。大部分编程语言不允许一个类同时继承多个类的行为。

装饰模式提供了一种灵活的方式来扩展对象的功能,而无需通过子类化来实现。在装饰模式中,创建一个装饰器类,该类包含一个指向被装饰对象的引用,并实现与被装饰对象相同的接口。然后,通过创建不同的装饰器类来为对象添加新的行为或责任。

比如在消息通知示例中,我们可以将简单邮件通知行为放在基类通知器中,但将所有其他通知方法放入装饰中。

在这里插入图片描述

实现结构

在这里插入图片描述

  1. 部件(Component)类声明封装器和被封装对象的公用接口。
  2. 具体部件(Concrete Component)类是被封装对象所属的类。它定义了基础行为,但装饰类可以改变这些行为。
  3. 基础装饰(Base Decorator)类拥有一个指向被封装对象的引用成员变量。该变量的类型应当被声明为通用部件接口,这样它就可以引用具体的部件和装饰。装饰基类会将所有操作委派给被封装的对象。
  4. 具体装饰类定义了可动态添加到部件的额外行为。具体装饰类会重写装饰基类的方法,并在调用父类方法之前或之后进行额外的行为。
  5. 客户端可以使用多层装饰来封装部件, 只要它能使用通用接口与所有对象互动即可。

适用场景及优缺点

  • 如果你希望在无需修改代码的情况下即可使用对象,且希望在运行时为对象新增额外的行为,可以使用装饰模式。

装饰能将业务逻辑组织为层次结构,你可为各层创建一个装饰,在运行时将各种不同逻辑组合成对象。由于这些对象都遵循通用接口,客户端代码能以相同的方式使用这些对象。

  • 如果用继承来扩展对象行为的方案难以实现或者根本不可行,你可以使用该模式。

许多编程语言使用final 最终关键字来限制对某个类的进一步扩展。复用最终类已有行为的唯一方法是使用装饰模式:用封装器对其进行封装。

优点:

  • 你无需创建新子类即可扩展对象的行为。
  • 你可以在运行时添加或删除对象的功能。
  • 你可以用多个装饰封装对象来组合几种行为。
  • 单一职责原则。你可以将实现了许多不同行为的一个大类拆 分为多个较小的类。

缺点:

  • 在封装器栈中删除特定封装器比较困难。
  • 实现行为不受装饰栈顺序影响的装饰比较困难。
  • 各层的初始化配置代码看上去可能会很糟糕。

实例演示

正常情况下磁盘中的数据文件可以直接读取,但是对于敏感数据需要进行压缩和加密。我们需要实现两个装饰器,它们都改变了从磁盘读写数据的方式

  • 加密:对数据进行脱敏处理
  • 压缩:对数据进行压缩处理

Component.h:

#ifndef COMPONENT_H_
#define COMPONENT_H_

#include <string>

// 部件: 是具体部件和装饰类的共同基类, 在C++中实现成抽象基类
class DataSource {
 public:
    virtual void writeData(std::string data) = 0;
};

#endif  // COMPONENT_H_

ConcreteComponent.h:

#ifndef CONCRETE_COMPONENT_H_
#define CONCRETE_COMPONENT_H_

#include <string>
#include <cstdio>
#include <iostream>
#include "Component.h"

// 具体组件提供操作的默认实现, 这些类在程序中可能会有几个变体
class FileDataSource : public DataSource {
 public:
    explicit FileDataSource(std::string file_name) : file_name_(file_name) {}
    void writeData(std::string data) override {
        printf("写入文件%s中: %s\n", file_name_.c_str(), data.c_str());
        return;
    }

 private:
    std::string file_name_;
};

#endif  // CONCRETE_COMPONENT_H_

BaseDecorator.h:

#ifndef BASE_DECORATOR_H_
#define BASE_DECORATOR_H_

#include <string>
#include "Component.h"

// 装饰基类和其他组件遵循相同的接口。该类的主要任务是定义所有具体装饰的封装接口。
// 封装的默认实现代码中可能会包含一个保存被封装组件的成员变量,并且负责对其进行初始化。
class DataSourceDecorator : public DataSource {
 public:
    explicit DataSourceDecorator(DataSource* ds) : data_source_(ds) {}
    void writeData(std::string data) override {
        data_source_->writeData(data);
    }

 protected:
    DataSource* data_source_;  // component
};

#endif  // BASE_DECORATOR_H_

ConcreteDecorator.h:

#ifndef CONCRETE_DECORATOR_H_
#define CONCRETE_DECORATOR_H_

#include <string>
#include "BaseDecorator.h"

// 加密装饰器
class EncryptionDecorator : public DataSourceDecorator {
 public:
    using DataSourceDecorator::DataSourceDecorator;
    void writeData(std::string data) override {
        // 1. 对传递数据进行加密(这里仅简单实现)
        data = "已加密(" + data + ")";
        // 2. 将加密后数据传递给被封装对象 writeData(写入数据)方法
        data_source_->writeData(data);
        return;
    }
};

// 压缩装饰器
class CompressionDecorator : public DataSourceDecorator {
 public:
    using DataSourceDecorator::DataSourceDecorator;
    void writeData(std::string data) override {
        // 1. 对传递数据进行压缩(这里仅简单实现)
        data = "已压缩(" + data + ")";
        // 2. 将压缩后数据传递给被封装对象 writeData(写入数据)方法
        data_source_->writeData(data);
        return;
    }
};

#endif  // CONCRETE_DECORATOR_H_

main.cpp:

#include "ConcreteComponent.h"
#include "ConcreteDecorator.h"

int main() {
    FileDataSource* source1 = new FileDataSource("stdout");

    // 将明码数据写入目标文件
    source1->writeData("tomocat");

    // 将压缩数据写入目标文件
    CompressionDecorator* source2 = new CompressionDecorator(source1);
    source2->writeData("tomocat");

    // 将压缩且加密数据写入目标文件
    EncryptionDecorator* source3 = new EncryptionDecorator(source2);
    source3->writeData("tomocat");

    delete source1;
    delete source2;
    delete source3;
}

输出结果:

写入文件stdout: tomocat
写入文件stdout: 已压缩(tomocat)
写入文件stdout: 已压缩(已加密(tomocat))

外观模式(Facade)

前言

假设你必须在代码中使用某个复杂的库或框架中的众多对象。正常情况下,你需要负责所有对象的初始化工作、管理其依赖关系并按正确的顺序执行方法等。

最终,程序中类的业务逻辑将与第三方类的实现细节紧密耦合,使得理解和维护代码的工作很难进行。

外观类为包含许多活动部件的复杂子系统提供一个简单的接口。与直接调用子系统相比,外观提供的功能可能比较有限,但它却包含了客户端真正关心的功能。

例如, 上传猫咪搞笑短视频到社交媒体网站的应用可能会用到专业的视频转换库, 但它只需使用一个包含encode(filename, format) 方法(以文件名与文件格式为参数进行编码的方法)的类即可。在创建这个类并将其连接到视频转换库后,你就拥有了自己的第一个外观。

实现结构

在这里插入图片描述

  1. 外观(Facade)提供了一种访问特定子系统功能的便捷方式,其了解如何重定向客户端请求,知晓如何操作一切活动部件。
  2. 创建附加外观(Additional Facade) 类可以避免多种不相关的功能污染单一外观,使其变成又一个复杂结构。客户端和其他外观都可使用附加外观。
  3. 复杂子系统(Complex Subsystem)由数十个不同对象构成。如果要用这些对象完成有意义的工作,你必须深入了解子系统的实现细节,比如按照正确顺序初始化对象和为其提供正确格式的数据。子系统类不会意识到外观的存在,它们在系统内运作并且相互之间可直接进行交互。
  4. 客户端(Client)使用外观代替对子系统对象的直接调用。

适用场景及优缺点

  • 如果你需要一个指向复杂子系统的直接接口,且该接口的功能有限,则可以使用外观模式。

子系统通常会随着时间的推进变得越来越复杂。即便是应用了设计模式,通常你也会创建更多的类。尽管在多种情形中子系统可能是更灵活或易于复用的,但其所需的配置和样板代码数量将会增长得更快。为了解决这个问题,外观将会提供指向子系统中最常用功能的快捷方式,能够满足客户端的大部分需求。

  • 如果需要将子系统组织为多层结构,可以使用外观。

创建外观来定义子系统中各层次的入口。你可以要求子系统仅使用外观来进行交互,以减少子系统之间的耦合。

优点:你可以让自己的代码独立于复杂子系统。

缺点:外观可能成为与程序中所有类都耦合的上帝对象。

实例演示

计算机本身是一个及其复杂的系统,我们通过外观模式屏蔽电脑开机这一动作背后复杂子系统的运作。

Facade.h:

#ifndef FACADE_H_
#define FACADE_H_

#include "SubSystem.h"

class ComputerOperator {
 public:
    ComputerOperator() {
        memory_ = new Memory();
        processor_ = new Processor();
        hard_disk_ = new HardDisk();
        os_ = new OS();
    }
    ~ComputerOperator() {
        delete memory_;
        delete processor_;
        delete hard_disk_;
        delete os_;
        memory_ = nullptr;
        processor_ = nullptr;
        hard_disk_ = nullptr;
        os_ = nullptr;
    }

    void powerOn() {
        std::cout << "正在开机..." << std::endl;
        memory_->selfCheck();
        processor_->run();
        hard_disk_->read();
        os_->load();
        std::cout << "开机成功!" << std::endl;
    }

 private:
    Memory* memory_;
    Processor* processor_;
    HardDisk* hard_disk_;
    OS* os_;
};

#endif  // FACADE_H_

SubSystem.h:

#ifndef SUB_SYSTEM_H_
#define SUB_SYSTEM_H_

#include<iostream>

// 内存
class Memory {
 public:
    Memory() {}
    void selfCheck() {
        std::cout << "内存自检中..." << std::endl;
        std::cout << "内存自检完成!" << std::endl;
    }
};

// 处理器
class Processor {
 public:
    Processor() {}
    void run() {
        std::cout << "启动CPU中..." << std::endl;
        std::cout << "启动CPU成功!" << std::endl;
    }
};

// 硬盘
class HardDisk {
 public:
    HardDisk() {}
    void read() {
        std::cout << "读取硬盘中..." << std::endl;
        std::cout << "读取硬盘成功!" << std::endl;
    }
};

// 操作系统
class OS {
 public:
    OS() {}
    void load() {
        std::cout << "载入操作系统中..." << std::endl;
        std::cout << "载入操作系统成功!" << std::endl;
    }
};

#endif  // SUB_SYSTEM_H_

main.cpp:

#include "Facade.h"

int main() {
    ComputerOperator* computer_operator = new ComputerOperator();
    computer_operator->powerOn();
    delete computer_operator;
}

输出结果

正在开机...
内存自检中...
内存自检完成!
启动CPU中...
启动CPU成功!
读取硬盘中...
读取硬盘成功!
载入操作系统中...
载入操作系统成功!
开机成功!

享元模式(Flyweight)

前言

假如你希望在长时间工作后放松一下,所以开发了一款简单的游戏:玩家们在地图上移动并相互射击。你决定实现一个真实的粒子系统,并将其作为游戏的特色。大量的子弹、导弹和爆炸弹片会在整个地图上穿行,为玩家提供紧张刺激的游戏体验。

开发完成后,你推送提交了最新版本的程序,并在编译游戏后将其发送给了一个朋友进行测试。尽管该游戏在你的电脑上完美运行,但是你的朋友却无法长时间进行游戏:游戏总是会在他的电脑上运行几分钟后崩溃。在研究了几个小时的调试消息记录后,你发现导致游戏崩溃的原因是内存容量不足。朋友的设备性能远比不上你的电脑,因此游戏运行在他的电脑上时很快就会出现问题。

真正的问题与粒子系统有关。每个粒子(一颗子弹、一枚导弹或一块弹片)都由包含完整数据的独立对象来表示。当玩家在游戏中鏖战进入高潮后的某一时刻,游戏将无法在剩余内存中载入新建粒子,于是程序就崩溃了。

在这里插入图片描述

仔细观察粒子(Particle)类, 你可能会注意到颜色(color)和精灵图(sprite)这两个成员变量所消耗的内存要比其他变量多得多。更糟糕的是,对于所有的粒子来说,这两个成员变量所存储的数据几乎完全一样(比如所有子弹的颜色和精灵图都一样)。

在这里插入图片描述

每个粒子的另一些状态(坐标、移动矢量和速度)则是不同的。因为这些成员变量的数值会不断变化。这些数据代表粒子在存续期间不断变化的情景,但每个粒子的颜色和精灵图则会保持不变。

对象的常量数据通常被称为内在状态,其位于对象中,其他对象只能读取但不能修改其数值。而对象的其他状态常常能被其他对象“从外部”改变,因此被称为外在状态。

享元模式建议不在对象中存储外在状态,而是将其传递给依赖于它的一个特殊方法。程序只在对象中保存内在状态,以方便在不同情景下重用。这些对象的区别仅在于其内在状态(与外在状态相比,内在状态的变体要少很多),因此你所需的对象数量会大大削减。

在这里插入图片描述

假如能从粒子类中抽出外在状态,那么我们只需三个不同的对象(子弹、导弹和弹片)就能表示游戏中的所有粒子。你现在很可能已经猜到了,我们将这样一个仅存储内在状态的对象称为享元。

实现结构

在这里插入图片描述

  1. 享元模式只是一种优化。在应用该模式之前,你要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题,并且确保该问题无法使用其他更好的方式来解决。
  2. 享元(Flyweight)类包含原始对象中部分能在多个对象中共享的状态。同一享元对象可在许多不同情景中使用。享元中存储的状态被称为“内在状态”。传递给享元方法的状态被称为“外在状态”。
  3. 情景(Context)类包含原始对象中各不相同的外在状态。情景与享元对象组合在一起就能表示原始对象的全部状态。
  4. 通常情况下,原始对象的行为会保留在享元类中。因此调用享元方法必须提供部分外在状态作为参数。但你也可将行为移动到情景类中,然后将连入的享元作为单纯的数据对象。
  5. 客户端(Client)负责计算或存储享元的外在状态。在客户端看来,享元是一种可在运行时进行配置的模板对象,具体的配置方式为向其方法中传入一些情景数据参数。
  6. 享元工厂(Flyweight Factory)会对已有享元的缓存池进行管理。有了工厂后,客户端就无需直接创建享元,它们只需调用工厂并向其传递目标享元的一些内在状态即可。工厂会根据参数在之前已创建的享元中进行查找,如果找到满足条件的享元就将其返回;如果没有找到就根据参数新建享元。

适用场景及优缺点

仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式,应用该模式所获的收益大小取决于使用它的方式和情景。它在下列情况中最有效:

  • 程序需要生成数量巨大的相似对象
  • 这将耗尽目标设备的所有内存
  • 对象中包含可抽取且能在多个对象间共享的重复状态。

优点:如果程序中有很多相似对象,那么你将可以节省大量内存。

缺点:

  • 需要牺牲执行速度来换取内存,因为他人每次调用享元方法时都需要重新计算部分情景数据。
  • 代码会变得更加复杂。

实例演示

在本例中,享元模式能有效减少在画布上渲染数百万个树状对象时所需的内存。

Flyweight.h:

#ifndef FLYWEIGHT_H_
#define FLYWEIGHT_H_

#include <string>

// 享元类包含了树类型的部分状态, 这些成员变量保存的数值对于特定树而言是唯一的。
// 很多树木之间包含共同的名字、颜色和纹理, 如果在每棵树中都存储这些数据就会浪费大量内存。
// 因此我们将这些「内在状态」导出到一个单独的对象中, 然后让众多的单个树对象去引用它。
class TreeType {
 public:
    TreeType(std::string n, std::string c, std::string t) :
        name_(n), color_(c), texture_(t) {}
    void draw(std::string canvas, double x, double y) {
        // 1. 创建特定类型、颜色和纹理的位图
        // 2. 在画布坐标(x,y)处绘制位图
        return;
    }

 private:
    std::string name_;
    std::string color_;
    std::string texture_;
};

#endif  // FLYWEIGHT_H_

Context.h:

#ifndef CONTEXT_H_
#define CONTEXT_H_

#include <string>
#include "Flyweight.h"

// 情景对象包含树类型的「外在状态」, 程序中可以创建数十亿个此类对象, 因为它们体积很小: 仅有两个浮点坐标类型和一个引用成员变量
class Tree {
 public:
    Tree(double x, double y, TreeType* t) : x_(x), y_(y), type_(t) {}
    void draw(std::string canvas) {
        return type_->draw(canvas, x_, y_);
    }

 private:
    double x_;
    double y_;
    TreeType* type_;
};

#endif  // CONTEXT_H_

FlyweightFactory.h:

#ifndef FLYWEIGHT_FACTORY_H_
#define FLYWEIGHT_FACTORY_H_

#include <map>
#include <string>
#include <mutex>
#include "Flyweight.h"

// 享元工厂: 决定是否复用已有享元或者创建一个新的对象, 同时它也是一个单例模式
class TreeFactory {
 public:
    static TreeFactory* getInstance() {
        if (instance_ == nullptr) {
            mutex_.lock();
            if (instance_ == nullptr) {
                instance_ = new TreeFactory();
            }
            mutex_.unlock();
        }
        return instance_;
    }
    TreeType* getTreeType(std::string name, std::string color, std::string texture) {
        std::string key = name + "_" + color + "_" + texture;
        auto iter = tree_types_.find(key);
        if (iter == tree_types_.end()) {
            // 新的tree type
            TreeType* new_tree_type = new TreeType(name, color, texture);
            tree_types_[key] = new_tree_type;
            return new_tree_type;
        } else {
            // 已存在的tree type
            return iter->second;
        }
    }

 private:
    TreeFactory() {}
    static TreeFactory* instance_;
    static std::mutex mutex_;

    // 共享池, 其中key格式为name_color_texture
    std::map<std::string, TreeType*> tree_types_;
};

#endif  // FLYWEIGHT_FACTORY_H_

FlyweightFactory.cpp:

#include "FlyweightFactory.h"

TreeFactory* TreeFactory::instance_ = nullptr;
std::mutex TreeFactory::mutex_;

Client.h:

#ifndef CLIENT_H_
#define CLIENT_H_

#include <vector>
#include <iostream>
#include <string>
#include "FlyweightFactory.h"
#include "Context.h"

// Forest包含数量及其庞大的Tree
class Forest {
 public:
    void planTree(double x, double y, std::string name, std::string color, std::string texture) {
        TreeType* type = TreeFactory::getInstance()->getTreeType(name, color, texture);
        Tree tree = Tree(x, y, type);
        trees_.push_back(tree);
    }
    void draw() {
        for (auto tree : trees_) {
            tree.draw("canvas");
        }
    }

 private:
    std::vector<Tree> trees_;
};

#endif  // CLIENT_H_

main.cpp:

#include "Client.h"

int main() {
    Forest* forest = new Forest();

    // 在forest中种植很多棵树
    for (int i = 0; i < 500; i++) {
        for (int j = 0; j < 500; j++) {
            double x = i;
            double y = j;
            // 树类型1: 红色的杉树
            forest->planTree(x, y, "杉树", "红色", "");
            // 树类型2: 绿色的榕树
            forest->planTree(x, y, "榕树", "绿色", "");
            // 树类型3: 白色的桦树
            forest->planTree(x, y, "桦树", "白色", "");
        }
    }

    forest->draw();

    delete forest;
}

代理模式(Proxy)

前言

举个例子:有这样一个消耗大量系统资源的巨型对象, 你只是偶尔需要使用它,并非总是需要。

在这里插入图片描述

你可以实现延迟初始化:在实际有需要时再创建该对象。对象的所有客户端都要执行延迟初始代码。不幸的是,这很可能会带来很多重复代码。 在理想情况下,我们希望将代码直接放入对象的类中,但这并非总是能实现:比如类可能是第三方封闭库的一部分。

代理模式建议新建一个与原服务对象接口相同的代理类,然后更新应用以将代理对象传递给所有原始对象客户端。代理类接收到客户端请求后会创建实际的服务对象,并将所有工作委派给它。

代理将自己伪装成数据库对象,可在客户端或实际数据库对象不知情的情况下处理延迟初始化和缓存查询结果的工作。

在这里插入图片描述

实现结构

在这里插入图片描述

  1. 服务接口(Service Interface)声明了服务接口。代理必须遵循该接口才能伪装成服务对象。
  2. 服务(Service)类提供了一些实用的业务逻辑。
  3. 代理(Proxy)类包含一个指向服务对象的引用成员变量。代理完成其任务(例如延迟初始化、记录日志、访问控制和缓存等)后会将请求传递给服务对象。通常情况下,代理会对其服务对象的整个生命周期进行管理。
  4. 客户端(Client) 能通过同一接口与服务或代理进行交互,所以你可在一切需要服务对象的代码中使用代理。

适用场景及优缺点

  • 延迟初始化(虚拟代理)。如果你有一个偶尔使用的重量级服务对象,一直保持该对象运行会消耗系统资源时,可使用代理模式。

你无需在程序启动时就创建该对象,可将对象的初始化延迟到真正有需要的时候。

  • 访问控制(保护代理)。如果你只希望特定客户端使用服务对象,这里的对象可以是操作系统中非常重要的部分,而客户端则是各种已启动的程序(包括恶意程序),此时可使用代理模式。

代理可仅在客户端凭据满足要求时将请求传递给服务对象。

  • 本地执行远程服务(远程代理)。适用于服务对象位于远程服务器上的情形。

在这种情形中,代理通过网络传递客户端请求,负责处理所有与网络相关的复杂细节。

优点:

  • 你可以在客户端毫无察觉的情况下控制服务对象。
  • 如果客户端对服务对象的生命周期没有特殊要求,你可以对生命周期进行管理。
  • 即使服务对象还未准备好或不存在,代理也可以正常工作。
  • 开闭原则。你可以在不对服务或客户端做出修改的情况下创 建新代理。

缺点:

  • 代码可能会变得复杂,因为需要新建许多类。
  • 服务响应可能会延迟。

实例演示

本例演示如何使用代理模式在第三方视频程序库中添加延迟初始化和缓存。

程序库提供了视频下载类。但是该类的效率非常低。如果客户端程序多次请求同一视频,程序库会反复下载该视频,而不会将首次下载的文件缓存下来复用。

代理类实现和原下载器相同的接口,并将所有工作委派给原下载器。不过,代理类会保存所有的文件下载记录,如果程序多次请求同一文件,它会返回缓存的文件。

ServiceInterface.h:

#ifndef SERVICE_INTERFACE_H_
#define SERVICE_INTERFACE_H_

#include <string>

// 远程服务接口
class ThirdPartyTVLib {
 public:
    virtual std::string listVideos() = 0;
    virtual std::string getVideoInfo(int id) = 0;
};

#endif  // SERVICE_INTERFACE_H_

Service.h:

#ifndef SERVICE_H_
#define SERVICE_H_

#include <string>
#include "ServiceInterface.h"

// 视频下载类
// 该类的方法可以向远程视频后端服务请求信息, 请求速度取决于用户和服务器的网络状况
// 如果同时发送大量请求, 即使所请求的信息一模一样, 程序的速度依然会变慢
class ThirdPartyTVClass : public ThirdPartyTVLib {
 public:
    std::string listVideos() override {
        // 向远程视频后端服务发送一个API请求获取视频信息, 这里忽略实现
        return "video list";
    }

    std::string getVideoInfo(int id) override {
        // 向远程视频后端服务发送一个API请求获取某个视频的元数据, 这里忽略实现
        return "video info";
    }
};

#endif  //  SERVICE_H_

Proxy.h:

#ifndef PROXY_H_
#define PROXY_H_

#include <string>
#include "ServiceInterface.h"

// 为了节省网络带宽, 我们可以将请求缓存下来并保存一段时间
// 当代理类接受到真实请求后才会将其委派给服务对象
class CachedTVClass : public ThirdPartyTVLib {
 public:
    explicit CachedTVClass(ThirdPartyTVLib* service) : service_(service), need_reset_(false), list_cache_(""), video_cache_("") {}
    void reset() {
        need_reset_ = true;
    }

    std::string listVideos() override {
        if (list_cache_ == "" || need_reset_) {
            list_cache_ = service_->listVideos();
        }
        return list_cache_;
    }

    std::string getVideoInfo(int id) override {
        if (video_cache_ == "" || need_reset_) {
            video_cache_ = service_->getVideoInfo(id);
        }
        return video_cache_;
    }

 private:
    ThirdPartyTVLib* service_;
    std::string list_cache_;
    std::string video_cache_;
    bool need_reset_;
};

#endif  // PROXY_H_

Client.h:

#ifndef CLIENT_H_
#define CLIENT_H_

#include <string>
#include <cstdio>
#include "Service.h"

// 之前直接与服务对象交互的 GUI 类不需要改变, 前提是它仅通过接口与服务对象交互。
// 我们可以安全地传递一个代理对象来代替真实服务对象, 因为它们都实现了相同的接口。
class TVManager {
 public:
    explicit TVManager(ThirdPartyTVLib* s) : service_(s) {}
    void renderVideoPage(int id) {
        std::string video_info = service_->getVideoInfo(id);
        // 渲染视频页面, 这里忽略实现
        printf("渲染视频页面: %s\n", video_info.c_str());
        return;
    }
    void renderListPanel() {
        std::string videos = service_->listVideos();
        // 渲染视频缩略图列表, 这里忽略实现
        printf("渲染视频缩略图列表: %s\n", videos.c_str());
        return;
    }

 private:
    ThirdPartyTVLib* service_;
};

#endif  // CLIENT_H_

main.cpp:

#include "Client.h"
#include "Service.h"
#include "Proxy.h"

int main() {
    ThirdPartyTVClass* aTVService = new ThirdPartyTVClass();
    CachedTVClass* aTVProxy = new CachedTVClass(aTVService);
    TVManager* manager = new TVManager(aTVProxy);

    manager->renderVideoPage(1);
    manager->renderListPanel();

    delete aTVService;
    delete aTVProxy;
    delete manager;
}
  • 18
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值