編碼中的Adapter,不僅是一種設計模式,更是一種架構理念與解決方案

来源:https://www.cnblogs.com/softwarearch/archive/2022/09/27/16732534.html
-Advertisement-
Play Games

在項目編碼中經常會遇到一些新的需求試圖復用已有的功能邏輯進行實現的場景,但是已有的邏輯又不能完全滿足新需求的要求,所以就會出現各種生搬硬套的操作。本篇文檔就一起來聊一聊如何藉助Adapter實現高效復用已有邏輯、讓代碼復用起來更加的得體與優雅。 ...


大家好,又見面了。

不知道下麵這玩意大家有沒有見過或者使用過?這是一個插座轉換器。我們都知道日常使用的是220v的交流電,而國外不同國家使用的電流電壓是不一樣的(比如日本使用的是110v)、且插座的介面樣式也是各不相同的(比如歐洲國家使用的是兩個小圓柱狀的插頭介面),如果我們到別的國家去旅行的時候,藉助這個插座轉換器,就可以讓我們的手機充電器在國外也能正常使用了。

當然,除了使用插座轉換器,還有個方法也可以讓我們出國之後正常的使用各種電子產品,那就是在當地重新買一套!顯然,這樣的成本就會非常巨大,明顯不符合我們 勤(nang)儉(zhong)持(xiu)家(se) 的特征。

看過我前面的文章的小伙伴應該知道,我的文章中一直反覆的在闡述自己的一個觀念,即“編碼源於生活” ,這裡依舊不例外。現實生活中的朴素哲學思維,在代碼世界中其實也無時無刻不在體現著。上面舉的例子,在我們的項目中又何嘗不是在頻繁上演此類情況呢?

我們先按照原有的業務邏輯實現了一套代碼,後來又來了個新的需求,如果重新開發一套需要投入大量的人力物力,所以首選方案就是去思考如何去復用已有的邏輯,以最小的代價將業務對接適配使用現有的邏輯去實現。

本篇文章中,我們就從這個“插座轉換器”來作為切入點,聊一聊在軟體系統中無處不在的“插座轉換器” —— 編碼中的適配器(Adapter)。選定以Adapter為題材進行闡述,並非是因為Adapter在技術實現上有多複雜,其實Adapter真正實現起來是非常簡單的,而且很多人有意或無意中其實也都在使用。更多的是想一起探討這種藉助Adapter來複用與相容已有邏輯的思路,以及如何利用Adapter來踐行OCP(開閉原則)的系統架構設計理念

Adapter的百媚千姿

新瓶舊酒:復用現成的實現邏輯

新瓶裝舊酒,在我們的系統裡面是一個很“節省”的操作,可以讓我們基於一個現有的能力快速的封裝提供出一個全新的業務功能,當然有的時候,系統現有的能力可能會某些方面無法完全滿足新業務的需求,需要做一些轉換適配處理。

舉個例子:

一個視頻網站,原先已有一個評論能力,用戶可以在視頻下方發表評論,然後評論內容以列表的形式展示在視頻下方頁面上。現在需要開發一個新功能,支持視頻發送彈幕能力,並將彈幕顯示在視頻播放畫面上。

從需求功能上來說,評論彈幕有很多相似之處。對後端而言,其處理邏輯與存儲數據結構幾乎都是相同的,只是在數據列表API實現的時候,需要過濾出評論信息展示到評論區、或者過濾出彈幕信息顯示到視頻畫面上。但是由於彈幕信息有一些特殊的屬性,又沒法直接完全使用現有的評論介面,比如彈幕可能會設置顯示在屏幕的位置、彈幕的字體顏色等等。這種情況下,我們可以通過構造個Adapter適配器的方式,在復用已有評論能力的基礎上,順便擴展實現需要的彈幕新特性。

如上圖所示,我們可以在Adapter中封裝擴展彈幕需要的新特性,然後對於數據存儲等邏輯則直接復用已有的評論功能處理邏輯,這樣就可以大大減少我們的開發工作量、後續也只需要維護一套主體代碼即可。

負重前行:相容歷史版本

