一. 外观模式
- 背景
在现实生活中,常常存在办事较复杂的例子,如办房产证或注册一家公司,有时要同多个部门联系,这时要是有一个综合部门能解决一切手续问题就好了。
软件设计也是这样,当一个系统的功能越来越强,子系统会越来越多,客户对系统的访问也变得越来越复杂。这时如果系统内部发生改变,客户端也要跟着改变,这违背了“开闭原则”,也违背了“迪米特法则”,所以有必要为多个子系统提供一个统一的接口,从而降低系统的耦合度,这就是外观模式的目标。
- 定义和特点
(1). 定义
是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
(2). 优点
A. 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
B. 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
C. 降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。
(3). 缺点
A. 不能很好地限制客户使用子系统类。
B. 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
- 具体实现
(1). 模式结构
A. 外观角色:为多个子系统对外提供一个共同的接口。
B. 子系统角色:实现系统的部分功能,客户可以通过外观角色访问它。
C. 客户端:通过一个外观角色访问各个子系统的功能。
结构图如下:
(2). 使用场景
见下面代码。
(3). 代码实操
子系统代码
/// <summary>
/// 子业务类1
/// </summary>
public class ChildService1
{
public void MyHandler1()
{
Console.WriteLine("我正在处理业务1");
}
}
/// <summary>
/// 子业务类2
/// </summary>
public class ChildService2
{
public void MyHandler2()
{
Console.WriteLine("我正在处理业务2");
}
}
/// <summary>
/// 子业务类3
/// </summary>
public class ChildService3
{
public void MyHandler3()
{
Console.WriteLine("我正在处理业务3");
}
}
外观角色
/// <summary>
/// 外观角色
/// </summary>
public class FacadeService
{
private ChildService1 s1 = new ChildService1();
private ChildService2 s2 = new ChildService2();
private ChildService3 s3 = new ChildService3();
public void MyHandler()
{
s1.MyHandler1();
s2.MyHandler2();
s3.MyHandler3();
}
}
测试
{
Console.WriteLine("---------------下面是普通调用---------------");
ChildService1 s1 = new ChildService1();
ChildService2 s2 = new ChildService2();
ChildService3 s3 = new ChildService3();
s1.MyHandler1();
s2.MyHandler2();
s3.MyHandler3();
Console.WriteLine("---------------下面是外观模式调用---------------");
FacadeService f = new FacadeService();
f.MyHandler();
}
运行结果
- 适用场景分析
(1). 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
(2). 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
(3). 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。
更多C++后端开发技术点知识内容包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,MongoDB,ZK,流媒体,音视频开发,Linux内核,TCP/IP,协程,DPDK多个高级知识点。
【文章福利】另外还整理一些C++后台开发架构师 相关学习资料,面试题,教学视频,以及学习路线图,免费分享有需要的可以点击 C++后端学习资料 免费领取
二. 适配器模式
- 背景
在现实生活中,经常出现两个对象因接口不兼容而不能在一起工作的实例,这时需要第三者进行适配。例如,讲中文的人同讲英文的人对话时需要一个翻译,用直流电的笔记本电脑接交流电源时需要一个电源适配器,用计算机访问照相机的 SD 内存卡时需要一个读卡器等。
在软件设计中也可能出现:需要开发的具有某种业务功能的组件在现有的组件库中已经存在(且不能修改),但它们与当前系统的接口规范不兼容,如果重新开发这些组件成本又很高,这时用适配器模式能很好地解决这些问题。
- 定义和特点
(1). 定义:
将一个类的方法转换成客户希望的另外一种要求的实现规范,使得原本由于接口不兼容而不能一起工作的那些类能一起工作,这就是适配器模式。适配器模式分为【类适配器模式】和【对象适配器模式】两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
(2). 优点
A. 客户端通过适配器可以按照系统要求的编程规范透明地调用目标接口。
B. 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
C. 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
(3). 缺点
对类适配器来说,更换适配器的实现过程比较复杂。
- 具体实现
(1). 模式结构
A. 目标接口:当前系统业务所要求遵守编程规范的接口,它可以是抽象类或接口。
B. 适配者类(被适配的类):第三方提供的新类或系统中已经存在的类,它与系统要求的编程规范的目标接口不兼容。
C. 适配器类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
类适配器图:
对象适配器图:
(2). 使用场景
系统要求所有的数据库帮助类必须实现ISqlHelp接口,面向该接口编程,如SQLServerHelp类。 此时第三方提供了一个新的MySql的帮助类(假设是dll,不能修改),它的编程规范和ISqlHelp不兼容,这个时候就需要引入适配器类,使二者能相互兼容。
(3). 代码实操
系统要求的标准开发规范代码
/// <summary>
/// 数据库连接抽象接口
/// (代码中统一要求, 对数据库操作都要面向该接口编程)
/// </summary>
public interface ISqlHelp
{
public void Add();
public void Del();
public void Modify();
}
/// <summary>
/// SQLServer数据库操作类
/// (这是一个样例)
/// </summary>
public class SQLServerHelp : ISqlHelp
{
public void Add()
{
Console.WriteLine($"sqlserver数据库Add成功");
}
public void Del()
{
Console.WriteLine($"sqlserver数据库Del成功");
}
public void Modify()
{
Console.WriteLine($"sqlserver数据库Modify成功");
}
}
//1. 项目要求的标准编程模式
Console.WriteLine("----------------------------1. 项目要求的标准编程模式---------------------------------");
ISqlHelp s1 = new SQLServerHelp();
s1.Add();
s1.Del();
s1.Modify();
第三提供的新类, 不满足开发规范
///
/// MySQL帮助类(第三方提供,不能修改)
/// 没有实现ISqlHelp接口,有自己的一套逻辑.
/// 但是项目有统一编程要求,要基于ISqlHelp接口编程,
/// 我们不能修改MySqlHelp内部的逻辑,所以这个时候要通过适配器模式进行适配
///
public class MySQLHelp
{
public void AddMySQL()
{
Console.WriteLine($"MySQL数据库Add成功");
}
public void DelMySQL()
{
Console.WriteLine($"MySQL数据库Del成功");
}
public void ModifyMySQL()
{
Console.WriteLine($"MySQL数据库Modify成功");
}
}
//2. 第三方给的MySql帮助类,不符合标准模式
Console.WriteLine("------------------2. 第三方给的MySql帮助类,不符合标准模式----------------------");
MySQLHelp m = new MySQLHelp();
m.AddMySQL();
m.DelMySQL();
m.ModifyMySQL();
通过适配器模式兼容新的开发规范
/// <summary>
/// MySQLHelp适配器类1
/// (通过继承的方式实现)
/// </summary>
public class MySQLHelpAdapter1 : MySQLHelp, ISqlHelp
{
public void Add()
{
AddMySQL();
}
public void Del()
{
DelMySQL();
}
public void Modify()
{
ModifyMySQL();
}
}
///
/// MySQLHelp适配器类2
/// (通过组合的方式实现,可以属性注入、构造函数注入、方法注入)
///
public class MySQLHelpAdapter2 : ISqlHelp
{
public MySQLHelpAdapter2()
{
}
//属性注入,直接写死
private MySQLHelp _mySqlHelp = new MySQLHelp();
/// <summary>
/// 构造函数注入
/// (可以改成抽象模式)
/// </summary>
/// <param name="mySqlHelp"></param>
public MySQLHelpAdapter2(MySQLHelp mySqlHelp)
{
this._mySqlHelp = mySqlHelp;
}
/// <summary>
/// 方法注入
/// (可以改成抽象模式)
/// </summary>
/// <param name="mySqlHelp"></param>
public void SetAdapter(MySQLHelp mySqlHelp)
{
this._mySqlHelp = mySqlHelp;
}
public void Add()
{
_mySqlHelp.AddMySQL();
}
public void Del()
{
_mySqlHelp.DelMySQL();
}
public void Modify()
{
_mySqlHelp.ModifyMySQL();
}
}
//3.通过适配器模式使用MySQL帮助类,且满足标准编程模式
Console.WriteLine("------------------3.1 通过继承模式实现适配器------------------");
ISqlHelp s2 = new MySQLHelpAdapter1();
s2.Add();
s2.Del();
s2.Modify();
Console.WriteLine("------------------3.2 通过组合模式实现适配器------------------");
ISqlHelp s3 = new MySQLHelpAdapter2();
s3.Add();
s3.Del();
s3.Modify();
适配器运行结果:
- 适用场景分析
(1). 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
(2). 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
三. 模板方法模式
- 背景
在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。
例如,去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。
这样的例子在生活中还有很多,例如,一个人每天会起床、吃饭、做事、睡觉等,其中“做事”的内容每天可能不同。我们把这些规定了流程或格式的实例定义成模板,允许使用者根据自己的需求去更新它,例如,简历模板、论文模板、Word 中模板文件等。
- 定义和特点
(1). 定义
定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。
(2). 优点
A. 它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
B. 它在父类中提取了公共的部分代码,便于代码复用。
C. 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。
(3). 缺点
A. 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
B. 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
- 具体实现
(1). 模式结构
A. 抽象类:负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。这些方法的定义如下。
① 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
② 基本方法:是整个算法中的一个步骤,包含以下几种类型。
抽象方法:在抽象类中申明,由具体子类实现。
具体方法:在抽象类中已经实现,在具体子类中可以继承或重写它。
B. 具体子类:实现抽象类中所定义的抽象方法。
结构图如下:
(2). 使用场景
处理银行业务:取号→排队→办业务→评分, 除了办业务因人而异外(在子类中实现), 其它三步都是固定的,在抽象类中实现。
(3). 代码实操
抽象类:
///
/// 抽象类
/// 处理银行业务:取号→排队→办业务→评分
/// 除了办业务因人而异外, 其它三步都是固定的,在抽象类中实现
///
public abstract class AbstractHandler
{
///
/// 模板方法
/// 里面的步骤是按照固定顺序执行的
///
public void TemplateMethod()
{
Handler1();
Handler2();
Handler3();
Handler4();
}
/// <summary>
/// 具体方法1
/// </summary>
public void Handler1()
{
Console.WriteLine("我是取号业务");
}
/// <summary>
/// 具体方法2
/// </summary>
public void Handler2()
{
Console.WriteLine("我是排队业务");
}
/// <summary>
/// 抽象方法3
/// 个人业务,因人而异,需要去子类中实现
/// </summary>
public abstract void Handler3();
/// <summary>
/// 具体方法4
/// </summary>
public void Handler4()
{
Console.WriteLine("我是评分业务");
}
}
具体子类:
/// <summary>
/// 具体子类1
/// </summary>
public class ChildHandler1 : AbstractHandler
{
/// <summary>
/// 重写办个人业务的方法
/// </summary>
public override void Handler3()
{
Console.WriteLine("我来进行理财业务");
}
}
/// <summary>
/// 具体子类2
/// </summary>
public class ChildHandler2 : AbstractHandler
{
/// <summary>
/// 重写办个人业务的方法
/// </summary>
public override void Handler3()
{
Console.WriteLine("我来进行存款业务");
}
}
测试:
{
//流程1
Console.WriteLine("---------------下面是流程1---------------------");
AbstractHandler h1 = new ChildHandler1();
h1.TemplateMethod();
//流程2
Console.WriteLine("---------------下面是流程2---------------------");
AbstractHandler h2 = new ChildHandler2();
h2.TemplateMethod();
}
运行结果:
- 适用场景分析
(1). 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
(2). 当多个子类存在公共的行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。