【結構型模式八】適配器(Adapter)

1 場景問題#

1.1 裝配電腦的例子##

  1. 舊的硬盤和電源

小李有一臺老的臺式電腦,硬盤實在是太小了,僅僅40GB,但是除了這個問題外,整機性能還不錯,廢棄不用太可惜了,于是決定去加裝一塊新的硬盤。

在裝機公司為小李的電腦加裝新硬盤的時候,小李也在邊上觀看,順便了解點硬件知識。很快的,裝機人員把兩塊硬盤都安裝好了,細心的小李發現,這兩塊硬盤的連接方式是不一樣的。

經過裝機人員的耐心講解,小李搞清楚了它們的不同。以前的硬盤是串口的,如圖4.1,電腦電源如圖4.2,那么連接電源的時候是直接連接。

舊的硬盤和電腦電源
  1. 加入新的硬盤

但是現在的新硬盤是并口的,如圖4.3,電源的輸出口無法直接連接到新的硬盤上了,于是就有了轉接線,一邊和電源的輸出口連接,一邊和新的硬盤電源輸入口連接,解決了電源輸出接口和硬盤輸入接口不匹配的問題,如圖4.4:

新的硬盤是并口的
  1. 有何問題

如果把上面的問題抽象一下,用對象來描述,那就是:有一個電源類和舊的硬盤類配合工作得很好,現在又有了一個新的硬盤類,現在想讓新的硬盤類和電源類也配合使用,但是發現它們的接口無法匹配,問題就產生了:如何讓原有的電源類的接口能夠適應新的硬盤類的電源接口的需要呢?

  1. 如何解決

解決方法是采用一個轉接線類,轉接線可以把電源的接口適配成為新的硬盤所需要的接口,那么這個轉接線類就類似本章的主角——適配器。

1.2 同時支持數據庫和文件的日志管理##

看了上面這個例子,估計對適配器模式有一點感覺了。這是個在生活中常見的例子,類似的例子很多,比如:各種管道的轉接頭、不同制式的插座等等。但是這種例子只能幫助大家理解適配器模式的功能,跟實際的應用系統開發總是有那么些差距,會感覺到好像是理解了模式的功能,但是一到真實的系統開發中,就不知道如何使用這個模式了,有些隔靴搔癢的感覺。因此,下面還是以實際系統中的例子來講述,以幫助大家真正理解和應用適配器模式。

考慮一個記錄日志的應用,由于用戶對日志記錄的要求很高,使得開發人員不能簡單的采用一些已有的日志工具或日志框架來滿足用戶的要求,而需要按照用戶的要求重新開發新的日志管理系統。當然這里不可能完全按照實際系統那樣去完整實現,只是抽取跟適配器模式相關的部分來講述。

  1. 日志管理第一版

在第一版的時候,用戶要求日志以文件的形式記錄。開發人員遵照用戶的要求,對日志文件的存取實現如下。

先簡單定義日志對象,也就是描述日志的對象模型,由于這個對象需要被寫入文件中,因此這個對象需要序列化,示例代碼如下:

/**
   * 日志數據對象
   */
public class LogModel implements Serializable  {
    /**
     * 日志編號
     */
    private String logId;
    /**
     * 操作人員
     */
    private String operateUser;
    /**
     * 操作時間,以yyyy-MM-dd HH:mm:ss的格式記錄
     */
    private String operateTime;
    /**
     * 日志內容
     */
    private String logContent;

    public String getLogId() {
        return logId;
    }
    public void setLogId(String logId) {
        this.logId = logId;
    }
    public String getOperateUser() {
        return operateUser;
    }
    public void setOperateUser(String operateUser) {
        this.operateUser = operateUser;
    }
    public String getOperateTime() {
        return operateTime;
    }
    public void setOperateTime(String operateTime) {
        this.operateTime = operateTime;
    }
    public String getLogContent() {
        return logContent;
    }
    public void setLogContent(String logContent) {
        this.logContent = logContent;
    }
    public String toString() {
        return "logId="+logId+",operateUser="+operateUser+",operateTime="+operateTime+",logContent="+logContent;
    }
}

接下來定義一個操作日志文件的接口,示例代碼如下:

