【設計模式】適配器模式 Adapter Pattern

来源:https://www.cnblogs.com/vaiyanzi/archive/2018/08/08/9440685.html
-Advertisement-
Play Games

適配器模式在軟體開發界使用及其廣泛,在工業界,現實中也是屢見不鮮。比如手機充電器,筆記本充電器,廣播接收器,電視接收器等等。都是適配器。 適配器主要作用是讓本來不相容的兩個事物相容和諧的一起工作。比如, 通常我們使用的交流電都是220v,但是手機電池能夠承載的5v電壓,因此直接將我們使用的220v交 ...


適配器模式在軟體開發界使用及其廣泛,在工業界,現實中也是屢見不鮮。比如手機充電器,筆記本充電器,廣播接收器,電視接收器等等。都是適配器。

image 

適配器主要作用是讓本來不相容的兩個事物相容和諧的一起工作。比如, 通常我們使用的交流電都是220v,但是手機電池能夠承載的5v電壓,因此直接將我們使用的220v交流電直接接到手機上,手機肯定就壞,第二個作用是匹配交流電插座和手機充電介面不相容的問題,因此,一個充電器解決了電和手機存在的倆個問題(電壓和介面),並使其正常工作。

那麼在軟體開發過程中也會經常碰到這樣的問題,那就是系統都開發好了,突然有一天客戶說要接入其它系統的數據,但是當你看到介面接入文檔時發現兩邊的介面都對不上,數據結構定義的也不一樣,比如說,我們系統中有個定義的方法叫 GetUserByUserId(int userId) 返回的數據結構是這樣定義的:

image

public class User
{
    public int UserId { get; set; }
    public string UserName { get; set; }
    public int Age { get; set; }
    public string Address { get; set; }
    public string TelNumber{get;set;}
    public string MobileNumber { get; set; }
}

而對方系統介面也定義了一個方法叫 GetUserInfoById(int id)  但是返回的數據結構長這樣子:

image

public class UserInfo
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public Address Address { get; set; }
    public string TelphoneNumber { get; set; }
    public string CellphoneNumber { get; set; }
}
public class Address
{
    public Country Country { get; set; }
    public string City{get;set;}
    public  string Street{get;set;}
    public string Number { get; set; }
    public Location Location { get; set; }
    public string PostCode { get; set; }
       
}

public class Country
{
    public string Name { get; set; }
    public string Number { get; set; }
    public string Abbreviation { get; set; }
}
public class Location
{
    public long Longitude { get; set; }
    public long Latitude { get; set; }
}

 

 

那我們該怎麼對接這個外部系統的用戶到我們的系統中來呢? 這就是了我們要討論的適配器(Adapter) 模式了。

一、適配器模式的定義

適配器模式(Adapter Pattern):將一個介面轉換成客戶希望的另一個介面,使介面不相容的那些類可以一起工作,其別名為包裝器(Wrapper)。適配器模式既可以作為類結構型模式,也可以作為對象結構型模式。

二、適配器模式的結構圖

image 1、Target(目標抽象類):

目標抽象類定義客戶所需介面,可以是一個抽象類或介面,也可以是具體類。

2、Adapter(適配器類):

適配器可以調用另一個介面,作為一個轉換器,對Adaptee和Target進行適配,適配器類是適配器模式的核心,在對象適配器中,它通過繼承Target並關聯一個Adaptee對象使二者產生聯繫。在類機構中他直接繼承target介面和一個Adaptee類來實現。     

3、Adaptee(適配者類):

適配者即被適配的角色,它定義了一個已經存在的介面,這個介面需要適配,適配者類一般是一個具體類,包含了客戶希望使用的業務方法,在某些情況下可能沒有適配者類的源代碼。

三、適配器模式的經典實現

public abstract class Target
{
    public abstract void Request();
}
public class Adaptee
{
    public void specificRequest()
    {
        Console.WriteLine("I'm Adaptee method");
    }
}
public class Adapter : Target
{
    private Adaptee _adaptee;
    public Adapter(Adaptee adaptee)
    {
        _adaptee = adaptee;
    }
    public override void Request()
    {
        _adaptee.specificRequest();
    }
}

客戶端調用代碼:

static void Main(string[] args)
{
    Target target = new Adapter(new Adaptee());
    target.Request();

    Console.ReadKey();
}

結果輸出:

image

四、適配器模式實例