和上面討論的場景相反,實際開發中還有一種非常常見的情況,就是原先的時候實現了一套業務邏輯,然後因為業務變化或者系統重構,需要對底層具體實現邏輯進行大改。這種情況下,為了保證此前調用該API的業務可以正常使用,通常有兩種思路:

  1. 保持原先的內容不動,完全另起爐竈全新實現一套,然後兩套邏輯並存,同時維護;

  2. 按照新的邏輯去實現,並將原先的對外API適配轉換對接使用新邏輯實現。

顯然,從成本與可維護性層面考慮,思路2更為可行、更加經濟

對比我們文首舉的那個“插頭轉換器”的例子,我們可以把圖中V1版本業務邏輯當做我們國內的手機充電插頭,而圖中綠色部分的V2新版本依賴邏輯,則是歐洲地區的圓孔牆面插座,那麼如何讓國標的扁口插頭能用上歐標的圓孔插座呢?關鍵就是那個插頭轉換器(Adapter)。

另類心機:屏蔽開源協議傳染

大家可以回想下,曾經是否也有過從github上“借鑒”一些代碼放進了自己的項目中,然後簡單修改為符合自己訴求的邏輯,便當做是自研代碼去正常使用了?不知道你是否有關註過你所拷貝的代碼所對應的開源協議呢?要小心啦、這個看似平常的操作,也許會給項目埋下致命隱患

為什麼說的這麼危言聳聽呢?因為有一些不太友好的開源協議(比如GPL協議),會要求使用了其代碼的項目如果商用就必須要開源其全部源碼!而對於很多軟體公司而言,源碼便是公司的核心資產,是公司最為核心的競爭力,將源碼開源無異於是要了老闆和公司的命。也許有人會對此很不屑,大家都這麼乾,似乎並沒有發現有人來追責呢?有個詞叫做“樹大招風”,只要你的產品做的夠大,就一定會被盯上 —— 你品,你細品。在當前知識版權保護越來越強的情況下,我們還是應該關註並提前做好應對這種危機出現的可能,避免埋下隱患。

這種情況下,可以基於Adapter的機制,實現棄卒保車的效果。即構建一個適配層,然後僅將適配層進行開源,而核心的模塊代碼中,則通過介面調用的方式使用適配層即可,這樣避免了核心模塊代碼被開源協議傳染。由於核心模塊中並沒有集成被二次改動後的開源源碼,所以也不具有開放源碼的義務、而Adapter層沒有任何核心業務邏輯,即使開源對公司、對項目也沒有影響。

基於Adapter適配層的方式來切斷開源協議傳染的成功實踐,最典型的莫過於Android項目(AOSP)了。因為AOSP是基於Linux kernel內核進行構建的,而Linux Kernel使用的是GPL協議,那麼按照要求,AOSP也需要開源其源碼。但是問題來了,如果AOSP開源源碼了,勢必導致所有基於Android定製的各個硬體廠商底層的設備驅動相關的代碼也都要全部開源,顯然不會有公司願意這麼乾。

為了讓各個公司可以放心的基於Android去開發自己的產品,AOSP將自己的協議搞成了Apache開源協議,這樣對產商而言就非常友好了,無需將自己的核心源碼開源。那麼Google是如何做到將本來需要以GPL協議開源的AOSP給變為使用Apache協議開源的呢?其實就是做了一個Adapter —— 也即HALHardware Abstract Layer,硬體抽象層)。

Adapter是一種理念

關於編碼中的Adapter,常規的文檔或者資料中,往往都是指的狹義上的適配器,也就是代碼class類維度的Adapter

我們跳出純粹的編碼層面,站到全局系統架構視角去審視的時候,其實Adapter在系統架構與編碼設計中是一個比較寬泛的概念。我個人更願意Adapter看做是一種問題解決的思想、一種方案設計的理念

根據要解決的問題level範圍的不同,Adapter對應的粒度與呈現形態也會有差異。

服務型Adapter

如果是在一個分散式微服務系統中,消息推送能力可以預見的會提供給很多不同的服務節點去調用,則可以將消息推送能力也封裝為一個對外微服務,業務通過RPC或者HTTP等方式進行遠程調用。