/**
   * 日志文件操作接口
   */
public interface LogFileOperateApi {
    /**
     * 讀取日志文件,從文件里面獲取存儲的日志列表對象
     * @return 存儲的日志列表對象
     */
    public List<LogModel> readLogFile();
    /**
     * 寫日志文件,把日志列表寫出到日志文件中去
     * @param list 要寫到日志文件的日志列表
     */
    public void writeLogFile(List<LogModel> list);
}

實現日志文件的存取,現在的實現也很簡單,就是讀寫文件,示例代碼如下:

/**
   * 實現對日志文件的操作
   */
public class LogFileOperate implements LogFileOperateApi{
    /**
     * 日志文件的路徑和文件名稱,默認是當前項目的根下的AdapterLog.log
     */
    private String logFilePathName = "AdapterLog.log";
    /**
     * 構造方法,傳入文件的路徑和名稱
     * @param logFilePathName 文件的路徑和名稱
     */
    public LogFileOperate(String logFilePathName) {
        //先判斷是否傳入了文件的路徑和名稱,如果是,
        //就重新設置操作的日志文件的路徑和名稱
        if(logFilePathName!=null && logFilePathName.trim().length()>0){
            this.logFilePathName = logFilePathName;
        }
    }
    public  List<LogModel> readLogFile() {
        List<LogModel> list = null;
        ObjectInputStream oin = null;
        try {
            File f = new File(logFilePathName);
            if(f.exists()) {
                oin = new ObjectInputStream(new BufferedInputStream(new FileInputStream(f)));
                list = (List<LogModel>)oin.readObject();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if(oin!=null) {
                    oin.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return list;
    }
    public void writeLogFile(List<LogModel> list){
        File f = new File(logFilePathName);
        ObjectOutputStream oout = null;
        try {
            oout = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(f)));
            oout.writeObject(list);        
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                oout.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

寫個客戶端來測試一下,看看好用不,示例代碼如下:

public class Client {
    public static void main(String[] args) {
        //準備日志內容,也就是測試的數據
        LogModel lm1 = new LogModel();
        lm1.setLogId("001");
        lm1.setOperateUser("admin");
        lm1.setOperateTime("2010-03-0210:08:18");
        lm1.setLogContent("這是一個測試");
    
        List<LogModel> list = new ArrayList<LogModel>();
        list.add(lm1);
        //創建操作日志文件的對象
        LogFileOperateApi api = new LogFileOperate("");
        //保存日志文件
        api.writeLogFile(list);
    
        //讀取日志文件的內容
        List<LogModel> readLog = api.readLogFile();
        System.out.println("readLog="+readLog);
    }
}

至此就簡單的實現了用戶的要求,把日志保存到文件中,并能從文件中把日志內容讀取出來,進行管理。看上去很容易,對吧,別慌,接著來。

  1. 日志管理第二版

用戶使用日志管理的第一版一段時間過后,開始考慮升級系統,決定要采用數據庫來管理日志,很快,按照數據庫的日志管理也實現出來了,并定義了日志管理的操作接口,主要是針對日志的增刪改查方法,接口的示例代碼如下:

/**
   * 定義操作日志的應用接口,為了示例的簡單,只是簡單的定義了增刪改查的方法
   */
public interface LogDbOperateApi {
    /**
     * 新增日志
     * @param lm 需要新增的日志對象
     */
    public void createLog(LogModel lm);
    /**
     * 修改日志
     * @param lm 需要修改的日志對象
     */
    public void updateLog(LogModel lm);
    /**
     * 刪除日志
     * @param lm 需要刪除的日志對象
     */
    public void removeLog(LogModel lm);
    /**
     * 獲取所有的日志
     * @return 所有的日志對象
     */
    public List<LogModel> getAllLog();
}

對于使用數據庫來保存日志的實現,這里就不去涉及了,反正知道有這么一個實現就可以了。

客戶提出了新的要求,能不能讓日志管理的第二版,實現同時支持數據庫存儲和文件存儲兩種方式?

1.3 有何問題##

有朋友可能會想,這有什么困難的呢,兩種實現方式不是都已經實現了的嗎,合并起來不就可以了?

問題就在于,現在的業務是使用的第二版的接口,直接使用第二版新加入的實現是沒有問題的,第二版新加入了保存日志到數據庫中;但是對于已有的實現方式,也就是在第一版中采用的文件存儲的方式,它的操作接口和第二版不一樣,這就導致現在的客戶端,無法以同樣的方式來直接使用第一版的實現,如下圖4.5所示:

無法兼容第一版的接口示意圖

這就意味著,要想同時支持文件和數據庫存儲兩種方式,需要再額外的做一些工作,才可以讓第一版的實現適應新的業務的需要。

可能有朋友會想,干脆按照第二版的接口要求重新實現一個文件操作的對象不就可以了,這樣確實可以,但是何必要重新做已經完成的功能呢?應該要想辦法復用,而不是重新實現

一種很容易想到的方式是直接修改已有的第一版的代碼。這種方式是不太好的,如果直接修改了第一版的代碼,那么可能會導致其它依賴于這些實現的應用不能正常運行,再說,有可能第一版和第二版的開發公司是不一樣的,在第二版實現的時候,根本拿不到第一版的源代碼。

那么該如何來實現呢?

2 解決方案#

2.1 適配器模式來解決##

用來解決上述問題的一個合理的解決方案就是適配器模式。那么什么是適配器模式呢?

  1. 適配器模式定義
適配器模式定義
  1. 應用適配器模式來解決的思路

仔細分析上面的問題,問題的根源在于接口的不兼容,功能是基本實現了的,也就是說,只要想辦法讓兩邊的接口匹配起來,就可以復用第一版的功能了。

按照適配器模式的實現方式,可以定義一個類來實現第二版的接口,然后在內部實現的時候,轉調第一版已經實現了的功能,這樣就可以通過對象組合的方式,既復用了第一版已有的功能,同時又在接口上滿足了第二版調用的要求。完成上述工作的這個類就是適配器。

2.2 模式結構和說明##

適配器模式的結構如圖所示:

適配器模式的結構圖

Client:客戶端,調用自己需要的領域接口Target。

Target:定義客戶端需要的跟特定領域相關的接口。

Adaptee:已經存在的接口,通常能滿足客戶端的功能要求,但是接口與客戶端要求的特定領域接口不一致,需要被適配

Adapter:適配器,把Adaptee適配成為Client需要的Target。

2.3 適配器模式示例代碼##

  1. 先看看Target接口的定義,示例代碼如下:
/**
   * 定義客戶端使用的接口,與特定領域相關
   */
public interface Target {
    /**
     * 示意方法,客戶端請求處理的方法
     */
    public void request();
}
  1. 再看看需要被適配的對象定義,示例代碼如下:
/**
   * 已經存在的接口,這個接口需要被適配
   */
public class Adaptee {
    /**
     * 示意方法,原本已經存在,已經實現的方法
     */
    public void specificRequest() {
        //具體的功能處理
    }
}
  1. 再看看適配器對象的基本實現,示例代碼如下:
/**
   * 適配器
   */
public class Adapter implements Target {
    /**
     * 持有需要被適配的接口對象
     */
    private Adaptee adaptee;
    /**
     * 構造方法,傳入需要被適配的對象
     * @param adaptee 需要被適配的對象
     */
    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }
    public void request() {
        //可能轉調已經實現了的方法,進行適配
        adaptee.specificRequest();
    }
}
  1. 再來看看使用適配器的客戶端,示例代碼如下:
/**
   * 使用適配器的客戶端
   */
public class Client {  
    public static void main(String[] args) {
        // 創建需被適配的對象
        Adaptee adaptee = new Adaptee();
        // 創建客戶端需要調用的接口對象
        Target target = new Adapter(adaptee);
        // 請求處理
        target.request();
    }
}

2.4 使用適配器模式來實現示例##

要使用適配器模式來實現示例,關鍵就是要實現這個適配器對象,它需要實現第二版的接口,但是在內部實現的時候,需要調用第一版已經實現的功能。也就是說,第二版的接口就相當于適配器模式中的Target接口,而第一版已有的實現就相當于適配器模式中的Adaptee對象

  1. 把這個適配器簡單的實現出來,示意一下,示例代碼如下:
/**
   * 適配器對象,把記錄日志到文件的功能適配成第二版需要的增刪改查的功能
   */
public class Adapter implements LogDbOperateApi{
    /**
     * 持有需要被適配的接口對象
     */
    private LogFileOperateApi adaptee;
    /**
     * 構造方法,傳入需要被適配的對象
     * @param adaptee 需要被適配的對象
     */
    public Adapter(LogFileOperateApi adaptee) {
        this.adaptee = adaptee;
    }

    public void createLog(LogModel lm) {
        //1:先讀取文件的內容
        List<LogModel> list = adaptee.readLogFile();
        //2:加入新的日志對象
        list.add(lm);
        //3:重新寫入文件
        adaptee.writeLogFile(list);
    }
    public List<LogModel> getAllLog() {
        return adaptee.readLogFile();
    }
    public void removeLog(LogModel lm) {
        //1:先讀取文件的內容
        List<LogModel> list = adaptee.readLogFile();
        //2:刪除相應的日志對象
        list.remove(lm);
        //3:重新寫入文件
        adaptee.writeLogFile(list);
    }
    public void updateLog(LogModel lm) {
        //1:先讀取文件的內容
        List<LogModel> list = adaptee.readLogFile();
        //2:修改相應的日志對象
        for(int i=0;i<list.size();i++){
            if(list.get(i).getLogId().equals(lm.getLogId())){
                list.set(i, lm);
                break;
            }
        }
        //3:重新寫入文件
        adaptee.writeLogFile(list);
    }
}
  1. 此時的客戶端也需要一些改變,示例代碼如下:
public class Client {
    public static void main(String[] args) {
        //準備日志內容,也就是測試的數據
        LogModel lm1 = new LogModel();
        lm1.setLogId("001");
        lm1.setOperateUser("admin");
        lm1.setOperateTime("2010-03-0210:08:18");
        lm1.setLogContent("這是一個測試");
        List<LogModel> list = new ArrayList<LogModel>();
        list.add(lm1);
        //創建操作日志文件的對象
        LogFileOperateApi logFileApi = new LogFileOperate("");
    
        //創建新版的操作日志的接口對象
        LogDbOperateApi api = new Adapter(logFileApi);
    
        //保存日志文件
        api.createLog(lm1);    
        //讀取日志文件的內容
        List<LogModel> allLog = api.getAllLog();
        System.out.println("allLog="+allLog);
    }
}
  1. 小結一下思路

① 原有文件存取日志的方式,運行得很好,如圖所示:

原有文件存取日志的方式

② 現在有了新的基于數據庫的實現,新的實現有自己的接口,如圖所示:

新的基于數據庫的實現

③ 現在想要在第二版的實現里面,能夠同時兼容第一版的功能,那么就應有一個類來實現第二版的接口,然后在這個類里面去調用已有的功能實現,這個類就是適配器,如下圖所示:

加入適配器的實現結構示意圖

上面是分步的思路,現在來看一下前面示例的整體結構,如圖所示:

適配器實現的示例的結構示意圖

如同上面的例子,原本新的日志操作接口不能和舊的文件實現一起工作,但是經過適配器適配后,新的日志操作接口就能和舊的文件實現日志存儲一起工作了。

3 模式講解#

3.1 認識適配器模式##

  1. 模式的功能

適配器模式的主要功能是進行轉換匹配,目的是復用已有的功能,而不是來實現新的接口。也就是說,客戶端需要的功能應該是已經實現好了的,不需要適配器模式來實現,適配器模式主要負責把不兼容的接口轉換成客戶端期望的樣子就好了

但這并不是說,在適配器里面就不能實現功能,適配器里面可以實現功能,稱這種適配器為智能適配器。再說了,在接口匹配和轉換的過程中,也是有可能需要額外實現一定的功能,才能夠轉換過來的,比如需要調整參數以進行匹配等。

  1. Adaptee和Target的關系

適配器模式中被適配的接口Adaptee和適配成為的接口Target是沒有關聯的,也就是說,Adaptee和Target中的方法既可以相同,也可以不同,極端情況下兩個接口里面的方法可能是完全不同的,當然極端情況下也可以完全相同。

這里所說的相同和不同,是指的方法定義的名稱、參數列表、返回值、包括方法本身的功能都可以相同和不同。

  1. 對象組合

根據前面的實現,你會發現,適配器的實現方式其實是依靠對象組合的方式。通過給適配器對象組合被適配的對象,然后當客戶端調用Target的時候,適配器會把相應的功能,委托給被適配的對象去完成。

3.2 適配器模式的實現##

  1. 適配器的常見實現

在實現適配器的時候,適配器通常是一個類,一般會讓適配器類去實現Target接口,然后在適配器的具體實現里面調用Adaptee。也就是說適配器通常是一個Target類型,而不是Adaptee類型。如同前面的例子演示的那樣。

  1. 智能適配器

在實際開發中,適配器也可以實現一些Adaptee沒有實現,但是在Target中定義的功能,這種情況就需要在適配器的實現里面,加入新功能的實現,這種適配器被稱為智能適配器。

如果要使用智能適配器,一般新加入的功能的實現,會用到很多Adaptee的功能,相當于利用Adaptee的功能來實現更高層的功能。當然也可以完全實現新加的功能,跟已有的功能都不靠邊,變相是擴展了功能。

  1. 適配多個Adaptee

適配器在適配的時候,可以適配多個Adaptee,也就是說實現某個新的Target的功能的時候,需要調用到多個模塊的功能,適配多個模塊的功能才能滿足新接口的要求

  1. 適配器Adapter實現的復雜程度

適配器Adapter實現的復雜程度,取決于Target和Adaptee的相似程度。

如果相似程度很高,比如只有方法名稱不一樣,那么Adapter只是需要簡單的轉調一下接口就好了。

如果相似程度低,比如兩邊接口的方法定義的功能完全不一樣,在Target中定義的一個方法,可能在Adaptee中定義了三個更小的方法,那么這個時候在實現Adapter的時候,就需要組合調用了。

  1. 缺省適配

缺省適配的意思是:為一個接口提供缺省實現。有了它,就不用直接去實現接口,而是采用繼承這個缺省適配對象,從而讓子類可以有選擇的去覆蓋實現需要的方法,對于不需要的方法,就使用缺省適配的方法就可以了。

3.3 雙向適配器##

適配器也可以實現雙向的適配,前面我們講的都是把Adaptee適配成為Target,其實也可以把Target適配成為Adaptee,也就是說這個適配器可以同時當作Target和Adaptee來使用

繼續前面講述的例子,如果說由于某些原因,第一版和第二版會同時共存一段時間,比如第二版的應用還在不斷調整中,也就是第二版還不夠穩定。客戶提出,希望在兩版共存期間,主要還是在使用第一版,同時希望第一版的日志也能記入到數據庫中,也就是客戶雖然操作的接口是第一版的日志接口,界面也是第一版的界面,但是可以使用第二版的把日志記錄到數據庫的功能。

也就是說希望兩版能實現雙向的適配,結構如下圖所示:

雙向適配結構圖

這里只加了幾個新的東西,一個就是DB存儲日志的實現,前面的例子是沒有的,因為直接被適配成使用文件存儲日志的實現了;另外一個就是雙向適配器,其實與把文件存儲的方式適配成為DB實現的接口是一樣的,只需要新加上把DB實現的功能適配成為文件實現的接口就好了。

  1. 先看看DB存儲日志的實現,為了簡單,這里就不去真的實現和數據庫交互了,示意一下,示例代碼如下:
/**
   * DB存儲日志的實現,為了簡單,這里就不去真的實現和數據庫交互了,示意一下
   */
public class LogDbOperate implements LogDbOperateApi{
    public void createLog(LogModel lm) {
        System.out.println("now in LogDbOperate createLog,lm="+lm);
    }
    public List<LogModel> getAllLog() {
        System.out.println("now in LogDbOperate getAllLog");
        return null;
    }
    public void removeLog(LogModel lm) {
        System.out.println("now in LogDbOperate removeLog,lm="+lm);
    }
    public void updateLog(LogModel lm) {
        System.out.println("now in LogDbOperate updateLog,lm="+lm);
    }
}
  1. 然后看看新的適配器的實現,由于是雙向的適配器,一個方向是:把新的DB實現的接口適配成為舊的文件操作需要的接口;另外一個方向是把舊的文件操作的接口適配成為新的DB實現需要的接口。示例代碼如下:
/**
   * 雙向適配器對象
   */
public class TwoDirectAdapter implements LogDbOperateApi,LogFileOperateApi{
    /**
     * 持有需要被適配的文件存儲日志的接口對象
     */
    private LogFileOperateApi fileLog;
    /**
     * 持有需要被適配的DB存儲日志的接口對象
     */
    private LogDbOperateApi  dbLog;
    /**
     * 構造方法,傳入需要被適配的對象
     * @param fileLog 需要被適配的文件存儲日志的接口對象
     * @param dbLog 需要被適配的DB存儲日志的接口對象
     */
    public TwoDirectAdapter(LogFileOperateApi fileLog, LogDbOperateApi dbLog) {
        this.fileLog = fileLog;
        this.dbLog = dbLog;
    }
    /*-----以下是把文件操作的方式適配成為DB實現方式的接口-----*/
    public void createLog(LogModel lm) {
        //1:先讀取文件的內容
        List<LogModel> list = fileLog.readLogFile();
        //2:加入新的日志對象
        list.add(lm);
        //3:重新寫入文件
        fileLog.writeLogFile(list);
    }
    public List<LogModel> getAllLog() {
        return fileLog.readLogFile();
    }
    public void removeLog(LogModel lm) {
        //1:先讀取文件的內容
        List<LogModel> list = fileLog.readLogFile();
        //2:刪除相應的日志對象
        list.remove(lm);
        //3:重新寫入文件
        fileLog.writeLogFile(list);
    }
    public void updateLog(LogModel lm) {
        //1:先讀取文件的內容
        List<LogModel> list = fileLog.readLogFile();
        //2:修改相應的日志對象
        for(int i=0;i<list.size();i++){
            if(list.get(i).getLogId().equals(lm.getLogId())){
                list.set(i, lm);
                break;
            }
        }
        //3:重新寫入文件
        fileLog.writeLogFile(list);
    }
    /*-----以下是把DB操作的方式適配成為文件實現方式的接口-----*/
    public List<LogModel> readLogFile() {
        return dbLog.getAllLog();
    }
    public void writeLogFile(List<LogModel> list) {
        //1:最簡單的實現思路,先刪除數據庫中的數據
        //2:然后循環把現在的數據加入到數據庫中
        for(LogModel lm : list){
            dbLog.createLog(lm);
        }    
    }
}
  1. 看看如何使用這個雙向適配器,測試一下,示例代碼如下:
public class Client {
    public static void main(String[] args) {
        //準備日志內容,也就是測試的數據
        LogModel lm1 = new LogModel();
        lm1.setLogId("001");
        lm1.setOperateUser("admin");
        lm1.setOperateTime("2010-03-0210:08:18");
        lm1.setLogContent("這是一個測試");
        List<LogModel> list = new ArrayList<LogModel>();
        list.add(lm1);
        //創建操作日志文件的對象
        LogFileOperateApi fileLogApi = new LogFileOperate("");
        LogDbOperateApi dbLogApi = new LogDbOperate();
    
        //創建經過雙向適配后的操作日志的接口對象
        LogFileOperateApi fileLogApi2 = new TwoDirectAdapter(fileLogApi,dbLogApi);
        LogDbOperateApi dbLogApi2 = new TwoDirectAdapter(fileLogApi,dbLogApi);
    
        //先測試從文件操作適配到第二版,
        //雖然調用的是第二版的接口,其實是文件操作在實現
        dbLogApi2.createLog(lm1);
        List<LogModel> allLog = dbLogApi2.getAllLog();
        System.out.println("allLog="+allLog);
    
        //再測試從數據庫存儲適配成第一版的接口,
        //也就是調用第一版的接口,其實是數據實現
        fileLogApi2.writeLogFile(list);
        fileLogApi2.readLogFile();
    }
}

事實上,使用適配器有一個潛在的問題,就是被適配的對象不再兼容Adaptee的接口,因為適配器只是實現了Target的接口,這導致并不是所有Adaptee對象可以被使用的地方都可以使用適配器

而雙向適配器就解決了這樣的問題,雙向適配器同時實現了Target和Adaptee的接口,使得雙向適配器可以在Target或Adaptee被使用的地方使用,以提供對所有客戶的透明性,尤其在兩個不同的客戶需要用不同的方式查看同一個對象時,適合使用雙向適配器。

3.4 適配器模式的優缺點##

  1. 更好的復用性

如果功能是已經有了的,只是接口不兼容,那么通過適配器模式就可以讓這些功能得到更好的復用。

  1. 更好的可擴展性

在實現適配器功能的時候,可以調用自己開發的功能,從而自然的擴展系統的功能。

  1. 過多的使用適配器,會讓系統非常零亂,不容易整體進行把握

比如:明明看到調用的是A接口,其實內部被適配成了B接口來實現,一個系統如果太多這種情況,無異于一場災難。因此如果不是很有必要,可以不使用適配器,而是直接對系統進行重構。

3.5 思考適配器模式##

  1. 適配器模式的本質

適配器模式的本質:轉換匹配,復用功能。

適配器通過轉換調用已有的實現,從而能把已有的實現匹配成需要的接口,使之能滿足客戶端的需要。也就是說轉換匹配是手段,而復用已有的功能才是目的。

在進行轉換匹配的過程中,適配器還可以在轉換調用的前后實現一些功能處理,也就是實現智能的適配。

  1. 何時選用適配器模式

建議在如下情況中,選用適配器模式:

如果你想要使用一個已經存在的類,但是它的接口不符合你的需求,這種情況可以使用適配器模式,來把已有的實現轉換成你需要的接口

如果你想創建一個可以復用的類,這個類可能和一些不兼容的類一起工作,這種情況可以使用適配器模式,到時候需要什么就適配什么

如果你想使用一些已經存在的子類,但是不可能對每一個子類都進行適配,這種情況可以選用對象適配器,直接適配這些子類的父類就可以了。

3.6 相關模式##

  1. 適配器模式與橋接模式

其實這兩個模式除了結構略為相似外,功能上完全不同。

適配器模式是把兩個或者多個接口的功能進行轉換匹配;而橋接模式是讓接口和實現部分相分離,以便它們可以相對獨立的變化。

  1. 適配器模式與裝飾模式

從某種意義上講,適配器模式能模擬實現簡單的裝飾模式的功能,也就是為已有功能增添功能。比如我們在適配器里面這么寫:

public void adapterMethod(){
      System.out.println("在調用Adaptee的方法之前完成一定的工作");
      //調用Adaptee的相關方法
      adaptee.method();
      System.out.println("在調用Adaptee的方法之后完成一定的工作");
}

如上的寫法,就相當于在調用Adaptee的被適配方法前后添加了新的功能,這樣適配過后,客戶端得到的功能就不單純是Adaptee的被適配方法的功能了。看看是不是類似裝飾模式的功能呢?

注意,僅僅是類似,造成這種類似的原因:兩種設計模式在實現上都是使用的對象組合,都可以在轉調組合對象的功能前后進行一些附加的處理,因此有這么一個相似性。它們的目的和本質都是不一樣的

兩個模式有一個很大的不同:一般適配器適配過后是需要改變接口的,如果不改接口就沒有必要適配了;而裝飾模式是不改接口的,無論多少層裝飾都是一個接口。因此裝飾模式可以很容易的支持遞歸組合,而適配器就做不到了,每次的接口不同,沒法遞歸。

  1. 適配器模式和代理模式

適配器模式可以跟代理模式組合使用,在實現適配器的時候,可以通過代理來調用Adaptee,這可以獲得更大的靈活性。

  1. 適配器模式和抽象工廠模式

在適配器實現的時候,通常需要得到被適配的對象,如果被適配的是一個接口,那么就可以結合一些可以創造對象實例的設計模式,來得到被適配的對象示例。比如:抽象工廠模式、單例模式、工廠方法模式等等。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容