討論完適配器模式的概念後我們來使用適配器模式解決文中開頭提出來的問題, 怎麼將UserProvider 介面適配到IUserService介面(註意:這裡所說的介面是廣義的介面,而不是C#中用I開頭定義的介面),有了適配器模式現在就變得簡單了,IUserService 介面就是適配器模式的目標抽象類(Target), UserProvider 就是適配器模式的適配者類(Adaptee),我們新建一個適配器類UserAdapter (Adapter) 就可以讓它們工作了。結構圖如下:

image

對象結構型實現:

在UserPorvider類中實例化兩個UserInfo對象(模擬數據存儲在資料庫中),假設它就是要接入的數據。那麼代碼就是這樣子:

public class User
{
    public int UserId { get; set; }
    public string UserName { get; set; }
    public int Age { get; set; }
    public string Address { get; set; }
    public string TelNumber { get; set; }
    public string MobileNumber { get; set; }
}

public class UserInfo
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public Address Address { get; set; }
    public string TelphoneNumber { get; set; }
    public string CellphoneNumber { get; set; }
}
public class Address
{
    public Country Country { get; set; }
    public string City { get; set; }
    public string Street { get; set; }
    public string Number { get; set; }
    public Location Location { get; set; }
    public string PostCode { get; set; }

}

public class Country
{
    public string Name { get; set; }
    public string Number { get; set; }
    public string Abbreviation { get; set; }
}
public class Location
{
    public double Longitude { get; set; }
    public double Latitude { get; set; }
}

public class UserProvider
{
    private static IDictionary<int, UserInfo> innerDictionary = new Dictionary<int, UserInfo>();
    static UserProvider()
    {
        innerDictionary.Add(1, new UserInfo
        {
            FirstName = "Kevin",
            LastName = "Durnt",
            Age = 30,
            CellphoneNumber = "136xxxx1234",
            TelphoneNumber = "010-34567890",
            Id = 1,
            Address = new Address
            {
                City = "Xi'an",
                Number = "24",
                PostCode = "710000",
                Street = "Gao xin",
                Country = new Country
                {
                    Abbreviation = "zh-CN",
                    Name = "China",
                    Number = "018",
                },
                Location = new Location
                {
                    Latitude = 31.123456,
                    Longitude = 35.23456,
                }
            }
        });
        innerDictionary.Add(2, new UserInfo
        {
            FirstName = "Kobe",
            LastName = "Durnt",
            Age = 39,
            CellphoneNumber = "139xxxx1234",
            TelphoneNumber = "010-24567890",
            Id = 2,
            Address = new Address
            {
                City = "Xi'an",
                Number = "24",
                PostCode = "710000",
                Street = "Gao xin",
                Country = new Country
                {
                    Abbreviation = "zh-CN",
                    Name = "China",
                    Number = "018",
                },
                Location = new Location
                {
                    Latitude = 31.123456,
                    Longitude = 35.23456
                }
            }
        });
    }
    public UserInfo GetUserById(int id)
    {
        return innerDictionary[id];
    }
}

public interface IUserService
{
    User GetUserByUserId(int userId);
}
public class UserAdapter : IUserService
{
    private UserProvider _userProvider;
    public UserAdapter(UserProvider userProvider)
    {
        _userProvider = userProvider;
    }
    public User GetUserByUserId(int userId)
    {
        UserInfo userInfo = _userProvider.GetUserById(userId);

        User user = new User();
        user.UserId = userInfo.Id;
        user.UserName = string.Format("{0} {1}", userInfo.FirstName, userInfo.LastName);
        user.TelNumber = userInfo.TelphoneNumber;
        user.MobileNumber = userInfo.CellphoneNumber;
        user.Age = userInfo.Age;
        user.Address = string.Format("{0} {1}, {2},{3}, Location:{4}, {5}",
            userInfo.Address.Street,
            userInfo.Address.Number,
            userInfo.Address.Country.Name,
            userInfo.Address.PostCode,
            userInfo.Address.Location.Latitude,
            userInfo.Address.Location.Longitude);

        return user;
    }
}

客戶端調用:

static void Main(string[] args)
{
    IUserService target = new UserAdapter(new UserProvider());
    User user=target.GetUserByUserId(1);

    Console.WriteLine("UserId: " + user.UserId);
    Console.WriteLine("UserName: " + user.UserName);
    Console.WriteLine("Age: " + user.Age);
    Console.WriteLine("TelNumber: " + user.TelNumber);
    Console.WriteLine("MobileNumber: " + user.MobileNumber);
    Console.Write("Address: " + user.Address);

    Console.ReadKey();
}

輸出結果:

image 

反射+配置實現熱替換

為了達到靈活配置的目的,其實在很多時候,客戶端不需要知道第三方介面長什麼樣,因此,在適配器類裡面可以隱藏掉調用第三方代碼的細節,那麼對Adaptee的實例化直接放到Adapter里,因此,客戶端直接依賴高層抽象Target就可以了,這樣就可以隨時將Adaptee 替換掉, 並且我們可以使用配置+反射來達到這種動態替換的效果。下麵我們稍加修改UserAdapter類,並加一個配置來完成這個設想:

