架构之路_七种结构型设计模式

一、适配器模式

你一定听过“网络适配器”吧?又叫网卡。它的作用是什么呢?——上网!

这样的回答显然不够专业,正确的答案是“网卡的一个重要功能就是要进行串行/并行转换。因为网卡和局域网之间的通信是通过电缆或双绞线以串行传输方式进行,而网卡和计算机之间通信则是通过计算机主板上的I/O总线以并行传输方式进行。”

你肯定要问:“这和我有什么关系?”

当然有了,因为你正在学习设计模式,而这就跟本文即将要介绍的适配器模式有关啦!

1.1 适配器模式简介

除了网卡适配器,你一定还听说过电源适配器吧?我国生活用电电压是220V,但我们的电脑、手机、平板、剃须刀(充电式)不会使用这么高的电压。这就需要电源适配器(充电器、变压器),使各个电子设备和220的供电电压兼容。电源适配器就充当了一个适配器的角色。

在软件系统设计中,当需要组合使用的类不兼容时,也需要类似于变压器一样的适配器来协调这些不兼容者,这就是适配器模式!

那么什么是适配器模式呢?

适配器模式:

将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。

与电源适配器类似,适配器模式中会设计一个叫做“适配器”的包装类,适配器包装的对象叫做适配者。根据定义,适配器是根据客户的需求,将适配者已有的接口转换成另一个接口,得以使不兼容的类可以协同工作。

1.2 适配器模式结构

适配器模式分为类适配器和对象适配器。

  • 适配器类(Adapter):适配器与适配者之间是继承或实现关系;
  • 适配者类(Adaptee):适配器与适配者之间是关联关系。
  • 目标抽象类(Target):定义客户所需要的接口。

类适配器和对象适配器的UML图如下。类适配器中,适配器类通过继承适配者类,并重新实现适配者的具体接口来达到适配客户所需要的接口的目的。对象适配器中,适配器类通过在类中实例化一个适配者类的对象,并将其封装在客户所需功能的接口里,达到最终的适配目的。
在这里插入图片描述

1.3 适配器模式代码实例

Jungle曾经在一个项目里多次使用了适配器模式。这里举个使用对象适配器模式的例子。

路径规划包括两个阶段:首先读取并解析工程图文件,得到其中的点、直线坐标;其次根据需求计算加工路径。软件控制器(Controller)上,系统点击“路径规划”按钮就自动完成上述过程。

Jungle已经封装好一个类DxfParser,该类可以读取后缀名为dxf的工程图文件,并解析其中的点、线,保存到路径列表里。另一个类PathPlanner用于计算加工路径。

这个例子中,Controller就是目标抽象类,DxfParser和PathPlanner是适配者类,这两个类提供的方法可以用于实现路径规划的需求。我们只需再定义一个适配器类Adapter即可。
在这里插入图片描述

1.3.1 目标抽象类

//目标抽象类
class Controller
{
public:
	Controller(){}
	virtual void pathPlanning() = 0;
private:
};

1.3.2 适配者类

//适配者类DxfParser
class DxfParser
{
public:
	DxfParser(){}
	void parseFile(){
		printf("解析文件提取数据\n");
	}
};
 
//适配者类PathPlanner
class PathPlanner
{
public:
	PathPlanner(){}
	void calculate(){
		printf("计算加工路径\n");
	}
};

1.3.3 适配器类

//适配器类Adapter
class Adapter:public Controller
{
public:
	Adapter(){
		dxfParser = new DxfParser();
		pathPlanner = new PathPlanner();
	}
	void pathPlanning(){
		printf("路径规划:\n");
		dxfParser->parseFile();
		pathPlanner->calculate();
	}
private:
	DxfParser   *dxfParser;
	PathPlanner *pathPlanner;
};

1.3.4 客户端代码示例

#include <iostream>
#include "AdapterPattern.h"
 
int main()
{
	Controller *controller = new Adapter();
	controller->pathPlanning();
 
	system("pause");
	return 0;
}

1.3.5 运行结果

在这里插入图片描述

1.4 适配器模式总结

优点:

  • 将目标类和适配者类解耦,引入一个适配器类实现代码重用,无需修改原有结构;

  • 增加类的透明和复用,对于客户端而言,适配者类是透明的;
    对象适配器可以把不同适配者适配到同一个目标(对象适配器);

缺点:

  • 对编程语言的限制:Java不支持多重继承,一次最多只能适配一个适配者类,不能同时适配多个适配者类;

适用环境:

  • 系统需要使用一些现有的类,但这些类的接口不符合系统需要,或者没有这些类的源代码;
  • 想创建一个重复使用的类,用于和一些彼此没有太大关联的类一起工作。

二、桥接模式

Jungle有两个手机,分别是M手机和N手机,M手机上有游戏Game1,N手机上有Game2。每次Jungle想玩Game1时,就使用M手机,想玩Game2时,就玩N手机。要是某天Jungle外出,心情大好,两个游戏都想玩,那Jungle还得带上两个手机???麻不麻烦?

如果新出一个游戏Game3,那Jungle是不是要再买一个手机呢?

同样都是游戏软件,为什么不把所有游戏都装到一个手机上呢?

在这里插入图片描述

2.1 桥接模式简介

如果系统中的某个类存在两个独立变化的维度,通过桥接模式可以将这两个维度分离开来,使两者独立扩展。如同上述实例,Jungle想用手机玩游戏,手机和游戏是两个独立变化的维度,增加一个游戏对手机没有影响,增加一个手机对游戏也没有影响。手机上可以安装游戏,而游戏必须在手机上玩,从这个角度而言,手机和游戏之间存在较强的耦合。

