意圖
把一個類的接口變換成客戶端所期待的另一種接口,從而使原本接口不匹配而無法在一起工作的兩個類能夠在一起工作。
場景
假設網絡游戲的客戶端程序分兩部分。一部分是和服務端通訊的大廳部分,大廳部分提供的功能有道具購買、讀取房間列表、創建房間以及啟動游戲程序。另一部分就是游戲程序了,游戲程序和大廳程序雖然屬於一個客戶端,但是由不同的公司在進行開發。游戲大廳通過實現約定的接口和游戲程序進行通訊。
一開始的設計就是,大廳程序是基於接口方式調用游戲程序啟動游戲場景方法的。在大廳程序開發接近完成的時候,公司決定和另外一家游戲公司合作,因此希望把大廳程序能適用另一個游戲。而這個新游戲的遵循的是另一套接口。是不是可以避免修改原先調用方法來啟動場景呢?或許你會說,既然只有一個方法修改,那麼修改一下也無妨,我們假設大廳程序和游戲程序之間有100個接口,其中的大部分都有修改呢?因為游戲程序接口的修改,大廳程序可能要修改不止100個地方。這樣接口的意義何在呢?
此時可以考慮使用Adapter模式來適配這種接口的不匹配情況。
示例代碼
using System;
using System.Collections.Generic;
using System.Text;
namespace AdapterExample
{
class Program
{
static void Main(string[] args)
{
Lobby lobby = new Lobby();
lobby.CreateRoom("HalfPaper");
lobby.StartGame();
}
}
interface IGame
{
void StartScene(string sceneName);
void EnterPlayer(string playerName);
}
class Lobby
{
private string sceneName;
public void CreateRoom(string sceneName)
{
this.sceneName = sceneName;
}
public void StartGame()
{
IGame game = new GameAdapter();
game.StartScene(sceneName);
game.EnterPlayer("yzhu");
}
}
class Game
{
public void LoadScene(string sceneName, string token)
{
if (token == "Abcd1234")
Console.WriteLine("Loading " + sceneName + "...");
else
Console.WriteLine("Invalid token!");
}
public void EnterPlayer(int playerID)
{
Console.WriteLine("player:" + playerID + " entered");
}
}
class GameAdapter : IGame
{
private Game game = new Game();
public void StartScene(string sceneName)
{
game.LoadScene(sceneName, "Abcd1234");
}
public void EnterPlayer(string playerName)
{
game.EnterPlayer(GetPlayerIDByPlayerName(playerName));
}
private int GetPlayerIDByPlayerName(string playerName)
{
return 12345;
}
}
}
代碼執行結果如下圖:
代碼說明
l 可以看到,原先的接口中,啟動游戲場景只需要一個參數,就是游戲場景名,而進入新的玩家需要提供玩家ID(新游戲都使用玩家ID而不使用玩家賬戶名)。
l IGame接口就是適配器模式中的目標角色,這是客戶所期待的接口。也是針對老的游戲程序所遵循的接口。
l Lobby類相當於調用方或者客戶,它原先的代碼可能是如下的:
IGame game = new Game();
但是由於接口的改變,現在不能直接實例化游戲類,只能實例化適配器類型。雖然還是需要改動,但是這個改動是很小的,而且完全可以通過用動態加載程序集來消除這種改動。
l GameAdapter類是適配器角色,它是適配器模式的核心,用於把源接口轉變為目標接口。在這裡,我們看到,它實現目標接口。
l Game類型是源角色,或者說是需要適配的對象。或許它也遵循了另外一套接口,不過我們不是很關心這個,因此代碼中也沒有體現。
l 使用了適配器模式後,客戶端代碼沒有做什麼修改。客戶端代碼老老實實的依賴接口,它並沒有錯,如果因此依賴對象的修改而需要大幅度修改就很無辜了,我們在適配器中把本來沒有關聯的兩個接口適配在了一起。我們可以看到,適配器做的不僅僅是換一換方法名,如果源角色和目標角色的差異非常大,那麼適配器需要做很多工作。
何時采用
l 從代碼角度來說, 如果需要調用的類所遵循的接口並不符合系統的要求或者說並不是客戶所期望的,那麼可以考慮使用適配器。
l 從應用角度來說, 如果因為產品遷移、合作模塊的變動,導致雙方一致的接口產生了不一致,或者是希望在兩個關聯不大的類型之間建立一種關系的情況下可以考慮適配器模式。
實現要點
l 適配器模式是否能成功運用的關鍵在於代碼本身是否是基於接口編程的,如果不是的話,那麼適配器無能為力。
l 適配器模式的實現很簡單,基本的思想就是適配器一定是遵循目標接口的。
l 適配器模式的變化比較多,可以通過繼承和組合方式進行適配,適配器可以是一組適配器產品,適配器也可以是抽象類型。
l 適配器模式和Facade的區別是,前者是遵循接口的,後者可以是不遵循接口的,比較靈活。
l 適配器模式和Proxy的區別是,前者是為對象提供不同的接口,或者為對象提供相同接口,並且前者有一點後補的味道,後者是在設計時就會運用的。
注意事項
l 在對兩個無關類進行適配的時候考慮一下適配的代價,一個非常龐大的適配器可能會對系統性能有影響。