【設計模式】適配器模式 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
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...