但两者可以很好的解耦,且解耦后扩展灵活:所有游戏安装在一个手机上,新出一个游戏,新安装就ok!买了新手机,同样可以装上所有游戏。这就是桥接模式:

桥接模式:

将抽象部分与它的实现部分解耦,使得两者都能够独立变化。

桥接模式将两个独立变化的维度设计成两个独立的继承等级结构(而不会将两者耦合在一起形成多层继承结构),在抽象层将二者建立起一个抽象关联,该关联关系类似一座桥,将两个独立的等级结构连接起来,故曰“桥接模式”。

2.2 桥接模式结构

桥接模式UML图如下图。由图可知,桥接模式包含以下角色:

在这里插入图片描述

  • Abstraction(抽象类):定义抽象类的接口(抽象接口),由聚合关系可知,抽象类中包含一个Implementor类型的对象,它与Implementor之间有关联关系,既可以包含抽象业务方法,也可以包含具体业务方法;

  • Implementor(实现类接口):定义实现类的接口,这个接口可以与Abstraction类的接口不同。一般而言,实现类接口只定义基本操作,而抽象类的接口还可能会做更多复杂的操作。

  • RefinedAbstraction(扩充抽象类):具体类,实现在抽象类中定义的接口,可以调用在Implementor中定义的方法;

  • ConcreteImplementor(具体实现类):具体实现了Implementor接口,在不同的具体实现类中实现不同的具体操作。运行时ConcreteImplementor将替换父类。

简言之,在Abstraction类中维护一个Implementor类指针,需要采用不同的实现方式的时候只需要传入不同的Implementor派生类就可以了。

2.3 桥接模式代码实例

以引言中的故事为例,Jungle学习了桥接模式后大受启发,想实现如下操作:

新手机上能够迅速在新手机上安装(setup)并玩(play)游戏

新增加一个游戏时Jungle能够在已有手机上安装并play

在这个实例里,手机是抽象类Abstraction,具有玩游戏这样的实现类接口Implementor,不同的手机品牌扩充抽象类RefinedAbstraction,多个不同的游戏则是具体实现类ConcreteImplementor。

2.3.1 实现类

//实现类接口
class Game
{
public:
	Game(){}
	virtual void play() = 0;
private:
};
 
//具体实现类GameA
class GameA:public Game
{
public:
	GameA(){}
	void play(){
		printf("Jungle玩游戏A\n");
	}
};
 
//具体实现类GameB
class GameB :public Game
{
public:
	GameB(){}
	void play(){
		printf("Jungle玩游戏B\n");
	}
};

实现类Game中声明了play的接口,不过它是一个虚方法,其实现在具体实现类GameA和GameB中定义。

2.3.2 抽象类和扩充抽象类

//抽象类Phone
class Phone
{
public:
	Phone(){
	}
	//安装游戏
	virtual void setupGame(Game *igame) = 0;
	virtual void play() = 0;
private:
	Game *game;
};
 
//扩充抽象类PhoneA
class PhoneA:public Phone 
{
public:
	PhoneA(){
	}
	//安装游戏
	void setupGame(Game *igame){
		this->game = igame;
	}
	void play(){
		this->game->play();
	}
private:
	Game *game;
};
 
//扩充抽象类PhoneB
class PhoneB :public Phone
{
public:
	PhoneB(){
	}
	//安装游戏
	void setupGame(Game *igame){
		this->game = igame;
	}
	void play(){
		this->game->play();
	}
private:
	Game *game;
};

抽象类Phone中也声明了两个虚方法,并且定义了一个实现类的对象,使抽象和实现具有关联关系。而对象的实例化则放在客户端使用时进行。

2.3.3 客户端代码示例

#include <iostream>
#include "BridgePattern.h"
 
int main()
{
	Game *game;
	Phone *phone;
 
	//Jungle买了PhoneA品牌的手机,想玩游戏A
	phone = new PhoneA();
	game = new GameA();
	phone->setupGame(game);
	phone->play();
	printf("++++++++++++++++++++++++++++++++++\n");
 
	//Jungle想在这个手机上玩游戏B
	game = new GameB();
	phone->setupGame(game);
	phone->play();
 
	system("pause");
	return 0;
}

2.3.4 运行结果

在这里插入图片描述

2.4 桥接模式总结

优点:

  • 分离抽象接口与实现部分,使用对象间的关联关系使抽象与实现解耦;

  • 桥接模式可以取代多层继承关系,多层继承违背单一职责原则,不利于代码复用;

  • 桥接模式提高了系统可扩展性,某个维度需要扩展只需增加实现类接口或者具体实现类,而且不影响另一个维度,符合开闭原则。

缺点:

  • 桥接模式难以理解,因为关联关系建立在抽象层,需要一开始就设计抽象层;
  • 如何准确识别系统中的两个维度是应用桥接模式的难点。

适用场景:

  • 如果一个系统需要在抽象化和具体化之间增加灵活性,避免在两个层次之间增加继承关系,可以使用桥接模式在抽象层建立关联关系;
  • 抽象部分和实现部分可以各自扩展而互不影响;
    一个类存在多个独立变化的维度,可采用桥接模式。

三、组合模式

今天Jungle又是被压榨的一天:

整个公司的组织结构就像是一个树形结构,公司分成几个区,每个区分成几个产品部门,一个产品部门又会分成几个组,组里可能还会有细分。

看到了吗?这就是组合模式的典型应用场景!

3.1 组合模式简介