這種是一種相對High Level的Adapter抽象使用(但抽象為服務獨立部署後,其實也不僅僅是個Adapter了),廣泛的應用於系統架構層面,是解決系統功能復用、業務解耦的一種有效手段。

在我此前的一篇文章中,介紹了一個構建通用線上文檔預覽服務的實際案例,裡面對“預覽編輯服務”的定位就是一個典型的服務型Adapter,如下圖所示。通過預覽編輯服務這個Adapter,將文檔預覽能力所涉及的後端對接OnlyOffice或者對接kkFileView等細節邏輯給屏蔽掉,業務服務通過Adapter進行調用,大大簡化了業務的使用複雜度,也保持了業務模塊與文檔預覽服務內部模塊之間的耦合。

服務型Adapter著眼解決的是系統進程層面的適配與統一封裝,自身既是一個Adapter,又是一個獨立的服務,封裝內部細節差異化的實現,保證其它進程服務相對簡單的調用邏輯。

依賴庫型Adapter

在一些中小型項目中,會有若幹個業務模塊中會用到消息發送的能力,但是整體體量與業務規劃層面而言,卻也無需單獨部署一個專門的消息推送服務進程,這種情況下,可以將其封裝為一個依賴庫,比如JAVA中的一個jar包,或者C++中的一個so庫文件,亦或是C#中的dll庫文件。這樣各個業務模塊可以集成此庫文件,直接進行API調用即可。

此種類型的Adapter實現,在很多的框架中非常常見。比如在JAVA中的SpringBoot中的日誌框架,底層可以選擇是使用logback,也可以選擇切換到log4j

代碼類Adapter

在單個項目模塊中,我們為了保持業務邏輯的清晰與獨立,也會通過Adapter類的方式,來解耦具體的業務邏輯。比如這裡的消息推送服務,如果僅當前模塊需要使用,則可以創建一個獨立的Adapter類,提供介面供其他類調用,在Adapter類中完成具體邏輯的封裝實現。

還是以前面舉的告警通知消息發送的例子來說明,使用Adapter方式隔離消息通道與業務邏輯的實現UML圖如下:

代碼類的Adapter在實際項目中使用的場景非常的廣泛,是用於屏蔽代碼底層差異化邏輯的不二選擇。在總結各種實際使用場景與優秀實踐的基礎上,演進為23種設計模式之一的適配器模式

下麵我們一起聊一聊適配器模式。

Adapter是一種設計模式

所謂設計模式,便是將常規代碼編碼中常遇到的一些場景的處理方式進行了總結與抽象,固化成一個優秀實踐範例模板,使其整體實現更符合設計原則的要求。也就是說:設計模式並非是憑空捏造的,其實就是來源於常規的編碼實踐總結

按照通俗意義上對代碼設計模式的理解,適配器模式也可以分為2種形式,即類適配器模式對象適配器模式

下麵分別闡述下。

類適配器模式

類適配器模式整體非常的簡單,涉及的角色也很少。類適配器模式中,Adapter與被適配的Adaptee之間,通過繼承的方式來實現,其UML圖如下所示。

主要角色說明如下:

  • Adaptee:原始被適配的類,即不符合訴求需要由Adapter進行適配的原始介面

  • Adapter:適配器本身,也是類適配器模式的核心,用於將Adaptee適配為目標的Target。

  • Target:期待獲取到的目標結果。也即Adaptee經由Adapter適配後得到的統一的目標介面

還是以前面的告警通知發送的場景為例,我們按照聚合的方式,演示下對應的Adapter實現邏輯。

@Service
public class MsgSendAdapter extends SmsSender implements IMsgSender {
    @Override
    public void send(AlarmDetail detail) {
        // detail轉SMS請求體的邏輯
        SmsContent sms = convertToSmsContent(detail);
        super.sendSms(sms);
    }
}

