赞
踩
结构型模式(Structural Pattern)描述如何将类或者对象结合在一起形成更大的结构,就像搭积木,可以通过 简单积木的组合形成复杂的、功能更为强大的结构。
结构型模式可以分为类结构型模式和对象结构型模式:
根据“ 合成复用原则 ”,在系统中尽量使用关联关系来替代继 承关系,因此大部分结构型模式都是对象结构型模式。
其包含以下几个模式:
假如你正在开发一款股票市场监测程序,它会从不同来源下载 XML 格式的股票数据,然后向用户呈现出美观的图表。
在开发过程中, 你决定在程序中整合一个第三方智能分析函数库。但是遇到了一个问题, 那就是分析函数库只兼容JSON 格式的数据。
你可以修改程序库来支持XML。但是,这可能需要修改部分依赖该程序库的现有代码。甚至还有更糟糕的情况,你可能根本没有程序库的源代码,从而无法对其进行修改。
你可以创建一个适配器。这是一个特殊的对象,能够转换对象接口,使其能与其他对象进行交互。
适配器模式通过封装对象将复杂的转换过程隐藏于幕后。被封装的对象甚至察觉不到适配器的存在。例如,你可以使用一个将所有数据转换为英制单位(如英尺和英里)的适配器封装运行于米和千米单位制中的对象。
适配器不仅可以转换不同格式的数据,其还有助于采用不同接口的对象之间的合作。它的运作方式如下:
实现时使用了构成原则:适配器实现了其中一个对象的接口,并对另一个对象进行封装。所有流行的编程语言都可以实现适配器。
适配器模式允许你创建一个中间层类,其可作为代码与遗留类、第三方类或提供怪异接口的类之间的转换器。
优点
缺点:代码整体复杂度增加,因为你需要新增一系列接口和类。有时直接更改服务类使其与其他代码兼容会更简单。
考虑经典的“方钉和圆孔”的问题,我们要做的是让方钉适配圆孔。
适配器让方钉假扮成一个圆钉(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
假如你有一个几何形状(Shape)类, 从它能扩展出两个子类: 圆形(Circle)和方形(Square)。你希望对这样的类层次结构进行扩展以使其包含颜色, 所以你打算创建名为红色(Red)和蓝色(Blue)的形状子类。但是,由于你已有两个子类,所以总共需要创建四个类才能覆盖所有组合,例如蓝色圆形(BlueCircle)和红色方形(RedSquare)。
在层次结构中新增形状和颜色将导致代码复杂程度指数增长。例如添加三角形状,你需要新增两个子类,也就是每种颜色一个;此后新增一种新颜色需要新增三个子类,即每种形状一个。如此以往,情况会越来越糟糕。
问题的根本原因是我们试图在两个独立的维度——形状与颜色——上扩展形状类。这在处理类继承时是很常见的问题。
桥接模式通过将继承改为组合的方式来解决这个问题。具体来说,就是抽取其中一个维度并使之成为独立的类层次,这样就可以在初始类中引用这个新层次的对象,从而使得一个类不必拥有所有的状态和行为。
根据该方法,我们可以将颜色相关的代码抽取到拥有红色和蓝色两个子类的颜色类中,然后在形状类中添加一个指向某一颜色对象的引用成员变量。现在,形状类可以将所有与颜色相关的工作委派给连入的颜色对象。这样的引用就成为了形状和颜色之间的桥梁。此后,新增颜色将不再需要修改形状的类层次,反之亦然。
类的代码行数越多,弄清其运作方式就越困难,对其进行修改所花费的时间就越长。一个功能上的变化可能需要在整个类范围内进行修改,而且常常会产生错误,甚至还会有一些严重的副作用。桥接模式可以将庞杂类拆分为几个类层次结构。
桥接建议将每个维度抽取为独立的类层次。初始类将相关工作委派给属于对应类层次的对象,无需自己完成所有工作。
当然并不是说一定要实现这一点,桥接模式可替换抽象部分中的实现对象,具体操作就和给成员变量赋新值一样简单。
优点:
缺点:对高内聚的类使用该模式可能会让代码更加复杂。
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;
}
输出结果
小号铅笔绘制红色的太阳.
例如你有两类对象:产品和盒子。一个盒子中可以包含多个产品或者几个较小的盒子。这些小盒子中同样可以包含一些产品或者更小的盒子。
假设你希望在这些类的基础上开发一个订购系统。订单中可以包含无包装的简单产品,也可以包含装满产品的盒子。此时你会如何计算每张订单的总价格呢?
从上图可以看出,订单中可能包括各种产品,这些产品放置在盒子中,然后又被放入一层又一层更大的盒子总。整个结构看上去像是一棵倒过来的树。
组合模式建议使用一个通用结构来与「产品」和「盒子」进行交互,并且在该接口中声明一个计算总价的方法:
该方法的最大优点在于你无需了解构成树状结构的对象的具体类。你也无需了解对象是简单的产品还是复杂的盒子。你只需要调用通用接口以相同的方式对其进行处理即可。当你调用该方法后,对象会将请求沿着树结构传递下去。
组合模式为你提供了两种共享公共接口的基本元素类型:简单叶节点和复杂容器。容器中可以包含叶节点和其他容器。这使得你可以构建树状嵌套递归对象结构。
组合模式中定义的所有元素共用同一个接口。在这一接口的帮助下,客户端不必在意其所使用的对象的具体类。
优点:
缺点:对于功能差异较大的类,提供公共接口或许会有困难。在特定情况下,你需要过度一般化组件接口,使其变得令人难以理解。
本例将借助组合模式帮助你在图形编辑器中实现一系列的几何图形。
组合图形(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)处绘制点
假设你正在开发一个提供通知功能的库,其他程序可使用它向用户发送关于重要事件的通知。
库的最初版本基于通知器Notifier 类,其中只有很少的几个成员变量,一个构造函数和一个send 发送方法。该方法可以接收来自客户端的消息参数,并将该消息发送给一系列的邮箱,邮箱列表则是通过构造函数传递给通知器的。作为客户端的第三方程序仅会创建和配置通知器对象一次,然后在有重要事件发生时对其进行调用。
此后某个时刻,你会发现库的用户希望使用除邮件通知之外的功能。许多用户会希望接收关于紧急事件的手机短信,还有些用户希望在微信上接收消息, 而公司用户则希望在QQ上接收消息。
但是很快有人会问:“为什么不同时使用多种通知形式呢?如果房子着火了,你大概会想在所有渠道中都收到相同的消息吧。”
你可以尝试创建一个特殊子类来将多种通知方法组合在一起以解决该问题。但这种方式会使得代码量迅速膨胀,不仅仅是程序库代码,客户端代码也会如此。
你必须找到其他方法来规划通知类的结构,否则它们的数量会极其庞大。但是你不能使用继承,因为可能引发的几个严重问题:
装饰模式提供了一种灵活的方式来扩展对象的功能,而无需通过子类化来实现。在装饰模式中,创建一个装饰器类,该类包含一个指向被装饰对象的引用,并实现与被装饰对象相同的接口。然后,通过创建不同的装饰器类来为对象添加新的行为或责任。
比如在消息通知示例中,我们可以将简单邮件通知行为放在基类通知器中,但将所有其他通知方法放入装饰中。
装饰能将业务逻辑组织为层次结构,你可为各层创建一个装饰,在运行时将各种不同逻辑组合成对象。由于这些对象都遵循通用接口,客户端代码能以相同的方式使用这些对象。
许多编程语言使用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))
假设你必须在代码中使用某个复杂的库或框架中的众多对象。正常情况下,你需要负责所有对象的初始化工作、管理其依赖关系并按正确的顺序执行方法等。
最终,程序中类的业务逻辑将与第三方类的实现细节紧密耦合,使得理解和维护代码的工作很难进行。
外观类为包含许多活动部件的复杂子系统提供一个简单的接口。与直接调用子系统相比,外观提供的功能可能比较有限,但它却包含了客户端真正关心的功能。
例如, 上传猫咪搞笑短视频到社交媒体网站的应用可能会用到专业的视频转换库, 但它只需使用一个包含encode(filename, format) 方法(以文件名与文件格式为参数进行编码的方法)的类即可。在创建这个类并将其连接到视频转换库后,你就拥有了自己的第一个外观。
子系统通常会随着时间的推进变得越来越复杂。即便是应用了设计模式,通常你也会创建更多的类。尽管在多种情形中子系统可能是更灵活或易于复用的,但其所需的配置和样板代码数量将会增长得更快。为了解决这个问题,外观将会提供指向子系统中最常用功能的快捷方式,能够满足客户端的大部分需求。
创建外观来定义子系统中各层次的入口。你可以要求子系统仅使用外观来进行交互,以减少子系统之间的耦合。
优点:你可以让自己的代码独立于复杂子系统。
缺点:外观可能成为与程序中所有类都耦合的上帝对象。
计算机本身是一个及其复杂的系统,我们通过外观模式屏蔽电脑开机这一动作背后复杂子系统的运作。
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成功!
读取硬盘中...
读取硬盘成功!
载入操作系统中...
载入操作系统成功!
开机成功!
假如你希望在长时间工作后放松一下,所以开发了一款简单的游戏:玩家们在地图上移动并相互射击。你决定实现一个真实的粒子系统,并将其作为游戏的特色。大量的子弹、导弹和爆炸弹片会在整个地图上穿行,为玩家提供紧张刺激的游戏体验。
开发完成后,你推送提交了最新版本的程序,并在编译游戏后将其发送给了一个朋友进行测试。尽管该游戏在你的电脑上完美运行,但是你的朋友却无法长时间进行游戏:游戏总是会在他的电脑上运行几分钟后崩溃。在研究了几个小时的调试消息记录后,你发现导致游戏崩溃的原因是内存容量不足。朋友的设备性能远比不上你的电脑,因此游戏运行在他的电脑上时很快就会出现问题。
真正的问题与粒子系统有关。每个粒子(一颗子弹、一枚导弹或一块弹片)都由包含完整数据的独立对象来表示。当玩家在游戏中鏖战进入高潮后的某一时刻,游戏将无法在剩余内存中载入新建粒子,于是程序就崩溃了。
仔细观察粒子(Particle)类, 你可能会注意到颜色(color)和精灵图(sprite)这两个成员变量所消耗的内存要比其他变量多得多。更糟糕的是,对于所有的粒子来说,这两个成员变量所存储的数据几乎完全一样(比如所有子弹的颜色和精灵图都一样)。
每个粒子的另一些状态(坐标、移动矢量和速度)则是不同的。因为这些成员变量的数值会不断变化。这些数据代表粒子在存续期间不断变化的情景,但每个粒子的颜色和精灵图则会保持不变。
对象的常量数据通常被称为内在状态,其位于对象中,其他对象只能读取但不能修改其数值。而对象的其他状态常常能被其他对象“从外部”改变,因此被称为外在状态。
享元模式建议不在对象中存储外在状态,而是将其传递给依赖于它的一个特殊方法。程序只在对象中保存内在状态,以方便在不同情景下重用。这些对象的区别仅在于其内在状态(与外在状态相比,内在状态的变体要少很多),因此你所需的对象数量会大大削减。
假如能从粒子类中抽出外在状态,那么我们只需三个不同的对象(子弹、导弹和弹片)就能表示游戏中的所有粒子。你现在很可能已经猜到了,我们将这样一个仅存储内在状态的对象称为享元。
仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式,应用该模式所获的收益大小取决于使用它的方式和情景。它在下列情况中最有效:
优点:如果程序中有很多相似对象,那么你将可以节省大量内存。
缺点:
在本例中,享元模式能有效减少在画布上渲染数百万个树状对象时所需的内存。
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; }
举个例子:有这样一个消耗大量系统资源的巨型对象, 你只是偶尔需要使用它,并非总是需要。
你可以实现延迟初始化:在实际有需要时再创建该对象。对象的所有客户端都要执行延迟初始代码。不幸的是,这很可能会带来很多重复代码。 在理想情况下,我们希望将代码直接放入对象的类中,但这并非总是能实现:比如类可能是第三方封闭库的一部分。
代理模式建议新建一个与原服务对象接口相同的代理类,然后更新应用以将代理对象传递给所有原始对象客户端。代理类接收到客户端请求后会创建实际的服务对象,并将所有工作委派给它。
代理将自己伪装成数据库对象,可在客户端或实际数据库对象不知情的情况下处理延迟初始化和缓存查询结果的工作。
你无需在程序启动时就创建该对象,可将对象的初始化延迟到真正有需要的时候。
代理可仅在客户端凭据满足要求时将请求传递给服务对象。
在这种情形中,代理通过网络传递客户端请求,负责处理所有与网络相关的复杂细节。
优点:
缺点:
本例演示如何使用代理模式在第三方视频程序库中添加延迟初始化和缓存。
程序库提供了视频下载类。但是该类的效率非常低。如果客户端程序多次请求同一视频,程序库会反复下载该视频,而不会将首次下载的文件缓存下来复用。
代理类实现和原下载器相同的接口,并将所有工作委派给原下载器。不过,代理类会保存所有的文件下载记录,如果程序多次请求同一文件,它会返回缓存的文件。
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; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。