组合模式关注包含叶子节点和容器节点的结构以及他们构成的组织形式。这样的组织形式的特点在于:叶子节点不再包含成员对象,而容器节点可以包含成员对象,这些对象可以是叶子节点,也可以是容器节点。这些节点通过不同的递归组合形成一个树形结构。好比Windows系统的目录结构,文件夹里包含文件和子文件夹,子文件夹里还可以包含文件和文件夹。
在这里插入图片描述
组合模式为叶子节点和容器节点提供了公共的抽象构建类,客户端无需关心所操作的对象是叶子节点还是容器节点,只需针对抽象构建类编程处理即可。

组合模式定义:

组合多个对象形成树形结构以表示具有部分-整体关系的层次结构。组合模式让客户端可以统一对待单个对象和组合对象。

3.2 组合模式结构

组合模式结构图如下图:

在这里插入图片描述

结合引言及组合模式UML图可知组合模式有如下角色:

  • Component(抽象构件):Component是一个抽象类,定义了构件的一些公共接口,这些接口是管理或者访问它的子构件的方法(如果有子构件),具体的实现在叶子构件和容器构件中进行。

  • Leaf(叶子构件):它代表树形结构中的叶子节点对象,叶子构件没有子节点,它实现了在抽象构件中定义的行为。对于抽象构件定义的管理子构件的方法,叶子构件可以通过抛出异常、提示错误等方式进行处理。

  • Composite(容器构件) :容器构件一方面具体实现公共接口,另一方面通过聚合关系包含子构件,子构件可以是容器构件,也可以是叶子构件。
    结合公司组织架构的例子,各个分级部门是容器构件,类似于Jungle的员工是叶子构件;结合Windows目录结构,文件夹是容器构件,可以包含子文件夹和文件,而文件则是叶子构件,不能再包含子构件。

3.2.1 透明组合模式

如UML图所示,组合模式分为透明组合模式和安全组合模式。在透明组合模式中,抽象构件Component声明了add、remove、getChild等所有管理和访问子构件的方法,不论是叶子构件还容器构件都具有相同的接口。客户端在使用时可以一致地对待所有对象,即具体是叶子构件还是容器构件,对客户端而言是透明的,因为它们都暴露相同的接口。

但是,叶子构件是没有子构件的,所有就没有add、remove和getChild方法的,所以必须在叶子构件的实现中提供相应的错误处理代码,否则代码会出错。

3.2.2 安全组合模式

在安全组合模式中,抽象构件Component没有声明任何管理和访问子构件的方法,在具体的实现类中才去定义这些行为。之所以叫“安全”,是因为叶子构件没有子构件,也就不必定义管理访问子构件的方法,对客户端而言,当它处理子构件时,不可能调用到类似透明组合模式中的子构件的add、remove等方法,因此也就不会出错。

安全模式的不足在于不够透明,客户端使用时必须有区别的对待叶子构件和容器构件。

3.3 组合模式代码实例

如下图,某个公司的组织结构分为总部、省级分部、市级分部和各个分部的行政办公室和教务办公室:
在这里插入图片描述
这是一个典型的树形结构,本例将采用透明这模式来实现上述结构。

3.3.1 抽象构件

//抽象构件
class Component
{
public:
	Component(){}
	Component(string iName){
		this->name = iName;
	}
	//增加一个部门或办公室
	virtual void add(Component*) = 0;
	//撤销一个部门或办公室
	virtual void remove(Component*) = 0;
	//
	virtual Component* getChild(int) = 0;
	//各部门操作
	virtual void operation() = 0;
	string getName(){
		return name;
	}
private:
	string name;
};

3.3.2 叶子构件

叶子构件定义了行政办公室和教务办公室,都集成自Office。

//叶子构件:办公室
class Office :public Component
{
public:
	Office(string iName){
		this->name = iName;
	}
	Office(){}
	void add(Component* c){
		printf("not support!\n");
	}
	void remove(Component* c){
		printf("not support!\n");
	}
	Component* getChild(int i){
		printf("not support!\n");
		return NULL;
	}
private:
	string name;
};
 
//叶子构件:行政办公室
class AdminOffice :public Office
{
public:
	AdminOffice(string iName){
		this->name = iName;
	}
	void operation(){
		printf("-----Administration Office:%s\n", name.c_str());
	}
private:
	string name;
};
 
//叶子构件:教务办公室
class DeanOffice :public Office
{
public:
	DeanOffice(string iName){
		this->name = iName;
	}
	void operation(){
		printf("-----Dean Office:%s\n", name.c_str());
	}
private:
	string name;
};

3.3.3 容器构件

//容器构件SubComponent
class SubComponent :public Component
{
public:
	SubComponent(string iName){
		this->name = iName;
	}
	void add(Component *c){
		componentList.push_back(c);
	}
	void remove(Component *c){
		for (int i = 0; i < componentList.size(); i++){
			if (componentList[i]->getName() == c->getName()){
				componentList.erase(componentList.begin() + i);
				break;
			}
		}
	}
	Component* getChild(int i){
		return (Component*)componentList[i];
	}
	void operation(){
		printf("%s\n", this->name.c_str());
		for (int i = 0; i < componentList.size(); i++){
			((Component*)componentList[i])->operation();
		}
	}
private:
	string name;
 
	//构件列表
	vector<Component*>componentList;
};

3.3.4 客户端代码示例

#include <iostream>
#include "CompositePattern.h"
 
