目录
介绍
文章的目标
理解本文您需要知道什么
适配器设计模式的简要提示
第一个示例:静态.NET类
第二个示例:使用第三方记录器替换自定义记录器
第三个示例:使用不同的自定义记录器替换自定义记录器
第四个示例:将旧静态类调整为新代码
结论
4个使用适配器设计模式的非常实用的例子
介绍在本文中,我想展示适配器设计模式如何在开发企业软件时让您的生活更轻松。
在本文中,我想向您展示4个常见的软件开发问题并为它们提供解决方案。我将使用适配器设计模式作为问题解决者!
文章的目标
本文的目的不是解释适配器模式的工作原理。有许多伟大的文章解释了这一点。我的目标是展示它如何帮助您在日常工作中编程。
我将展示非常实用的例子。在我作为软件开发人员的工作期间,我遇到了这些问题,因此很有可能您将遇到它或者您已经遇到过。
理解本文您需要知道什么要理解本文,您必须:
- 理论上至少要了解适配器模式
- 了解依赖注入设计模式
- 对自动化测试有基本的了解
- 对单元测试的模拟对象有基本的了解
- 熟悉C#语言
别说了,开始做!
适配器设计模式的简要提示正如我之前提到的,我不会教适配器模式是什么,我只是想简要提醒一下它的一般概念,所以...
适配器模式是结构设计模式之一。
维基百科说:
适配器可帮助两个不兼容的接口协同工作。
维基百科说:
接口可能不兼容,但内部功能应该适合需要。
维基百科说:
适配器设计模式允许通过将一个类的接口转换为客户端期望的接口来使其他不兼容的类一起工作。
而这正是它的作用。
在上图中,我们可以看到Adapter模式的类图。我们基本上有:
- Client——我们的应用程序中的一个类,调用者
- Adaptee——我们想要从我们的Client中使用的一个类,但因为不兼容的接口我们不能
- Adapter——允许我们从Client中使用Adaptee类的类
总而言之,如果我们想要从我们的系统中使用外部组件,我们可以通过它来实现这一点,我们可以使用适配器模式。
我们去具体实现吧!
第一个示例:静态.NET类现在想象一下,您正在开发一个green field项目,并且您的系统中有一个类,负责在本地磁盘上存储文件。
负责此操作的方法是将文件作为字节数组获取,并使用System.IO命名空间中的File类将其写入本地磁盘。
您的代码非常干净,您使用依赖注入来注入配置和记录器,您的代码如下所示:
public class FileSystemManager
{
private readonly IConfiguration _configuration;
private readonly ILogger _logger;
public FileSystemManager(IConfiguration configuration, ILogger logger)
{
_configuration = configuration;
_logger = logger;
}
public bool SaveFile(FileRepresentation file)
{
try
{
var path = string.Format("{0}{1}", _configuration.GetPathToRepository(file.Repository),
_fileHelper.GetFileNameInRepository(file.Name));
File.WriteAllBytes(path, file.Content);
return true;
}
catch (Exception ex)
{
_logger.LogError();
return false;
}
}
}
您构造存储文件的路径,然后使用WriteAllBytes 方法保存文件。
到现在为止还挺好!但是......我们无法隐藏接口背后的static File类,因为:
- 它是一个static类,不能实现任何接口
- 它是一个内置的.NET Framework类,我们无法使它实现我们的自定义接口
这是一个问题吗?是的!
想象一下,您想为此类编写单元测试,以检查:
- 如果文件存储正确,则函数SaveFile返回true值
- 如果函数SaveFile返回false值,文件是否被不正确地存储
- WriteAllBytes方法是否被使用正确构造的路径调用
- 如果WriteAllBytes方法会抛出错误,那将是什么输出
- 等等..
如果我们不能模拟这个File类,我们如何创建一个UNIT TEST(注意我现在不是在谈论集成测试)?
这是我们的第一个问题!
那我们现在能做什么呢???
使用我们的朋友——适配器设计模式!
但是我们怎么做呢?
我们首先介绍一个接口:
public interface IFileService
{
void SaveFile(string path, byte[] content);
}
并改变我们的FileSystemManager为UnitTestableFileSystemManager:
public class UnitTestableFileSystemManager
{
private readonly IConfiguration _configuration;
private readonly ILogger _logger;
private readonly IFileService _fileService;
public UnitTestableFileSystemManager(IConfiguration configuration,
ILogger logger, IFileService fileService)
{
_configuration = configuration;
_logger = logger;
_fileService = fileService;
}
public bool SaveFile(FileRepresentation file)
{
try
{
var path = string.Format("{0}{1}", _configuration.GetPathToRepository(file.Repository),
_fileHelper.GetFileNameInRepository(file.Name));
_fileService.SaveFile(path, file.Content);
return true;
}
catch (Exception ex)
{
_logger.LogError();
return false;
}
}
}
在上面的代码中,我用一个IFileService接口替换了一个File类,并使该类允许向其中注入该接口的具体实现。
现在我们可以轻松地模拟我们的文件服务并将其注入到UnitTestableFileSystemManager类中。由于我们可以以相同的方式处理配置类和记录器类,因此我们现在可以编写任意数量的单元测试!
但等一下,当static File类没有实现IFileService接口时,我们将在UnitTestableFileSystemManager类中注入什么实现?
现在我们需要Adapter:
public class FileServiceAdapter : IFileService
{
public void SaveFile(string path, byte[] content)
{
File.WriteAllBytes(path, content);
}
}
您可以注意到该类在内部FileServiceAdapter使用了File类。目标已经实现!欢呼!!!
注:当来自.NET框架的其他类(如SmtpClient)或来自第三方库的任何其他类遇到同样的问题时,您可以使用相同的方法
注:我们获得的不仅仅是单元测试的能力,我们现在能够用任何其他实现替换FileServiceAdapter。
第二个示例:使用第三方记录器替换自定义记录器
现在想象一下你正在开发几年前实现的系统。不幸的是,它经常发生在我们身边:)经常......
只要需要记录某些事件已发生或记录异常,系统就会使用DatabaseLogger类。好处是它由一个优秀的开发人员实现,并且DatabaseLogger 实现隐藏在ILogger接口后面:
public interface ILogger
{
void LogError(Exception ex);
void LogInfo(string message);
}
所以应用程序中特定类的代码如下所示:
public class SampleClassOne
{
private readonly ILogger _logger;
public SampleClassOne(ILogger logger)
{
_logger = logger;
}
public void SampleMethod()
{
// some code
_logger.LogInfo("User was added!");
// some code
_logger.LogInfo("Email was sent!");
// some code
}
}
public class SampleClassTwo
{
private readonly ILogger _logger;
public SampleClassTwo(ILogger logger)
{
_logger = logger;
}
public void SampleMethod()
{
try
{
// some code
_logger.LogInfo("File was saved!");
}
catch (Exception ex)
{
_logger.LogError(ex);
}
}
}
这是DatabaseLogger类的实现:
public class DatabaseLogger : ILogger
{
public void LogError(Exception ex)
{
// code responsible for storing an exception in the database
}
public void LogInfo(string message)
{
// code responsible for storing an information in the database
}
}
我们的任务是用Log4Net(实现日志功能的外部库)记录器替换DatabaseLogger,因为技术领导者决定我们正在标准化我们的系统,我们将Log4Net库用于公司的所有系统。
我们认为没问题!如果实现隐藏在接口后面,我们将只使用包含在Log4Net库中的记录器实现的类替换DatabaseLogger类。但是,我们意识到Log4Net的记录器实现没有实现我们的ILogger接口,它实现了ILog接口,其与我们系统接口不兼容。
所以我们有两个不兼容的接口,适配器设计模式要解决的经典问题!
那就让我们解决吧!
我们所要做的就是创建Log4NetAdapter类,其将实现ILogger接口:
public class Log4NetAdapter : ILogger
{
private readonly ILog _logger;
public Log4NetAdapter()
{
_logger = LogManager.GetLogger(typeof(Log4NetAdapter));
}
public void LogError(Exception ex)
{
_logger.Error(ex);
}
public void LogInfo(string message)
{
_logger.Info(message);
}
}
并告诉应用程序使用Log4NetAdapter 类而不是DatabaseLogger类作为ILogger依赖注入配置中的接口的实现。
您可以注意到我们的Log4NetAdapter 类在内部使用Log4Net记录器:
_logger = LogManager.GetLogger(typeof(Log4NetAdapter));
注:当然,要使Log4Net正常工作,您必须在代码或配置文件中添加记录器的配置,并在应用程序启动时注册Log4Net,但它与本文的主题无关。
第三个示例:使用不同的自定义记录器替换自定义记录器是的,本节标题看起来不太好。:)但是,让我们去具体实现吧。本节的目的是展示适配器设计模式不仅可以帮助您修改外部库中无法修改的类。
想象一下,您的情况与前一个示例中的情况类似。应用程序使用隐藏在ILogger接口后面的DatabaseLogger 类来记录一些事件和异常。
现在,您从团队负责人那得到了一项任务,即当应用程序捕获异常时,必须将包含异常详细信息的电子邮件发送到一组特殊的电子邮件地址。但是......事件发生的信息必须以与以前相同的方式记录——在数据库中。此外,对于电子邮件日志记录功能,您必须使用公司中使用的常见实现,即EmailLogger:
public class EmailLogger : IEmailLogger
{
public void SendError(Exception ex)
{
// code responsible for sending an exception on e-mail address
}
}
实现IEmailLogger 接口:
public interface IEmailLogger
{
void SendError(Exception ex);
}
该EmailLogger实现被放置在一个单独的项目中,以在应用程序之间共享它。现在,您可以以您想要的任何方式修改EmailLogger类,因为它是一个自定义实现。但是......在这种情况下它会是最好的解决方案吗?许多应用程序都使用EmailLogger代码,如果负责其应用程序的每个开发人员都修改其代码以使记录器与其应用程序保持一致,那么它将如何?
不太好。:)
另一件事是ILogger接口的实现现在必须同时支持数据库日志记录和电子邮件日志记录。
接下来的事情是,即使您决定让EmailLogger类实现应用程序使用的ILogger接口,EmailLogger库也必须引用您的应用程序(项目),并且您的应用程序(项目)必须引用EmailLogger项目以了解EmailLogger类的实现细节。它最终会得到循环依赖。要解决此问题,您必须引入另一个带接口的项目。
我有一个更好的解决方案。只需实现LoggerAdapter类(其将实现ILogger接口):
public class LoggerAdapter : ILogger
{
private readonly EmailLogger _emailLogger;
private readonly DatabaseLogger _databaseLogger;
public LoggerAdapter()
{
_emailLogger = new EmailLogger();
_databaseLogger = new DatabaseLogger();
}
public void LogError(Exception ex)
{
_emailLogger.SendError(ex);
}
public void LogInfo(string message)
{
_databaseLogger.LogInfo(message);
}
}
并告诉您的应用程序将其用作ILogger接口的实现(同时配置依赖项注入)。
瞧!第三个问题解决了
第四个示例:将旧静态类调整为新代码
下一个情况——你仍然是一个不开心的开发人员,维护着一个旧系统。
你以前的一位同事写了一个庞大的类,它有很多复杂的业务逻辑并且定义它为static。该类在应用程序的很多地方使用。在决定重构此类之前,您不希望触及其中的代码。
static类如下所示:
public static class StaticClass
{
public static decimal FirstComplexStaticMethod()
{
// complex logic
}
public static decimal SecondComplexStaticMethod()
{
// complex logic
}
}
并且在您的应用程序的许多类中使用,如下:
public class SampleClass
{
public void SampleMethod()
{
// some code
var resultOfComplexProcessing = StaticClass.FirstComplexStaticMethod();
// some code
var anotherResultOfComplexProcessing = StaticClass.SecondComplexStaticMethod();
// some code
}
}
现在你必须实现一个新模块,但是你需要在这个类中使用复杂的逻辑。同时,你想要编写一个新的,干净的代码,而不需要使用大型的static类——什么会使你的代码不可测试(我的意思是单元测试),并使你的类与旧static类紧密结合。你不能在接口后面隐藏这个有问题的类,因为static类不能实现任何接口。但是你不想让你的类与旧static类紧密结合。正如我之前提到的,您不希望更改static类的实现,因为您不想破坏其他旧的工作功能。
因此,您不希望新的质量代码看起来像下面的类:
public class NewCleanModule
{
public void SampleMethod()
{
// some code
var resultOfComplexProcessing = StaticClass.FirstComplexStaticMethod();
// some code
}
}
你被阻止了!
解决方案非常简单,只需使用适配器设计模式即可。:)
应用适配器模式后,您的新的漂亮类看起来像下面的类:
public class NewCleanModule
{
private readonly IComplexLogic _complexLogic;
public NewCleanModule(IComplexLogic complexLogic)
{
_complexLogic = complexLogic;
}
public void SampleMethod()
{
// some code
var resultOfComplexProcessing = _complexLogic.FirstComplexMethod();
// some code
}
}
我们在这做了什么?
让我来介绍IComplexLogic接口:
public interface IComplexLogic
{
decimal FirstComplexMethod();
}
它只暴露了一个方法——一个我们需要的新的,干净的类的方法——读取:SOL I D原则中的接口隔离原理。
这个接口的实现将被注入到我们的新类中,这将使我们能够为我们的新类创建单元测试,并允许我们在不改变调用者的情况下用重构的逻辑替换旧逻辑的旧实现。
很好,很好但是......我们还有一个问题。我们的旧static类无法实现任何接口。那么我们如何将这个类的对象当作IComplexLogic接口的实现注入呢?
适配器设计模式来了!
下面的代码介绍了我们如何使旧StaticClass模块适应新模块:
public class StaticClassAdapter : IComplexLogic
{
public decimal FirstComplexMethod()
{
return StaticClass.FirstComplexStaticMethod();
}
}
这种方法可以帮助您逐步重构您的应用程序。您将能够在具有大量编写错误的旧代码的应用程序中编写新的干净代码。
结论适配器模式非常简单,但是当您想要获得干净的代码时,它可以让您的生活更轻松。有时很难发现它可以轻松解决您的问题。我希望你从这篇文章中学到至少一个关于它的新用法。即使您过去曾使用类似于适配器设计模式的东西而不知道这种模式存在,也应该注意这一点。
为什么?
因为它将使团队中的沟通变得更加容易。对你的同事说你已经使用适配器模式来实现票证A而不是解释说:“我创建了类X,它实现了现有的接口Y,在这个类中,我初始化了类Z,在类X的方法A的实现中,我从类Z调用方法B”。
原文地址:https://www.codeproject.com/Articles/1110588/Csharp-How-the-Adapter-Design-Pattern-Can-Make-You