上述代碼中,MsgSendAdapter繼承了SmsSender類並且實現了IMsgSender介面,將父類SmsSender中的sendSms介面轉換為了IMsgSender介面提供的目標介面send(),業務可以調用IMsgSender.send()介面,實現對SmsSender.sendSms()邏輯的調用。

對象適配器模式

對象適配器模式與類適配器模式類似,區別點在於Adapter與被適配的Adaptee之間非繼承關係,而是對象組合關係。其UML圖如下:

按照對象適配器的設計思路,其代碼可以如下方式來實現:

@Service
public class MsgSendAdapter implements IMsgSender {
    @Autowired
    private SmsSender smsSender;

    @Override
    public void send(AlarmDetail detail) {
        // detail轉SMS請求體的邏輯
        SmsContent sms = convertToSmsContent(detail);
        smsSender.sendSms(sms);
    }
}

上述代碼中,MsgSendAdapter類中以組合的方式持有SmsSender對象(Adaptee),相比較類適配器的繼承邏輯,靈活性更高,所以對象適配器要更加的靈活與實用(其實在架構設計領域也一直有一種觀點叫“組合優於繼承”,因為繼承打破了面向對象的封裝)。

總結回顧

好啦,關於Adapter相關的討論與個人的理解,這裡就給大家分享到這裡。Adapter不僅是一個簡單的具體實現類,也不僅僅是23種設計模式之一,更是一種問題解決的思想、一種方案設計的理念。

關於本篇文檔中的內容,不知道屏幕前的各位小伙伴是否在項目中有使用過Adapter或者Adapter模式來幫助自己實現某些功能呢?是否對Adapter還有一些別的獨到見解呢?歡迎評論區留言一起交流下。

我是悟道,聊技術、又不僅僅聊技術~

如果覺得有用,請點贊 + 關註讓我感受到您的支持。也可以關註下我的公眾號【架構悟道】,獲取更及時的更新。

期待與你一起探討,一起成長為更好的自己。

本文來自博客園,作者:架構悟道,歡迎關註公眾號[架構悟道]持續獲取更多乾貨,轉載請註明原文鏈接:https://www.cnblogs.com/softwarearch/p/16732534.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • <div class="fuzhiWarp"> <div class="copydiv">這裡是DIV中的文本</div> <button type="button" class="fuzhibtn btn-default" data-clipboard-action="copy" data-cli ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一.typescript 高階類型 Exclude 和 Extract Exclude<T, U> TypeScript 2.8 中增加了 Exclude 類型,該如何理解這個高級類型的定義呢? type Exclude<T, U> = ...
  • 剛完成一些前端項目的開發,騰出精力來總結一些前端開發的技術點,以及繼續完善基於SqlSugar的開發框架循序漸進介紹的系列文章,本篇隨筆主要介紹一下基於Vue3+TypeScript的全局對象的註入和使用。我們知道在Vue2中全局註入一個全局變數使用protoType的方式,很方便的就註入了,而Vu... ...
  • 1 CMD 規範介紹 CMD: Common Module Definition, 通用模塊定義。與 AMD 規範類似,也是用於瀏覽器端,非同步載入模塊,一個文件就是一個模塊,當模塊使用時才會載入執行。其語法與 AMD 規範很類似。 1.1 定義模塊 定義模塊使用 define 函數: define( ...
  • uniapp webview h5 通信 window.postMessage 方式 父頁面 <template> <view> <!-- <web-view :webview-styles="webviewStyles" src="https://uniapp.dcloud.io/static/w ...
  • 模塊 HTML 網頁中,瀏覽器通過<script>標簽載入 JavaScript 腳本。 <!-- 頁面內嵌的腳本 --> <script type="application/javascript"> // module code </script> <!-- 外部腳本 --> <script ty ...
  • 命令模式(Command Pattern)是一種數據驅動的設計模式,它屬於行為型模式。請求以命令的形式包裹在對象中,並傳給調用對象。調用對象尋找可以處理該命令的合適的對象,並把該命令傳給相應的對象,該對象執行命令。 ...
  • 橋接模式是一種在日常開發中不是特別常用的設計模式,主要是因為上手難度較大,但是對於理解面向對象設計有非常大的幫助。 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...