適配器模式在軟體開發界使用及其廣泛,在工業界,現實中也是屢見不鮮。比如手機充電器,筆記本充電器,廣播接收器,電視接收器等等。都是適配器。 適配器主要作用是讓本來不相容的兩個事物相容和諧的一起工作。比如, 通常我們使用的交流電都是220v,但是手機電池能夠承載的5v電壓,因此直接將我們使用的220v交 ...
適配器模式在軟體開發界使用及其廣泛,在工業界,現實中也是屢見不鮮。比如手機充電器,筆記本充電器,廣播接收器,電視接收器等等。都是適配器。
適配器主要作用是讓本來不相容的兩個事物相容和諧的一起工作。比如, 通常我們使用的交流電都是220v,但是手機電池能夠承載的5v電壓,因此直接將我們使用的220v交流電直接接到手機上,手機肯定就壞,第二個作用是匹配交流電插座和手機充電介面不相容的問題,因此,一個充電器解決了電和手機存在的倆個問題(電壓和介面),並使其正常工作。
那麼在軟體開發過程中也會經常碰到這樣的問題,那就是系統都開發好了,突然有一天客戶說要接入其它系統的數據,但是當你看到介面接入文檔時發現兩邊的介面都對不上,數據結構定義的也不一樣,比如說,我們系統中有個定義的方法叫 GetUserByUserId(int userId) 返回的數據結構是這樣定義的:
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) 但是返回的數據結構長這樣子:
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)。適配器模式既可以作為類結構型模式,也可以作為對象結構型模式。
二、適配器模式的結構圖
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(); }
結果輸出:
四、適配器模式實例
討論完適配器模式的概念後我們來使用適配器模式解決文中開頭提出來的問題, 怎麼將UserProvider 介面適配到IUserService介面(註意:這裡所說的介面是廣義的介面,而不是C#中用I開頭定義的介面),有了適配器模式現在就變得簡單了,IUserService 介面就是適配器模式的目標抽象類(Target), UserProvider 就是適配器模式的適配者類(Adaptee),我們新建一個適配器類UserAdapter (Adapter) 就可以讓它們工作了。結構圖如下:
對象結構型實現:
在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(); }
輸出結果:
反射+配置實現熱替換
為了達到靈活配置的目的,其實在很多時候,客戶端不需要知道第三方介面長什麼樣,因此,在適配器類裡面可以隱藏掉調用第三方代碼的細節,那麼對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(); }
結果:
類結構實現
上面的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. 類結構適配器和對象結構適配器共有的優點:
-
將目標類和適配者類解耦,通過引入一個適配器類來重用現有的適配者類,無須修改原有結構。
- 增加了類的透明性和復用性,將具體的業務實現過程封裝在適配者類中,對於客戶端類而言是透明的,而且提高了適配者的復用性,同一個適配者類可以在多個不同的系統中復用。
- 靈活性和擴展性都非常好,通過使用配置文件,可以很方便地更換適配器,也可以在不修改原有代碼的基礎上增加新的適配器類,完全符合“開閉原則OCP”。
B.除了共有的優點外,類適配器還有如下優點:
- 由於適配器類是適配者類的子類,因此可以在適配器類中置換一些適配者的方法,使得適配器的靈活性更強。
C.除了共有的優點外,對象適配器還有如下優點:
- 一個對象適配器可以把多個不同的適配者適配到同一個目標。
- 可以適配一個適配者的父類,由於適配器和適配者之間是關聯關係,根據“里氏代換原則LSP”,適配者的子類也可通過該適配器進行適配。
六、適配器模式的缺點
A.類適配器的缺點
- 由於C#不支持類的多繼承,一次最多只能適配一個適配者類,不能同時適配多個適配者。
- 適配者類不能為最終類,C#中不能為sealed類,這樣無法繼承了。
- 在C#語言中,類適配器模式中的目標抽象類只能為介面,不能為類,其使用有一定的局限性。其實這些都是單類繼承的語言特性造成的。
B.對象適配器的缺點
- 與類適配器模式相比,要在適配器中置換適配者類的某些方法比較麻煩。如果一定要置換掉適配者類的一個或多個方法,可以先做一個適配者類的子類,將適配者類的方法置換掉,然後再把適配者類的子類當做真正的適配者進行適配,實現過程較為複雜, 另一種方法是直接在適配器類中將相應的方法重新實現掉。
七、適配器模式的使用場景
- 系統需要使用一些現有的類,而這些類的介面(如方法名)不符合系統的需要,甚至沒有這些類的源代碼。
- 想創建一個可以重覆使用的類,用於與一些彼此之間沒有太大關聯的一些類,包括一些可能在將來引進的類一起工作。
- 在調用第三方介面是,和現有的系統模型不配是可以使用Adapter模式將模型轉化一直。
八、擴展-Default Adapter Parttern
在使用適配器模式的時候經常會碰到一類場景,就是已有的類的所有方法都都正常工作,但是只有那麼幾個方法需要調用第三方的幾個系統提供的API,這時我們使用繼承在適配器類里重新實現一遍工作量太大。這就要使用適配器模式的一個變體。這就是預設適配器,預設適配器上Target類是一個具體的類,實現大多數方法,甚至所有方法,但都是成虛方法,這樣在適配器中有選擇的重寫Target中的方法就可以了。這種變體在實踐中繼承使用。也是很有用的一種模式。
會不會存在一個多功能的雙向適配器呢(比如A系統對接B系統,同時B系統也要對接A系統)? 如果用C#該如何實現呢?