A、在UserAdapter構造里去掉類型為UserProvider 的參數,UserAdapter變成這樣了:

public class UserAdapter : IUserService
{
    private UserProvider _userProvider;
    public UserAdapter()
    {
        _userProvider = new UserProvider();
    }
    public User GetUserByUserId(int userId)
    {
        UserInfo userInfo = _userProvider.GetUserById(userId);

        User user = new User();
        user.UserId = userInfo.Id;
        user.UserName = string.Format("{0} {1}", userInfo.FirstName, userInfo.LastName);
        user.TelNumber = userInfo.TelphoneNumber;
        user.MobileNumber = userInfo.CellphoneNumber;
        user.Age = userInfo.Age;
        user.Address = string.Format("{0} {1}, {2},{3}, Location:{4}, {5}",
            userInfo.Address.Street,
            userInfo.Address.Number,
            userInfo.Address.Country.Name,
            userInfo.Address.PostCode,
            userInfo.Address.Location.Latitude,
            userInfo.Address.Location.Longitude);

        return user;
    }
}

B. 在App.config中加入如下配置:

<appSettings>
  <add key="Adapter" value="DesignPattern.Adapter.UserAdapter"/>
</appSettings>

C.在代碼中使用反射得到具體的Adapter 類,然後調用相應方法:

static void Main(string[] args)
{    
    var setting = ConfigurationSettings.AppSettings["Adapter"];
    Assembly assembly=Assembly.GetExecutingAssembly();
    IUserService target = assembly.CreateInstance(setting) as IUserService;
    
    User user=target.GetUserByUserId(1);

    Console.WriteLine("UserId: " + user.UserId);
    Console.WriteLine("UserName: " + user.UserName);
    Console.WriteLine("Age: " + user.Age);
    Console.WriteLine("TelNumber: " + user.TelNumber);
    Console.WriteLine("MobileNumber: " + user.MobileNumber);
    Console.Write("Address: " + user.Address);

    Console.ReadKey();
}

結果:

image

類結構實現

上面的adapter是對象結構型的實現。adapter 還可以是類結構型模式, 類適配器和對象適配器的不同之處就是適配器與適配者的關係不同。對象適配器,適配器與適配者之間是關聯關係,而類適配器,適配器與適配者之間是繼承關係。

下來我們使用類結構來實現上面的需求:

public class UserClassAdapter : UserProvider, IUserService
{      
    public User GetUserByUserId(int userId)
    {
        UserInfo userInfo =this.GetUserById(userId);

        User user = new User();
        user.UserId = userInfo.Id;
        user.UserName = string.Format("{0} {1}", userInfo.FirstName, userInfo.LastName);
        user.TelNumber = userInfo.TelphoneNumber;
        user.MobileNumber = userInfo.CellphoneNumber;
        user.Age = userInfo.Age;
        user.Address = string.Format("{0} {1}, {2},{3}, Location:{4}, {5}",
            userInfo.Address.Street,
            userInfo.Address.Number,
            userInfo.Address.Country.Name,
            userInfo.Address.PostCode,
            userInfo.Address.Location.Latitude,
            userInfo.Address.Location.Longitude);

        return user;
    }
}

僅僅只需要需要將UserAdapter和UserProvider的關係改成集成就可以了。 輸出結果和之前是一樣的。

在C#中由於類只能是單繼承關係, 一個類只能繼承自一個類,但可以繼承多個介面,如果Target角色是類,Adaptee也是類的話就不能使用類結構模式。

五、適配器模式的缺點

A. 類結構適配器和對象結構適配器共有的優點:

  1. 將目標類和適配者類解耦,通過引入一個適配器類來重用現有的適配者類,無須修改原有結構。

  2.  增加了類的透明性和復用性將具體的業務實現過程封裝在適配者類中,對於客戶端類而言是透明的,而且提高了適配者的復用性,同一個適配者類可以在多個不同的系統中復用。
  3. 靈活性和擴展性都非常好,通過使用配置文件,可以很方便地更換適配器,也可以在不修改原有代碼的基礎上增加新的適配器類,完全符合“開閉原則OCP”。

B.除了共有的優點外,類適配器還有如下優點:

  1. 由於適配器類是適配者類的子類,因此可以在適配器類中置換一些適配者的方法,使得適配器的靈活性更強。