int main()
{
	Component *head, *sichuanBranch, *cdBranch, *myBranch, *office1, *office2, *office3,
		*office4, *office5, *office6, *office7, *office8;
 
	head = new SubComponent("总部");
	sichuanBranch = new SubComponent("四川分部");
	office1 = new AdminOffice("行政办公室");
	office2 = new DeanOffice("教务办公室");
	
	cdBranch = new SubComponent("成都分部");
	myBranch = new SubComponent("绵阳分部");
	office3 = new AdminOffice("行政办公室");
	office4 = new DeanOffice("教务办公室");
	
	office5 = new AdminOffice("行政办公室");
	office6 = new DeanOffice("教务办公室");
	
	office7 = new AdminOffice("行政办公室");
	office8 = new DeanOffice("教务办公室");
	
	cdBranch->add(office5);
	cdBranch->add(office6);
 
	myBranch->add(office7);
	myBranch->add(office8);
 
	sichuanBranch->add(office3);
	sichuanBranch->add(office4);
	sichuanBranch->add(cdBranch);
	sichuanBranch->add(myBranch);
 
	head->add(office1);
	head->add(office2);
	head->add(sichuanBranch);
	
	head->operation();
 
	system("pause");
	return 0;
}

3.3.5 运行结果

在这里插入图片描述

3.4 组合模式总结

优点:

  • 清楚地定义分层次的复杂对象,表示出复杂对象的层次结构,让客户端忽略层次的差异;

  • 客户端可以一致地使用层次结构中各个层次的对象,而不必关心其具体构件的行为如何实现;

  • 在组合模式中增加新的叶子构件和容器构件非常方便,易于扩展,符合开闭原则;

  • 为树形结构的案例提供了解决方案。

缺点:

  • 子构件或容器构件的行为受限制,因为它们来自相同的抽象层。如果要定义某个容器或者某个叶子节点特有的方法,那么要求在运行时判断对象类型,增加了代码的复杂度。

适用场景:

  • 系统中需要用到树形结构;
  • 系统中能够分离出容器节点和叶子节点;
  • 具有整体和部门的层次结构中,能够通过某种方式忽略层次差异,使得客户端可以一致对待。

四、装饰模式

在软件设计中,对已有对象的功能进行扩展,以获得更加符合用户需求的对象,使得对象具有更加强大的功能,这就是装饰模式。

4.1 装饰模式简介

装饰模式可以在不改变一个对象本身功能的基础上给对象增加额外的新行为,比如手机,为防止摔坏,可以给手机贴膜或者带上保护套;为美观,可以在保护套上贴卡通贴纸;为便于携带可以增加挂绳,如下图。但这并不改变手机的本质。
在这里插入图片描述

装饰模式:

动态地给一个对象增加一些额外的职责。就扩展功能而言,装饰模式提供了一种比使用子类更加灵活的替代方案。 

装饰模式是一种用于替代继承的技术。通过一种无须定义子类的方式给对象动态增加职责,使用对象之间的关联关系取代类之间的继承关系。装饰模式中引入了装饰类,在装饰类中既可以调用待装饰的原有对象的方法,还可以增加新的方法,以扩充原有类的功能。

4.2 装饰模式结构

装饰模式的结构如图所示:

在这里插入图片描述

装饰模式中有如下角色:

  • Component(抽象构件):是具体构件类和装饰类的共同基类,声明了在具体构件中定义的方法,客户端可以一致的对待使用装饰前后的对象;

  • ConcreteComponent(具体构件):具体构件定义了构件具体的方法,装饰类可以给它增加更多的功能;

  • Decorator(抽象装饰类):用于给具体构件增加职责,但具体职责在其子类中实现。抽象装饰类通过聚合关系定义一个抽象构件的对象,通过该对象可以调用装饰之前构件的方法,并通过其子类扩展该方法,达到装饰的目的;

  • ConcreteDecorator(具体装饰类): 向构件增加新的功能。

以上面提到的手机为例,手机就是一个具体构件,而手机壳、手机贴纸和手机挂绳,都是具体的装饰类。以Jungle在冷天出门前精心打扮为例,Jungle本人是一个具体构件对象,各类衣裤围巾手套都是具体的装饰类对象。

4.3 装饰模式代码实例

本节以给手机带上手机壳、贴上手机贴纸、系上手机挂绳为例,展示装饰模式的代码。该例的UML图如下所示:
在这里插入图片描述

4.3.1 抽象构件类

//抽象构件
class Component
{
public:
	Component(){}
	virtual void operation() = 0;
};

4.3.2 具体构件类

//具体构件类
class Phone :public Component
{
public:
	Phone(){}
	void operation(){
		printf("手机\n");
	}
};

4.3.3 装饰类

抽象装饰类

//抽象装饰类
class Decorator :public Component
{
public:
	Decorator(){}
	Decorator(Component *c){
		this->component = c;
	}
	void operation(){
		this->component->operation();
	}
	Component *getComponent(){
		return this->component;
	}
	void setComponent(Component *c){
		this->component = c;
	}
private:
	Component *component;
};

抽象装饰类中有一个成员对象component,以及setter和getter方法。
具体装饰类
具体装饰类一共有三个,分别是手机壳装饰类DecoratorShell、贴纸装饰类DecoratorSticker和挂绳装饰类DecoratorRope。每一个具体装饰类都增加了各自新的职责newBehavior。

//具体装饰类:手机壳
class DecoratorShell:public Decorator
{
public:
	DecoratorShell(){}
	DecoratorShell(Component *c){
		this->setComponent(c);
	}
	void operation(){
		this->getComponent()->operation();
		this->newBehavior();
	}
	void newBehavior(){
		printf("装手机壳\n");
	}
};
 
