C#:介面卡設計模式如何讓您的生活更輕鬆
目錄
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