C.除了共有的優點外,對象適配器還有如下優點:

  1. 一個對象適配器可以把多個不同的適配者適配到同一個目標
  2. 可以適配一個適配者的父類,由於適配器和適配者之間是關聯關係,根據“里氏代換原則LSP”,適配者的子類也可通過該適配器進行適配。

六、適配器模式的缺點

A.類適配器的缺點

  1. 由於C#不支持類的多繼承,一次最多只能適配一個適配者類,不能同時適配多個適配者。
  2. 適配者類不能為最終類,C#中不能為sealed類,這樣無法繼承了。
  3. 在C#語言中,類適配器模式中的目標抽象類只能為介面,不能為類,其使用有一定的局限性。其實這些都是單類繼承的語言特性造成的。

B.對象適配器的缺點

  1. 與類適配器模式相比,要在適配器中置換適配者類的某些方法比較麻煩。如果一定要置換掉適配者類的一個或多個方法,可以先做一個適配者類的子類,將適配者類的方法置換掉,然後再把適配者類的子類當做真正的適配者進行適配,實現過程較為複雜, 另一種方法是直接在適配器類中將相應的方法重新實現掉。

七、適配器模式的使用場景

  1. 系統需要使用一些現有的類,而這些類的介面(如方法名)不符合系統的需要,甚至沒有這些類的源代碼。
  2. 想創建一個可以重覆使用的類,用於與一些彼此之間沒有太大關聯的一些類,包括一些可能在將來引進的類一起工作。
  3. 在調用第三方介面是,和現有的系統模型不配是可以使用Adapter模式將模型轉化一直。

八、擴展-Default Adapter Parttern

在使用適配器模式的時候經常會碰到一類場景,就是已有的類的所有方法都都正常工作,但是只有那麼幾個方法需要調用第三方的幾個系統提供的API,這時我們使用繼承在適配器類里重新實現一遍工作量太大。這就要使用適配器模式的一個變體。這就是預設適配器,預設適配器上Target類是一個具體的類,實現大多數方法,甚至所有方法,但都是成虛方法,這樣在適配器中有選擇的重寫Target中的方法就可以了。這種變體在實踐中繼承使用。也是很有用的一種模式。

 

會不會存在一個多功能的雙向適配器呢(比如A系統對接B系統,同時B系統也要對接A系統)? 如果用C#該如何實現呢?


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

-Advertisement-
Play Games
更多相關文章
  • 前端獲取到的數據為 2018-08-17 16:37:50使用正則表達式 var time = obj.replace(/\-/g, "/");將格式改成 2018/08/17 16:37:50,解決! ...
  • Media Queries: How to target desktop, tablet and mobile? ...
  • setInterval() 方法 定義和用法 setInterval() 方法可按照指定的周期(以毫秒計)來調用函數或計算表達式。 setInterval() 方法會不停地調用函數,直到 clearInterval() 被調用或視窗被關閉。由 setInterval() 返回的 ID 值可用作 cl ...
  • 開篇還是引用呂振宇老師的那篇經典的文章《設計模式隨筆-蠟筆與毛筆的故事》。這個真是太經典了,沒有比這個例子能更好的闡明橋接模式了,這裡我就直接盜來用了。 現在市面上賣的蠟筆很多,各種型號,各種顏色種類繁多, 假如一盒蠟筆有24種顏色,那麼它能塗抹出24種不同的顏色來,蠟筆型號是固定的,如果想畫出各種 ...
  • 一、正向代理(Forward Proxy) 二、 反向代理(reverse proxy) 三、透明代理 四、nginx中的代理 ...
  • 我們都知道平常在使用SpringBoot和SpringCloud的時候,如果需要載入一兩個配置文件的話我們通常使用@Value("${屬性名稱}")註解去載入。但是如果配置文件屬性特別多的時候使用這種方式就顯得特別的不友好了。 比如說,我們要載入下方這個名為application.yml的配置文件。 ...
  • 我在知乎上看到一個形象解釋的例子: 個人理解:將具體業務和底層邏輯解耦的組件。 大致的效果是:需要利用服務的人(前端寫業務的),不需要知道底層邏輯(提供服務的)的具體實現,只要拿著中間件結果來用就好了。 舉個例子:我開了一家炸雞店(業務端),然而周邊有太多屠雞場(底層),為了成本我肯定想一個個比價, ...
  • 前幾天和同事聊天,同事說: “業務的服務(相對於我們基礎架構這邊的底層技術)在技術上就需要解決三個問題:分散式、通信和存儲。” 我回憶之前做業務的時光,覺得確實,再加上一個“服務治理”就差不多了。想想“服務設計要解決的問題”這個話題可以把之前靜兒寫的很多文章做一個歸納概括。今天做一個總結。 分散式 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...