//具体装饰类:手机贴纸
class DecoratorSticker :public Decorator
{
public:
	DecoratorSticker(){}
	DecoratorSticker(Component *c){
		this->setComponent(c);
	}
	void operation(){
		this->getComponent()->operation();
		this->newBehavior();
	}
	void newBehavior(){
		printf("贴卡通贴纸\n");
	}
};
 
//具体装饰类:手机挂绳
class DecoratorRope :public Decorator
{
public:
	DecoratorRope(){}
	DecoratorRope(Component *c){
		this->setComponent(c);
	}
	void operation(){
		this->getComponent()->operation();
		this->newBehavior();
	}
	void newBehavior(){
		printf("系手机挂绳\n");
	}
};

4.3.4 客户端代码示例

客户端展示了三段代码,分别为三个手机配上不同的装饰:

#include <iostream>
#include "DecoratorPattern.h"
 
int main()
{
	printf("\nJungle的第一个手机:\n");
	Component *c;
	Component *com_Shell;
	c = new Phone();
	com_Shell = new DecoratorShell(c);
	com_Shell->operation();
 
	printf("\nJungle的第二个手机:\n");
	Component *c2;
	Component *com_Shell2;
	c2 = new Phone();
	com_Shell2 = new DecoratorShell(c2);
	Component *com_Sticker;
	com_Sticker = new DecoratorSticker(com_Shell2);
	com_Sticker->operation();
 
	printf("\nJungle的第三个手机:\n");
	Component *c3;
	Component *com_Shell3;
	c3 = new Phone();
	com_Shell3 = new DecoratorShell(c3);
	Component *com_Sticker2;
	com_Sticker2 = new DecoratorSticker(com_Shell3);
	Component *com_Rope;
	com_Rope = new DecoratorRope(com_Sticker2);
	com_Rope->operation();
 
	printf("\n\n");
 
	system("pause");
	return 0;
}

4.3.5 运行结果

在这里插入图片描述

4.4 总结

优点:

  • 对于扩展一个类的新功能,装饰模式比继承更加灵活;

  • 动态扩展一个对象的功能;

  • 可以对一个对象进行多次装饰(如上述例子第二个手机和第三个手机);

  • 具体构件类和具体装饰类可以独立变化和扩展,符合开闭原则。

缺点:

  • 装饰模式中会增加很多小的对象,对象的区别主要在于各种装饰的连接方式不同,而并不是职责不同,大量小对象的产生会占用较多的系统资源;
  • 装饰模式比继承模式更灵活,但也更容易出错,更难于排错。

适用场景:

  • 在不影响其他对象的情况下,给单个对象动态扩展职责;
  • 不适宜采用继承的方式进行扩展的时候,可以考虑使用装饰模式。

五、外观模式

在这里插入图片描述
在这个例子中,厨师整合了一系列复杂的过程,外界(Jungle)只需与厨师交互即可。在软件设计模式中,有一类设计模式正式如此——外观模式。

5.1 外观模式简介

外观模式是一种使用频率较高的设计模式,它提供一个外观角色封装多个复杂的子系统,简化客户端与子系统之间的交互,方便客户端使用。外观模式可以降低系统的耦合度。如果没有外观类,不同的客户端在需要和多个不同的子系统交互,系统中将存在复杂的引用关系,如下图。引入了外观类,原有的复杂的引用关系都由外观类实现,不同的客户端只需要与外观类交互。

在这里插入图片描述

外观模式:

为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

外观模式的应用很多,比如浏览器,用户要查找什么东西,不论是浏览知乎、腾讯或者CSDN,用户都只需要打开浏览器即可,剩下的搜索工作由浏览器完成。

5.2 外观模式结构

外观模式的UML结构图如下所示:

在这里插入图片描述

外观模式一共有以下角色:

  • Facade(外观角色):外观角色可以知道多个相关子系统的功能,它将所有从客户端发来的请求委派给相应的子系统,传递给相应的子系统处理。
  • SubSystem(子系统角色):子系统是一个类,或者由多个类组成的类的集合,它实现子系统具体的功能。

5.3 外观模式代码实例

电脑主机(Mainframe)中只需要按下主机的开机按钮(powerOn),即可调用其他硬件设备和软件的启动方法,如内存(Memory)的自检(selfCheck)、CPU的运行(run)、硬盘(HardDisk)的读取(read)、操作系统(OS)的载入(load)等。如果某一过程发生错误则电脑开机失败。

这里Jungle用外观模式来模拟该过程,该例子UML图如下:
在这里插入图片描述

5.3.1 子系统类

本例中一共有4个子系统,因此设计4个类:Memory、CPU、HardDisk和OS,并且每个子系统都有自己独立的流程。

//子系统:内存
class Memory
{
public:
	Memory(){}
	void selfCheck(){
		printf("…………内存自检……\n");
	}
};
 
//子系统:CPU
class CPU
{
public:
	CPU(){}
	void run(){
		printf("…………运行CPU运行……\n");
	}
};
 
//子系统:硬盘
class HardDisk
{
public:
	HardDisk(){}
	void read(){
		printf("…………读取硬盘……\n");
	}
};
 
//子系统:操作系统
class OS
{
public:
	OS(){}
	void load(){
		printf("…………载入操作系统……\n");
	}
};

5.3.2 外观类设计

//外观类
class Facade
{
public:
	Facade(){
		memory = new Memory();
		cpu = new CPU();
		hardDisk = new HardDisk();
		os = new OS();
	}
	void powerOn(){
		printf("正在开机……\n");
		memory->selfCheck();
		cpu->run();
		hardDisk->read();
		os->load();
		printf("开机完成!\n");
	}
private:
	Memory *memory;
	CPU *cpu;
	HardDisk *hardDisk;
	OS *os;
};

