学习设计模式《三》——适配器模式
一、基础概念
适配器模式的本质是【转换匹配,复用功能】;
适配器模式定义:将一个类的接口转换为客户希望的另外一个接口;适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
适配器模式的目的:复用已有的功能,进行转换匹配现有接口(即:负责把不兼容的接口转换为客户端期望的样子);
何时选用适配器模式?
1、想要使用一个已经存在的类,但是它的接口不符合你的需求;
2、想创建一个可以复用的类,这个类和一些不兼容的类一起工作;
3、想使用一些已经存在的类,但是不可能对每一个子类进行适配(直接适配这些子类的父类);
序号 | 适配器模式的优点 | 适配器模式的缺点 |
1 | 更好的复用性 (功能已经有了,只是接口不兼容,通过适配器模式就可以让这些已有功能得到更好复用) | 过多使用适配器,会让系统非常零乱,不容易进行整体把握 (即:明明看到调用的是A接口,但其实内部被适配成了B接口来实现;或系统中出现太多这种情况,是一场灾难;若无必要,建议直接重构) |
2 | 更好的可扩展性 (实现适配器的时候,可以调用已经开发的功能,更加自然的扩展系统功能) |
二、适配器模式示例
2.1、日志管理第一版
日志管理的第一版,只要求将日志内容记录到本地文件中即可:
1、定义日志对象
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace AdapterPattern.LogManagerV1
{/// <summary>/// 日志模型/// </summary>internal class LogModel{//日志编号private string? logId;//日志操作人员private string? operateUser;//日志操作时间(以yyyy-MM-DD HH:mm:ss格式记录)private string? operateTime;//日志内容private string? logContent;/// <summary>/// 日志编号/// </summary>public string? LogId { get => logId; set => logId = value; }/// <summary>/// 日志操作人员/// </summary>public string? OperateUser { get => operateUser; set => operateUser = value; }/// <summary>/// 日志操作时间(以yyyy-MM-DD HH:mm:ss格式记录)/// </summary>public string? OperateTime { get=>operateTime; set=>operateTime=value; }/// <summary>/// 日志内容/// </summary>public string? LogContent { get => logContent; set => logContent = value; }/// <summary>/// 写入配置文件内容/// </summary>/// <param name="separator">内容分隔符(默认为逗号)</param>/// <returns></returns>public string toStringWrite(char separator=','){StringBuilder stringBuilder = new StringBuilder();stringBuilder.AppendJoin(separator, new[] {logId,OperateUser,OperateTime,logContent }); return stringBuilder.ToString();}/// <summary>/// 界面展示内容/// </summary>/// <param name="separator">内容分割符(默认为一个空格)</param>/// <returns></returns>public string toStringShow(char separator = ' '){StringBuilder stringBuilder = new StringBuilder();stringBuilder.AppendFormat($"LogId={LogId}{separator}");stringBuilder.AppendFormat($"OperateUser={OperateUser}{separator}");stringBuilder.AppendFormat($"OperateTime={OperateTime}{separator}");stringBuilder.AppendFormat($"LogContent={LogContent}{separator}");return stringBuilder.ToString();}}//Class_end
}
2、定义操作日志文件的接口
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace AdapterPattern.LogManagerV1
{/// <summary>/// 日志文件操作接口/// </summary>internal interface ILogFileOpereate{/// <summary>/// 读取日志文件,从文件里面获取存储的日志列表内容/// </summary>/// <returns>返回读取的日志内容列表</returns>List<LogModel> ReadLogFile();/// <summary>/// 写日志文件,把日志列表内容写到日志文件中/// </summary>/// <param name="list">需写入的日志内容列表</param>void WriteLogFile(List<LogModel>list);}//Interface_end
}
3、定义一个类继承操作日志文件接口并实现操作日志的具体方法
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace AdapterPattern.LogManagerV1
{/// <summary>/// 继承接口并实现接口内容(对日志文件的操作)/// </summary>internal class LogFileOperate : ILogFileOpereate{private readonly static string _BaseDir=AppDomain.CurrentDomain.BaseDirectory;private string _logFilePathAndName=Path.Combine(_BaseDir,@"LogFile\AdapterLog.log");public LogFileOperate(){}public LogFileOperate(string logFilePathAndName){//先判断是否传入了新的日志文件路径和名称,若有则替换否则使用默认路径if (!string.IsNullOrEmpty(logFilePathAndName)){this._logFilePathAndName = logFilePathAndName;}}public string LogFilePathAndName { get{string? logPath = Path.GetDirectoryName(_logFilePathAndName);if (!Directory.Exists(logPath)){Directory.CreateDirectory(logPath);}if (!File.Exists(_logFilePathAndName)){File.Create(_logFilePathAndName).Close();}return _logFilePathAndName;}}public List<LogModel> ReadLogFile(){List<LogModel> logModels=new List<LogModel>();using (FileStream fs=new FileStream(LogFilePathAndName,FileMode.Open)){using (StreamReader sr = new StreamReader(fs)){string strLine = sr.ReadLine();while (!string.IsNullOrEmpty(strLine)){string[] strLineArray=strLine.Split(',');LogModel logModel = new LogModel();logModel.LogId = strLineArray[0];logModel.OperateUser = strLineArray[1];logModel.OperateTime = strLineArray[2];logModel.LogContent = strLineArray[3];logModels.Add(logModel);strLine= sr.ReadLine();}}}return logModels;}public void WriteLogFile(List<LogModel> list){using (FileStream fs=new FileStream(LogFilePathAndName,FileMode.Append,FileAccess.Write,FileShare.Write)){using (StreamWriter sw=new StreamWriter(fs)){foreach (LogModel logModel in list) { sw.WriteLine(logModel.toStringWrite());}}}}}//Class_end
}
4、客户端测试
using AdapterPattern.LogManagerV1;
using AdapterPattern.LogManagerV2;namespace AdapterPattern
{internal class Program{//客户端:用来测试static void Main(string[] args){TestLogOpc();Console.ReadLine();}/// <summary>/// 测试日志文件操作/// </summary>private static void TestLogOpc(){Console.WriteLine("-----测试日志文件操作------");//准备一个日志内容对象LogModel logModel=new LogModel();logModel.LogId = Guid.NewGuid().ToString();logModel.OperateUser = "test";logModel.OperateTime= DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");logModel.LogContent = $"这是一个测试内容{new Random(Guid.NewGuid().GetHashCode()).Next(0,99)}_写入文件";List<LogModel> logModels=new List<LogModel>();logModels.Add(logModel);//创建日志文件操作对象string logFilePathAndName = @"H:\Log\Adapter.log";ILogFileOpereate logFileOpereate = new LogFileOperate(logFilePathAndName);//写入日志文件内容到本地文件中logFileOpereate.WriteLogFile(logModels);//读取本地日志文件内容List<LogModel> readLogModels= new List<LogModel>();readLogModels = logFileOpereate.ReadLogFile();//将读取的日志文件内容展示到界面上foreach (var item in readLogModels){string str = $"读取的日志文件内容为:{item.toStringShow()}";Console.WriteLine(str);}}}//Class_end
}
运行结果如下:
FileStream 类 (System.IO) | Microsoft Learnhttps://learn.microsoft.com/zh-cn/dotnet/api/system.io.filestream?view=net-9.0 Path 类 (System.IO) | Microsoft Learn
https://learn.microsoft.com/zh-cn/dotnet/api/system.io.path?view=net-9.0
Stream 类 (System.IO) | Microsoft Learnhttps://learn.microsoft.com/zh-cn/dotnet/api/system.io.stream?view=net-9.0
2.2、日志管理第二版(单向适配)
实现了第一版日志管理一段时间后,系统升级;客户提出需要使用数据库来管理日志;此时我们针对这个需求定义了数据库管理日志的接口(包含日志的增、删、查、改):
1、定义数据库管理日志的接口
using AdapterPattern.LogManagerV1;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace AdapterPattern.LogManagerV2
{/// <summary>/// 现在采用数据库来管理日志,定义日志的数据库接口/// </summary>internal interface ILogDBOperate{//新增日志bool CreateLog(LogModel logModel);//删除日志bool DeleteLog(LogModel logModel);//修改日志bool UpdateLog(LogModel logModel);//查询所有日志List<LogModel> QueryAllLog();}//Interface_end
}
2、定义一个类继承【据库管理日志的接口】并实现对应的方法
using AdapterPattern.LogManagerV1;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace AdapterPattern.LogManagerV2
{/// <summary>/// 数据库存储日志类(这里仅作示意就不真正实现与数据库的交互)/// </summary>internal class LogDBOperate : ILogDBOperate{public bool CreateLog(LogModel logModel){Console.WriteLine($"记录日志到数据库:{logModel.toStringShow()}");//省略了连接数据库并保存日志的操作return true;}public bool DeleteLog(LogModel logModel){Console.WriteLine($"删除数据库日志:{logModel.toStringShow()}");//省略了连接数据库并删除日志的操作return true;}public List<LogModel> QueryAllLog(){Console.WriteLine($"获取数据库的所有日志");//省略了连接数据库并查询日志的操作return new List<LogModel>();}public bool UpdateLog(LogModel logModel){Console.WriteLine($"更新数据库日志:{logModel.toStringShow()}");//省略了连接数据库并更新日志的操作return true;}}//Class_end
}
现在的问题是:目前的业务内容都是使用第二版的数据库管理日志接口方法操作的;现在直接使用第二版的数据管理日志是没有问题的;可是对于已经有的日志管理方式(存储到本地文件日志)与现在数据库管理的接口不一致;导致现在的客户端无法以同样的方法来直接使用第一版实现。这意味着【想要同时支持文件和数据库两种方式对日志操作,需要在额外的做一些工作,才可以让第一版的实现适应新的业务需要】如下图所示:
一种很容易得方式就是直接修改已有的第一版代码(但是这种方式不太好:原因是
【1、若直接修改第一版代码,可能会导致其他依赖这些实现的应用不能正常运行】;
【2、有可能第一版代码与第二版代码开发公司不一样,在实现第二版的时候,根本获取不到第一版的源码】)。
此时我们就可以使用适配器模式来解决这个问题:(即:我们按照第二版的接口定义一个类并继承第二版的接口,然后在这个类的内部使用第一版已有的实现方法进行复用组合操作)这就是一个单向的适配器了:
1、新建一个适配器类(继承第二版的日志操作接口【然后使用第一版的日志操作实现来完成第二版的增、删、改、查】方法)
/***
* Title:"设计模式" 项目
* 主题:适配器模式(单向适配器)
* Description:
* 基础概念:本质是【转换匹配,复用功能】
* 适配器模式:将一个类的接口转换为客户希望的另外一个接口;适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
*
* 适配器模式的目的:复用已有的功能,进行转换匹配现有接口(即:负责把不兼容的接口转换为客户端期望的样子)
*
* 适配器模式优点:
* 1、更好的复用性(功能已经有了,只是接口不兼容,通过适配器模式就可以让这些已有功能得到更好复用)
* 2、更好的可扩展性(实现适配器的时候,可以调用已经开发的功能,更加自然的扩展系统功能)
*
* 适配器模式的缺点:
* 1、过多使用适配器,会让系统非常零乱,不容易进行整体把握(即:明明看到调用的是A接口,
* 但其实内部被适配成了B接口来实现;或系统中出现太多这种情况,是一场灾难;若无必要,建议直接重构)
*
* 何时选用适配器模式?
* 1、想要使用一个已经存在的类,但是它的接口不符合你的需求
* 2、想创建一个可以复用的类,这个类和一些不兼容的类一起工作
* 3、想使用一些已经存在的类,但是不可能对每一个子类进行适配(直接适配这些子类的父类)
*
* Date:2025
* Version:0.1版本
* Author:Coffee
* Modify Recoder:***/using AdapterPattern.LogManagerV1;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace AdapterPattern.LogManagerV2
{/// <summary>/// 单向适配器类(继承第二版的日志操作接口【然后使用第一版的日志操作实现来完成第二版的增、删、改、查】方法/// </summary>internal class LogFileAdapter : ILogDBOperate{//持有需要被适配的接口对象(第一版文件日志接口)private ILogFileOpereate logFileOpereate;public LogFileAdapter(ILogFileOpereate logFileOpereate){this.logFileOpereate = logFileOpereate;}public bool CreateLog(LogModel logModel){List<LogModel> logModels=new List<LogModel>();//1、加入新的日志对象logModels.Add(logModel);//2、重新写入文件logFileOpereate.WriteLogFile(logModels);return true;}public bool DeleteLog(LogModel logModel){//1、读取日志文件内容List<LogModel> logModels = new List<LogModel>();//2、移除对应的日志对象logModels.Remove(logModel);//重新写入日志文件logFileOpereate.WriteLogFile(logModels);return true;}public List<LogModel> QueryAllLog(){return logFileOpereate.ReadLogFile();}public bool UpdateLog(LogModel logModel){//1、先读取文件内容List<LogModel> logModels=logFileOpereate.ReadLogFile();//2、修改相应地日志对象int count=logModels.Count;for (int i = 0; i < count; i++){if (logModels[i].LogId.Equals(logModel.LogId)){logModels[i] = logModel;break;}}//3、重新写入文件logFileOpereate.WriteLogFile(logModels);return true;}}//Class_end
}
2、客户端的单向适配器实现
using AdapterPattern.LogManagerV1;
using AdapterPattern.LogManagerV2;namespace AdapterPattern
{internal class Program{//客户端:用来测试static void Main(string[] args){TestLogAdapterOpc();Console.ReadLine();}/// <summary>/// 测试日志单向适配器操作/// </summary>private static void TestLogAdapterOpc(){Console.WriteLine("\n-----测试日志单向适配器操作------");//准备一个日志内容对象LogModel logModel = new LogModel();logModel.LogId = Guid.NewGuid().ToString();logModel.OperateUser = "adapter";logModel.OperateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");logModel.LogContent = $"这是一个测试内容{new Random(Guid.NewGuid().GetHashCode()).Next(0, 99)}_单向适配器";List<LogModel> logModels = new List<LogModel>();logModels.Add(logModel);//创建日志文件操作对象ILogFileOpereate logFileOpereate = new LogFileOperate();//创建新版操作日志的单向适配器接口对象ILogDBOperate logDBOperate = new LogFileAdapter(logFileOpereate);//写入日志文件内容到本地文件中logDBOperate.CreateLog(logModel);//读取本地日志文件内容List<LogModel> readLogModels = new List<LogModel>();readLogModels = logDBOperate.QueryAllLog();//将读取的日志文件内容展示到界面上foreach (var item in readLogModels){string str = $"读取的日志文件内容为:{item.toStringShow()}";Console.WriteLine(str);}}}//Class_end
}
运行结果如下:
2.3、日志管理第三版(双向适配)
已经完成了单向适配;但是由于某些原因,第一版与第二版的客户端会共存一段时间;这段时间内第二版的应用还在不断迭代调整中,不够稳定。客户希望在两版共存期间,主要还是使用第一版;同时希望第一版的日志也能够记录到数据库中(即:客户虽然目前还是使用第一版的客户端操作第一版的日志接口,但是此时也可以使用第二版的数据库日志功能)也就是希望两个版本实现双向适配,如下图所示:
1、新建双向适配器(同时继承第一版、第二版日志接口)
using AdapterPattern.LogManagerV1;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace AdapterPattern.LogManagerV2
{/// <summary>/// 双向适配器(同时继承第一版、第二版日志操作接口并实现)/// </summary>internal class TwoDirectAdapter : ILogFileOpereate, ILogDBOperate{//持有需要被适配文件存储日志的接口对象private ILogFileOpereate logFileOpereate;//持有需要被适配数据库存储日志的接口对象private ILogDBOperate logDBOperate;/// <summary>/// 构造方法(传入需要被适配的对象)/// </summary>/// <param name="logFileOpereate">文件存储日志接口对象</param>/// <param name="logDBOperate">数据库存储日志接口对象</param>public TwoDirectAdapter(ILogFileOpereate logFileOpereate,ILogDBOperate logDBOperate){this.logFileOpereate = logFileOpereate;this.logDBOperate = logDBOperate; }/*增删改查方法是用文件操作日志适配数据库操作日志实现方式的接口方法*/public bool CreateLog(LogModel logModel){List<LogModel> logModels = new List<LogModel>();logModels.Add(logModel);logFileOpereate.WriteLogFile(logModels);return true;}public bool DeleteLog(LogModel logModel){//1、读取日志文件内容List<LogModel> logModels = logFileOpereate.ReadLogFile();//2、移除对应的日志对象logModels.Remove(logModel);//重新写入日志文件logFileOpereate.WriteLogFile(logModels);return true;}public List<LogModel> QueryAllLog(){return logFileOpereate.ReadLogFile();}public bool UpdateLog(LogModel logModel){//1、先读取文件内容List<LogModel> logModels = logFileOpereate.ReadLogFile();//2、修改相应地日志对象int count = logModels.Count;for (int i = 0; i < count; i++){if (logModels[i].LogId.Equals(logModel.LogId)){logModels[i] = logModel;break;}}//3、重新写入文件logFileOpereate.WriteLogFile(logModels);return true;}/*如下两个方法是使用数据库操作日志的方式适配文件操作日志的接口方法*/public List<LogModel> ReadLogFile(){return logDBOperate.QueryAllLog();}public void WriteLogFile(List<LogModel> list){foreach (LogModel logModel in list){logDBOperate.CreateLog(logModel);}}}//Class_end
}
2、客户端使用双向适配器
using AdapterPattern.LogManagerV1;
using AdapterPattern.LogManagerV2;namespace AdapterPattern
{internal class Program{//客户端:用来测试static void Main(string[] args){TestTwoDirectAdapterOpc();Console.ReadLine();}/// <summary>/// 测试双向日志适配器操作/// </summary>private static void TestTwoDirectAdapterOpc(){Console.WriteLine("\n-----测试双向日志适配器操作------");//准备一个日志内容对象LogModel logModel = new LogModel();logModel.LogId = Guid.NewGuid().ToString();logModel.OperateUser = "TwoDirectAdapter";logModel.OperateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");logModel.LogContent = $"这是一个测试内容{new Random(Guid.NewGuid().GetHashCode()).Next(0, 99)}_双向适配器";List<LogModel> logModels = new List<LogModel>();logModels.Add(logModel);//创建日志文件操作对象string logFilePathAndName = @"H:\Log\Adapter.log";ILogFileOpereate logFileOpereate = new LogFileOperate(logFilePathAndName);//创建数据库日志操作对象ILogDBOperate logDBOperate = new LogDBOperate();//创建经过双向适配器后操作日志的对象ILogFileOpereate logFileOpereateTwoDirectAdapter = new TwoDirectAdapter(logFileOpereate,logDBOperate);ILogDBOperate logDBOperateTwoDirectAdapter= new TwoDirectAdapter(logFileOpereate, logDBOperate);/*测试从文件日志操作适配数据库日志*/Console.WriteLine("\n\n使用数据库日志接口方法保存日志到本地日志文件中");logDBOperateTwoDirectAdapter.CreateLog(logModel);List<LogModel> allLog= logDBOperateTwoDirectAdapter.QueryAllLog();foreach (var item in allLog){Console.WriteLine($"使用数据库日志接口方法获取到本地日志文件的所有信息:{item.toStringShow()}");}/*测试*/Console.WriteLine("\n\n使用日志文件接口方法保存日志到数据库中");logFileOpereateTwoDirectAdapter.WriteLogFile(logModels);Console.WriteLine("\n使用日志文件接口方法读取到数据库所有日志");logFileOpereateTwoDirectAdapter.ReadLogFile();}}//Class_end
}
运行结果如下:
三、项目源码工程
kafeiweimei/Learning_DesignPattern: 这是一个关于C#语言编写的基础设计模式项目工程,方便学习理解常见的26种设计模式https://github.com/kafeiweimei/Learning_DesignPattern