5.3.3 客户端代码示例

#include <iostream>
#include "FacadePattern.h"
 
int main()
{
	Facade *facade = new Facade();
	facade->powerOn();
 
	printf("\n\n");
 
	system("pause");
	return 0;
}

看到了吗,客户端的代码就是如此简单,跟子系统无关!

5.3.5 运行结果

在这里插入图片描述

5.4 总结

优点:

  • 外观模式使得客户端不必关心子系统组件,减少了与客户端交互的对象的数量,简化了客户端的编程;

  • 外观模式可以大大降低系统的耦合度;

  • 子系统的变化并不需要修改客户端,只需要适当修改外观类即可;

  • 子系统之间不会相互影响。

缺点:

  • 如果需要增加或者减少子系统,需要修改外观类,违反开闭原则;
  • 并不能限制客户端直接与子系统交互,但如果加强限制,又使得系统灵活度降低。

适用场景:

  • 为访问一系列复杂的子系统提供一个统一的、简单的入口,可以使用外观模式;
  • 客户端与多个子系统之间存在很大依赖,但在客户端编程,又会增加系统耦合度,且使客户端编程复杂,可以使用外观模式。

六、享元模式

类似的,你想输入一段英文段落,无论每个单词再长再复杂,也无非都是由26个字母中的几个组成的。上述两个示例的共同点在于,整个环境中存在大量相同或者相似的、需要重复使用的对象。针对这样的场景,面向对象设计中有一类值得借鉴的设计模式是不错的解决方案——享元模式。

6.1 享元模式简介

如果一个系统在运行时创建太多相同或者相似的对象,会占用大量内存和资源,降低系统性能。享元模式通过共享技术实现相同或相似的细粒度对象的复用,提供一个享元池存储已经创建好的对象,并通过享元工厂类将享元对象提供给客户端使用。

享元模式:
运用共享技术有效地支持大量细粒度对象的复用。

享元模式要求被共享的对象必须是细粒度对象。 如上面提到的输入英文段落的例子,26个字母可能随时被客户重复使用。尽管每个字母可能出现的位置不一样,但在物理上它们共享同一个对象(同一个实例)。利用享元模式,可以创建一个存储26个字母对象的享元池,需要时从享元池中取出。
在这里插入图片描述

享元对象能够做到共享的关键在于区分了内部状态和外部状态:

  • 内部状态:存储在享元对象内部,不会随着环境的改变而改变的,内部状态可以共享。比如围棋中棋子的形状、大小,不会随着外部变化而变化;比如字母A,无论谁使用,都是A,不会变化;
  • 外部状态:随环境变化而变化、不可以共享的状态,如棋子的位置、颜色,如每个字母的位置。外部状态一般由客户端保存,在使用时再传入到享元对象内部。不同的外部状态之间是相互独立的,棋子A和棋子B的位置可以不同,并且不会相互影响。

6.2 享元模式结构

享元模式常常结合工厂模式一起使用,其结构包含抽象享元类、具体享元类、非共享具体享元类和享元工厂类:

  • Flyweight(抽象享元类):是一个抽象类,声明了具体享元类公共的方法,这些方法可以向外部提供享元对象的内部状态数据,也可以通过这些方法设置外部状态;

  • ConcreteFlyweight(具体享元类):具体实现抽象享元类声明的方法,具体享元类中为内部状态提供存储空间。具体享元类常常结合单例模式来设计实现,保证每个享元类对象只被创建一次,为每个具体享元类提供唯一的享元对象。

  • UnsharedConcreteFlyweight(非共享具体享元类):并不是所有抽象享元类的子类都需要被共享,可以将这些类设计为非共享具体享元类;

  • FlyweightFactory(享元工厂类):用于创建并管理享元对象,针对抽象享元类编程,将各种具体享元类对象存储在一个享元池中,享元池一般设计为一个存储键值对的集合(或者其他类型的集合),可结合工厂模式设计。客户需要某个享元对象时,如果享元池中已有该对象实例,则返回该实例,否则创建一个新的实例,给客户返回新的实例,并将新实例保存在享元池中。
    在这里插入图片描述

6.3 享元模式代码实例

很多网络设备都是支持共享的,如交换机(switch)、集线器(hub)等。多台中断计算机可以连接同一台网络设备,并通过网络设备进行数据转发。本节Jungle将使用享元模式来模拟共享网络设备的实例。

本例中,交换机(switch)和集线器(hub)是具体享元对象。UML图如下所示:
在这里插入图片描述

6.3.1 抽象享元类

// 抽象享元类
class NetDevice
{
public:
	NetDevice(){}
	virtual string getName() = 0;
 
	void print(){
		printf("NetDevice :%s\n",getName().c_str());
	}
};

6.3.2 具体享元类

具体享元类有集线器和交换机,实现了抽象享元类声明的方法。

// 具体享元类:集线器
class Hub :public NetDevice
{
public:
	Hub(){}
	string getName(){
		return "集线器";
	}
};
 
// 具体享元类:交换机
class Switch :public NetDevice
{
public:
	Switch(){}
	string getName(){
		return "交换机";
	}
};

6.3.3 享元工厂类

享元工厂类采用了单例模式,保证工厂实例的唯一性。采用一个vector作为共享池。

// 享元工厂类
class NetDeviceFactory
{
public:
	NetDevice* getNetDevice(char ch){
		if (ch == 'S'){
			return devicePool[1];
		}
		else if (ch == 'H'){
			return devicePool[0];
		}
		else{
			printf("wrong input!\n");
		}
		return NULL;
	}
 
	// 单例模式:返回享元工厂类的唯一实例
	static NetDeviceFactory* getFactory(){
		if (instance == NULL){
			m_mutex.lock();
			if (instance == NULL){
				instance = new NetDeviceFactory();
			}
			m_mutex.unlock();
		}
		return instance;
	}
 
private:
	NetDeviceFactory(){
		Hub *hub = new Hub();
		Switch *switcher = new Switch();
		devicePool.push_back(hub);
		devicePool.push_back(switcher);
	}
	static NetDeviceFactory* instance;
	static std::mutex m_mutex;
 
	// 共享池:用一个vector来表示
	vector<NetDevice*> devicePool;
};
 
NetDeviceFactory* NetDeviceFactory::instance = NULL;
std::mutex NetDeviceFactory::m_mutex;

6.3.4 客户端代码示例

#include <iostream>
#include "FlyweightPattern.h"
 
int main()
{
	NetDeviceFactory *factory = NetDeviceFactory::getFactory();
 
	NetDevice *device1, *device2, *device3, *device4;
 
	// 客户端1获取一个hub
	device1 = factory->getNetDevice('H');
	device1->print();
	// 客户端2获取一个hub
	device2 = factory->getNetDevice('H');
	device2->print();
	// 判断两个hub是否是同一个
	printf("判断两个hub是否是同一个:\n");
	printf("device1:%p\ndevice2:%p\n", device1, device2);
 
	printf("\n\n\n\n");
	// 客户端3获取一个switch
	device3 = factory->getNetDevice('S');
	device3->print();
	// 客户端4获取一个switch
	device4 = factory->getNetDevice('S');
	device4->print();
	// 判断两个switch是否是同一个
	printf("判断两个switch是否是同一个:\n");
	printf("device3:%p\ndevice4:%p\n", device3, device4);
 
	printf("\n\n");
 
	system("pause");
	return 0;
}

客户端代码中,两个客户端分别获取集线器,Jungle打印出两个集线器的地址,来判断是否是同一个对象。同理,对交换机,Jungle也进行类似的判断。

6.3.5 运行结果

在这里插入图片描述

由测试结果可以看出,两个集线器对象的地址是相同的,说明它们都是同一个实例对象,两个交换机也都指向同一个交换机实例对象。由此说明本例的代码实现了网络设备的共享。

6.3.6 有外部状态的享元模式

进一步,尽管不同的终端计算机可能会共享同一个集线器(交换机),但是每个计算机接入的端口(port)是不一样的,端口就是每个享元对象的外部状态。 在享元模式的使用过程中,内部状态可以作为具体享元类的成员对象,而外部状态可以通过外部注入的方式添加到具体享元类中。

“通过外部注入”,因此,客户端可以通过函数传参的方式将“端口”号注入具体享元类:

// 抽象享元类
class NetDevice
{
public:
	NetDevice(){}
	virtual string getName() = 0;
 
	/*void print(){
		printf("NetDevice :%s\n",getName().c_str());
	}*/
	void print(int portNum){
		printf("NetDevice :%s  port: %d\n", getName().c_str(), portNum);
	}
};

那么客户端的使用方式将变为:

#include <iostream>
#include "FlyweightPattern.h"
 
int main()
{
	NetDeviceFactory *factory = NetDeviceFactory::getFactory();
 
	NetDevice *device1, *device2, *device3, *device4;
 
	// 客户端2获取一个hub
	device1 = factory->getNetDevice('H');
	device1->print(1);
	// 客户端2获取一个hub
	device2 = factory->getNetDevice('H');
	device2->print(2);
	// 判断两个hub是否是同一个
	printf("判断两个hub是否是同一个:\n");
	printf("device1:%p\ndevice2:%p\n", device1, device2);
 
	printf("\n\n\n\n");
	// 客户端3获取一个switch
	device3 = factory->getNetDevice('S');
	device3->print(1);
	// 客户端4获取一个hub
	device4 = factory->getNetDevice('S');
	device4->print(2);
	// 判断两个hub是否是同一个
	printf("判断两个switch是否是同一个:\n");
	printf("device3:%p\ndevice4:%p\n", device3, device4);
 
	printf("\n\n");
 
	system("pause");
	return 0;
}

效果如下:
在这里插入图片描述

6.4 总结

优点:

  • 享元模式通过享元池存储已经创建好的享元对象,实现相同或相似的细粒度对象的复用,大大减少了系统中的对象数量,节约了内存空间,提升了系统性能;
  • 享元模式通过内部状态和外部状态的区分,外部状态相互独立,客户端可以根据需求任意使用。

缺点:

  • 享元模式需要增加逻辑来取分出内部状态和外部状态,增加了编程的复杂度;

适用环境:

  • 当一个系统中有大量重复使用的相同或相似对象时,使用享元模式可以节约系统资源;
  • 对象的大部分状态都可以外部化,可以将这些状态传入对象中。

七、代理模式

“代理”这个词不陌生吧?买化妆品、买奶粉、买包包,都可以通过代理代购,甚至有专门的代购网站;

或者要购置一些自己不太清楚原理好坏的物品,可以找相关代理负责帮忙购买,当然了,得支付一定费用。

在软件设计模式中,也有一种模式可以提供与代购网站类似的功能。当客户端不能或者不便直接访问一个对象时,可以通过一个称为“代理”的第三方来间接访问,这样的设计模式称为代理模式。

7.1 代理模式简介

代理模式在软件设计中广泛应用,而且产生的变种很多,如远程代理、虚拟代理、缓冲代理、保护代理等。

代理模式:

给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。

代理模式是一种对象结构型模式,在该模式中引入了一个代理对象,在客户端和目标访问对象之间起到中介的作用。代理对象可以屏蔽或删除客户不想访问的内容和服务,也可以根据客户需求增加新的内容和服务。

7.2 代理模式结构

代理模式的关键是代理类(Proxy)。代理模式中引入了抽象层,客户端针对抽象层编程,这样使得客户端可以一致对待真实对象和代理对象。代理模式主要有抽象主题角色(Subject)、代理主题角色(Proxy)和真实主题角色(RealSubject) 组成,其UML图如下:

在这里插入图片描述

  • 抽象主题角色(Subject):声明了代理主题角色和真实主题角色共同的一些接口,因此在任何可以使用真实主题对象的地方都可以使用代理主题角色(想一想代购是不是也是这样?),客户端通常针对抽象主题编程;

  • 代理主题角色(Proxy):代理主题角色通过关联关系引用真实主题角色,因此可以控制和操纵真实主题对象;代理主题角色中提供一个与真实主题角色相同的接口(以在需要时代替真实主题角色),同时还可以在调用对真实主题对象的操作之前或之后增加新的服务和功能;

  • 真实主题角色(RealSubject):真实主题角色是代理角色所代表的真实对象,提供真正的业务操作,客户端可以通过代理主题角色间接地调用真实主题角色中定义的操作。

在实际开发过程中,代理模式产生了很多类型:

  • 远程代理(Remote Proxy):为一个位于不同地址空间的对象提供一个本地的代理对象。不同的地址空间可以在相同或不同的主机中。
  • 虚拟代理(Virtual Proxy):当创建一个对象需要消耗大量资源时,可以先创建一个消耗较少资源的虚拟代理来表示,当真正需要时再创建。
  • 保护代理(Protect Proxy):给不同的用户提供不同的对象访问权限。
  • 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时存储空间,以使更多用户可以共享这些结果。
  • 智能引用代理(Smart Reference Proxy):当一个对象被引用时提供一些额外的操作,比如将对象被调用的次数记录下来等。

7.3 代理模式代码实例

在某应用软件中需要记录业务方法的调用日志,在不修改现有业务的基础上位每个类提供一个日志记录代理类,在代理类中输出日志,例如在业务方法method()调用之前输出“方法method()被调用,调用时间为2019-10-28 07:33:30”,调用之后输出“方法method()”调用成功。在代理类中调用真实业务类的业务方法,使用代理模式设计该日志记录模块的结构。

在这个案例中,真实主题角色是真实业务类,在代理类中调用真实主题角色的method()方法。该实例的UML图如下:
在这里插入图片描述

7.3.1 抽象主题角色

声明抽象方法method():

// 抽象主题角色
class Subject
{
public:
	Subject(){}
	virtual void method() = 0;
};

7.3.2 真实主题角色

实现具体业务方法method():

// 真实主题角色
class RealSubject :public Subject
{
public:
	RealSubject(){}
	void method(){
		printf("调用业务方法\n");
	}
};

7.3.3 代理角色和Log类

// Log类
class Log
{
public:
	Log(){}
	string getTime(){
		time_t t = time(NULL);
		char ch[64] = { 0 };
		//年-月-日 时:分:秒
		strftime(ch, sizeof(ch)-1, "%Y-%m-%d %H:%M:%S", localtime(&t));     
		return ch;
	}
};
 
// 代理类
class Proxy:public Subject
{
public:
	Proxy(){
		realSubject = new RealSubject();
		log = new Log();
	}
	void preCallMethod(){
		printf("方法method()被调用,调用时间为%s\n",log->getTime().c_str());
	}
	void method(){
		preCallMethod();
		realSubject->method();
		postCallMethod();
	}
	void postCallMethod(){
		printf("方法method()调用调用成功!\n");
	}
private:
	RealSubject *realSubject;
	Log* log;
};

7.3.4 客户端代码示例

#include <iostream>
#include "ProxyPattern.h"
 
int main()
{
	Subject *subject;
	subject = new Proxy();
	subject->method();
 
	printf("\n\n");
 
	system("pause");
	return 0;
}

7.3.5 运行结果

在这里插入图片描述

7.4 总结

优点:

  • 代理模式能够协调调用者和被调用者,降低系统耦合度;

  • 客户端针对抽象主题角色编程,如果要增加或替换代理类,无需修改源代码,符合开闭原则,系统扩展性好;

  • 远程代理优点:为两个位于不同地址空间的对象的访问提供解决方案,可以将一些资源消耗较多的对象移至性能较好的计算机上,提高系统整体性能;

  • 虚拟代理优点:通过一个资源消耗较少的对象来代表一个消耗资源较多的对象,节省系统运行开销;

  • 缓冲代理优点:为某一个操作结果提供临时的存储空间,可以在后续操作中使用这些结果,缩短了执行时间;

  • 保护代理优点:控制对一个对象的访问权限,为不同客户提供不同的访问权限。

缺点:

  • 增加了代理类和代理对象,增加了代理对象中的某些处理流程,可能会使得系统响应变慢;
  • 有的代理模式(如远程代理)实现代码较为复杂。

适用环境:

  • 当客户端对象需要访问远程主机中的对象——可以使用远程代理;
  • 当需要用一个资源消耗较少的对象来代表一个资源消耗较多的对象——虚拟代理;
  • 当需要限制不同用户对一个独享的访问权限——保护代理;
  • 当需要为一个频繁访问的操作结果提供临时存储空间——缓冲代理;
  • 当需要为一个对象的访问提供一些额外的操作——智能引用代理。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值