1. 程式人生 > >介面卡模式原理及例項介紹

介面卡模式原理及例項介紹

本文首先介紹了介面卡模式的基本實現原理,然後通過一個例項讓讀者從程式碼的角度快速瞭解介面卡模式的執行方式,接著對介面卡模式在 JDK 中的應用做了一些介紹,最後對介面卡模式的兩種實現方式區別、優缺點、應用場景等做了一定的總結。

介面卡模式基本介紹

著名的設計模式“四人幫”這樣評價介面卡模式:

將一個類的介面轉換成客戶希望的另外一個介面。Adapter 模式使得原本由於介面不相容而不能一起工作的那些類可以一起工作。——Gang of Four

介面卡模式將一個類的介面適配成使用者所期待的。一個介面卡通常允許因為介面不相容而不能一起工作的類能夠在一起工作,做法是將類自己的介面包裹在一個已存在的類中。

Adapter 設計模式主要目的組合兩個不相干類,常用有兩種方法,第一種解決方案是修改各自類的介面。但是如果沒有原始碼,或者不願意為了一個應用而修改各自的介面,則需要使用 Adapter 介面卡,在兩種介面之間建立一個混合介面。

圖 1. 介面卡模式類圖
圖 1. 介面卡模式類圖

圖 1 所示是介面卡模式的類圖。Adapter 介面卡設計模式中有 3 個重要角色:被適配者 Adaptee,介面卡 Adapter 和目標物件 Target。其中兩個現存的想要組合到一起的類分別是被適配者 Adaptee 和目標物件 Target 角色,按照類圖所示,我們需要建立一個介面卡 Adapter 將其組合在一起。

具體實現程式碼請見清單 1-4。

清單 1. 客戶端使用的介面
/*
 * 定義客戶端使用的介面,與業務相關
 */
public interface Target {
 /*
 * 客戶端請求處理的方法
 */
public void request();
}
清單 2. 被適配的物件
/*
 * 已經存在的介面,這個介面需要配置
 */
public class Adaptee {
 /*
 * 原本存在的方法
 */
public void specificRequest(){
//業務程式碼
}
}
清單 3. 介面卡實現
/*
 * 介面卡類
 */
public class Adapter implements Target{
 /*
 * 持有需要被適配的介面物件
 */
private Adaptee adaptee;
/*
 * 構造方法,傳入需要被適配的物件
 * @param adaptee 需要被適配的物件
 */
public Adapter(Adaptee adaptee){
this.adaptee = adaptee;
}
@Override
public void request() {
// TODO Auto-generated method stub
adaptee.specificRequest();
}
 
}
清單 4. 客戶端程式碼
/*
 * 使用介面卡的客戶端
 */
public class Client {
 public static void main(String[] args){
 //建立需要被適配的物件
 Adaptee adaptee = new Adaptee();
 //建立客戶端需要呼叫的介面物件
 Target target = new Adapter(adaptee);
 //請求處理
 target.request();
 }
}

以下情況比較適合使用 Adapter 模式:

  1. 當你想使用一個已經存在的類,而它的介面不符合你的需求;

  2. 你想建立一個可以複用的類,該類可以與其他不相關的類或不可預見的類協同工作;

  3. 你想使用一些已經存在的子類,但是不可能對每一個都進行子類化以匹配它們的介面,物件介面卡可以適配它的父親介面。

介面卡模式使用示例程式碼

考慮一個記錄日誌的應用,使用者可能會提出要求採用檔案的方式儲存日誌,也可能會提出儲存日誌到資料庫的需求,這樣我們可以採用介面卡模式對舊的日誌類進行改造,提供新的支援方式。

首先我們需要一個簡單的日誌物件類,如清單 5 所示。

清單 5. 日誌物件類
/*
 * 日誌資料物件
 */
public class LogBean {
 private String logId;//日誌編號
 private String opeUserId;//操作人員
 
 public String getLogId(){
 return logId;
 }
 public void setLogId(String logId){
 this.logId = logId;
 }
 
 public String getOpeUserId(){
 return opeUserId;
 }
 public void setOpeUserId(String opeUserId){
 this.opeUserId = opeUserId;
}
public String toString(){
 return "logId="+logId+",opeUserId="+opeUserId;
}
}

接下來定義一個操作日誌檔案的介面,程式碼如清單 6 所示。

清單 6. 操作日誌介面
import java.util.List;

/*
 * 讀取日誌檔案,從檔案裡面獲取儲存的日誌列表物件
 * @return 儲存的日誌列表物件
 */
public interface LogFileOperateApi {
 public List<LogBean> readLogFile();
 /**
 * 寫日誌檔案,把日誌列表寫出到日誌檔案中去
 * @param list 要寫到日誌檔案的日誌列表
 */
 public void writeLogFile(List<LogBean> list);
}

然後實現日誌檔案的儲存和獲取,這裡忽略業務程式碼,程式碼如清單 7 所示。

清單 7. 實現對日誌檔案的獲取
import java.io.File;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;

/*
 * 實現對日誌檔案的操作
 */
public class LogFileOperate implements LogFileOperateApi{
 /*
 * 設定日誌檔案的路徑和檔名稱
 */
private String logFileName = "file.log";
/*
 * 構造方法,傳入檔案的路徑和名稱
 */
public LogFileOperate(String logFilename){
if(logFilename!=null){
this.logFileName = logFilename;
}
}

@Override
public List<LogBean> readLogFile() {
// TODO Auto-generated method stub
List<LogBean> list = null;
ObjectInputStream oin =null;
//業務程式碼
return list;
}

@Override
public void writeLogFile(List<LogBean> list) {
// TODO Auto-generated method stub
File file = new File(logFileName);
ObjectOutputStream oout = null;
//業務程式碼
}

}

如果這時候需要引入資料庫方式,引入介面卡之前,我們需要定義日誌管理的操作介面,程式碼如清單 8 所示。

清單 8. 定義資料庫操作介面
public interface LogDbOpeApi {
 /*
 * 新增日誌
 * @param 需要新增的日誌物件
 */
public void createLog(LogBean logbean);
}

接下來就要實現介面卡了,LogDbOpeApi 介面就相當於 Target 介面,LogFileOperate 就相當於 Adaptee 類。Adapter 類程式碼如清單 9 所示。

清單 9. Adapter 類實現
import java.util.List;

/*
 * 介面卡物件,將記錄日誌到檔案的功能適配成資料庫功能
 */
public class LogAdapter implements LogDbOpeApi{
 private LogFileOperateApi adaptee;
 public LogAdapter(LogFileOperateApi adaptee){
 this.adaptee = adaptee;
 }
@Override
public void createLog(LogBean logbean) {
// TODO Auto-generated method stub
List<LogBean> list = adaptee.readLogFile();
list.add(logbean);
adaptee.writeLogFile(list);
}
}

最後是客戶端程式碼的實現,如清單 10 所示。

清單 10. 客戶端類實現
import java.util.ArrayList;
import java.util.List;


public class LogClient {
 public static void main(String[] args){
 LogBean logbean = new LogBean();
 logbean.setLogId("1");
 logbean.setOpeUserId("michael");
 List<LogBean> list = new ArrayList<LogBean>();
 LogFileOperateApi logFileApi = new LogFileOperate("");
 //建立操作日誌的介面物件
 LogDbOpeApi api = new LogAdapter(logFileApi);
 api.createLog(logbean);
 }
}

介面卡模式在開源專案中的應用

JDK 中有大量使用介面卡模式的案例,清單 11 大致列舉了一些類。

清單 11. 使用介面卡模式的類
java.util.Arrays#asList()
javax.swing.JTable(TableModel)
java.io.InputStreamReader(InputStream)
java.io.OutputStreamWriter(OutputStream)
javax.xml.bind.annotation.adapters.XmlAdapter#marshal()
javax.xml.bind.annotation.adapters.XmlAdapter#unmarshal()

JDK1.1 之前提供的容器有 Arrays,Vector,Stack,Hashtable,Properties,BitSet,其中定義了一種訪問群集內各元素的標準方式,稱為 Enumeration(列舉器)介面,用法如清單 12 所示。

清單 12.Enumeration 介面實現方式
Vector v=new Vector();
for (Enumeration enum =v.elements(); enum.hasMoreElements();) {
Object o = enum.nextElement();
processObject(o);
}

JDK1.2 版本中引入了 Iterator 介面,新版本的集合物件(HashSet,HashMap,WeakHeahMap,ArrayList,TreeSet,TreeMap, LinkedList)是通過 Iterator 介面訪問集合元素的,用法如清單 13 所示。

清單 13. Iterator 介面實現方式
List list=new ArrayList();
for(Iterator it=list.iterator();it.hasNext();)
{
 System.out.println(it.next());
}

這樣,如果將老版本的程式執行在新的 Java 編譯器上就會出錯。因為 List 介面中已經沒有 elements(),而只有 iterator() 了。那麼如何將老版本的程式執行在新的 Java 編譯器上呢? 如果不加修改,是肯定不行的,但是修改要遵循“開-閉”原則。我們可以用 Java 設計模式中的介面卡模式解決這個問題。清單 14 所示是解決方法程式碼。

清單 14. 採用介面卡模式
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

public class NewEnumeration implements Enumeration
{

 Iterator it;
 public NewEnumeration(Iterator it)
 {
 this.it=it;
 // TODO Auto-generated constructor stub
 }

 public boolean hasMoreElements()
 {
 // TODO Auto-generated method stub
 return it.hasNext();
 }

 public Object nextElement()
 {
 // TODO Auto-generated method stub
 return it.next();
 }
 public static void main(String[] args)
 {
 List list=new ArrayList();
 list.add("a");
 list.add("b");
 list.add("C");
 for(Enumeration e=new NewEnumeration(list.iterator());e.hasMoreElements();)
 {
 System.out.println(e.nextElement());
 }
 }
}

清單 14 所示的 NewEnumeration 是一個介面卡類,通過它實現了從 Iterator 介面到 Enumeration 介面的適配,這樣我們就可以使用老版本的程式碼來使用新的集合物件了。

Java I/O 庫大量使用了介面卡模式,例如 ByteArrayInputStream 是一個介面卡類,它繼承了 InputStream 的介面,並且封裝了一個 byte 陣列。換言之,它將一個 byte 陣列的介面適配成 InputStream 流處理器的介面。

我們知道 Java 語言支援四種類型:Java 介面,Java 類,Java 陣列,原始型別(即 int,float 等)。前三種是引用型別,類和陣列的例項是物件,原始型別的值不是物件。也即,Java 語言的陣列是像所有的其他物件一樣的物件,而不管陣列中所儲存的元素型別是什麼。這樣一來的話,ByteArrayInputStream 就符合介面卡模式的描述,是一個物件形式的介面卡類。FileInputStream 是一個介面卡類。在 FileInputStream 繼承了 InputStrem 型別,同時持有一個對 FileDiscriptor 的引用。這是將一個 FileDiscriptor 物件適配成 InputStrem 型別的物件形式的介面卡模式。檢視 JDK1.4 的原始碼我們可以看到清單 15 所示的 FileInputStream 類的原始碼。

清單 15. FileInputStream 類
 Public class FileInputStream extends InputStream{
/* File Descriptor - handle to the open file */
private FileDescriptor fd;
public FileInputStream(FileDescriptor fdObj) {
SecurityManager security = System.getSecurityManager(); 
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkRead(fdObj);}fd = fdObj; 
} 
public FileInputStream(File file) throws FileNotFoundException {
String name = file.getPath();
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
fd = new FileDescriptor();
open(name);
}
//其它程式碼
}
}

同樣地,在 OutputStream 型別中,所有的原始流處理器都是介面卡類。ByteArrayOutputStream 繼承了 OutputStream 型別,同時持有一個對 byte 陣列的引用。它一個 byte 陣列的介面適配成 OutputString 型別的介面,因此也是一個物件形式的介面卡模式的應用。

FileOutputStream 繼承了 OutputStream 型別,同時持有一個對 FileDiscriptor 物件的引用。這是一個將 FileDiscriptor 介面適配成 OutputStream 介面形式的物件型介面卡模式。

Reader 型別的原始流處理器都是介面卡模式的應用。StringReader 是一個介面卡類,StringReader 類繼承了 Reader 型別,持有一個對 String 物件的引用。它將 String 的介面適配成 Reader 型別的介面。

Spring 中使用介面卡模式的典型應用

在 Spring 的 AOP 裡通過使用的 Advice(通知)來增強被代理類的功能。Spring 實現這一 AOP 功能的原理就使用代理模式(1、JDK 動態代理。2、CGLib 位元組碼生成技術代理。)對類進行方法級別的切面增強,即,生成被代理類的代理類,並在代理類的方法前,設定攔截器,通過執行攔截器中的內容增強了代理方法的功能,實現的面向切面程式設計。

Advice(通知)的型別有:BeforeAdvice、AfterReturningAdvice、ThrowSadvice 等。每個型別 Advice(通知)都有對應的攔截器,MethodBeforeAdviceInterceptor、AfterReturningAdviceInterceptor、ThrowsAdviceInterceptor。Spring 需要將每個 Advice(通知)都封裝成對應的攔截器型別,返回給容器,所以需要使用介面卡模式對 Advice 進行轉換。具體程式碼如清單 16-19 所示。

清單 16. MethodBeforeAdvice 類
public interface MethodBeforeAdvice extends BeforeAdvice { 
 
 void before(Method method, Object[] args, Object target) throws Throwable; 
 
} 
public interface MethodBeforeAdvice extends BeforeAdvice {

void before(Method method, Object[] args, Object target) throws Throwable;

}
清單 17. Adapter 類介面
public interface AdvisorAdapter { 
 
 boolean supportsAdvice(Advice advice); 
 
 MethodInterceptor getInterceptor(Advisor advisor); 
 
} 
public interface AdvisorAdapter {

boolean supportsAdvice(Advice advice);

MethodInterceptor getInterceptor(Advisor advisor);

}
清單 18. MethodBeforeAdviceAdapter 類
class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable { 
 
 public boolean supportsAdvice(Advice advice) { 
 return (advice instanceof MethodBeforeAdvice); 
 } 
 
 public MethodInterceptor getInterceptor(Advisor advisor) { 
 MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice(); 
 return new MethodBeforeAdviceInterceptor(advice); 
 } 
 
} 
class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable {

public boolean supportsAdvice(Advice advice) {
return (advice instanceof MethodBeforeAdvice);
}

public MethodInterceptor getInterceptor(Advisor advisor) {
MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice();
return new MethodBeforeAdviceInterceptor(advice);
}

}
清單 19. DefaultAdvisorAdapterRegistry 類
public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry, Serializable { 
 
 private final List<AdvisorAdapter> adapters = new ArrayList<AdvisorAdapter>(3); 
 
 
 /** 
 * Create a new DefaultAdvisorAdapterRegistry, registering well-known adapters. 
 */ 
 public DefaultAdvisorAdapterRegistry() {//這裡註冊了介面卡 
 registerAdvisorAdapter(new MethodBeforeAdviceAdapter()); 
 registerAdvisorAdapter(new AfterReturningAdviceAdapter()); 
 registerAdvisorAdapter(new ThrowsAdviceAdapter()); 
 } 
 
 
 public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException { 
 if (adviceObject instanceof Advisor) { 
 return (Advisor) adviceObject; 
 } 
 if (!(adviceObject instanceof Advice)) { 
 throw new UnknownAdviceTypeException(adviceObject); 
 } 
 Advice advice = (Advice) adviceObject; 
 if (advice instanceof MethodInterceptor) { 
 // So well-known it doesn't even need an adapter. 
 return new DefaultPointcutAdvisor(advice); 
 } 
 for (AdvisorAdapter adapter : this.adapters) { 
 // Check that it is supported. 
 if (adapter.supportsAdvice(advice)) {//這裡呼叫了介面卡的方法 
 return new DefaultPointcutAdvisor(advice); 
 } 
 } 
 throw new UnknownAdviceTypeException(advice); 
 } 
 
 public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException { 
 List<MethodInterceptor> interceptors = new ArrayList<MethodInterceptor>(3); 
 Advice advice = advisor.getAdvice(); 
 if (advice instanceof MethodInterceptor) { 
 interceptors.add((MethodInterceptor) advice); 
 } 
 for (AdvisorAdapter adapter : this.adapters) { 
 if (adapter.supportsAdvice(advice)) {//這裡呼叫了介面卡的方法 
 interceptors.add(adapter.getInterceptor(advisor)); 
 } 
 } 
 if (interceptors.isEmpty()) { 
 throw new UnknownAdviceTypeException(advisor.getAdvice()); 
 } 
 return interceptors.toArray(new MethodInterceptor[interceptors.size()]); 
 } 
 
 public void registerAdvisorAdapter(AdvisorAdapter adapter) { 
 this.adapters.add(adapter); 
 } 
 
}

雙向介面卡

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

清單 20. TwiceAdapter 類
import java.util.List;

/*
 * 雙向介面卡物件案例
 */
public class TwiceAdapter implements LogDbOpeApi,LogFileOperateApi {
 
/*
 * 持有需要被適配的檔案儲存日誌的介面物件
 */
private LogFileOperateApi fileLog;
/*
 * 持有需要被適配的 DB 儲存日誌的介面物件
 */
private LogDbOpeApi dbLog;

public TwiceAdapter(LogFileOperateApi fileLog,LogDbOpeApi dbLog){
this.fileLog = fileLog;
this.dbLog = dbLog;
}
@Override
public List<LogBean> readLogFile() {
// TODO Auto-generated method stub
return null;
}

@Override
public void writeLogFile(List<LogBean> list) {
// TODO Auto-generated method stub

}

@Override
public void createLog(LogBean logbean) {
// TODO Auto-generated method stub
List<LogBean> list = fileLog.readLogFile();
list.add(logbean);
fileLog.writeLogFile(list);
}

}

雙向介面卡同時實現了 Target 和 Adaptee 的介面,使得雙向介面卡可以在 Target 或 Adaptee 被使用的地方使用,以提供對所有客戶的透明性。尤其在兩個不同的客戶需要用不同的地方檢視同一個物件時,適合使用雙向介面卡。

物件介面卡和類介面卡

在標準的介面卡模式裡面,根據介面卡的實現方式,把介面卡分成物件介面卡和類介面卡。

物件介面卡

依賴於物件的組合,都是採用物件組合的方式,也就是物件介面卡實現的方式。

類介面卡

採用多重繼承對一個介面與另一個介面進行匹配。由於 Java 不支援多重繼承,所以到目前為止還沒有涉及。但可以通過讓介面卡去實現 Target 介面的方式來實現。

清單 21. ClassAdapter 類
import java.util.List;

/*
 * 類介面卡物件案例
 */
public class ClassAdapter extends LogFileOperate implements LogDbOpeApi{

public ClassAdapter(String logFilename) {
super(logFilename);
// TODO Auto-generated constructor stub
}

@Override
public void createLog(LogBean logbean) {
// TODO Auto-generated method stub
List<LogBean> list = this.readLogFile();
list.add(logbean);
this.writeLogFile(list);
}
 
}

在實現中,主要是介面卡的實現與以前不一樣,與物件介面卡實現同樣的功能相比,類介面卡在實現上有所改變:

  1. 需要繼承 LogFileOperate 的實現,然後再實現 LogDbOpeApi 介面;

  2. 需要按照繼承 LogFileOperate 的要求,提供傳入檔案路徑和名稱的構造方法;

  3. 不再需要持有 LogFileOperate 的物件,因為介面卡本身就是 LogFileOperate 物件的子類;

  4. 以前呼叫被適配物件的方法的地方,全部修改成呼叫自己的方法。

類介面卡和物件介面卡的選擇

  1. 從實現上:類介面卡使用物件繼承的方式,屬於靜態的定義方式。物件介面卡使用物件組合的方式,屬於動態組合的方式;

  2. 從工作模式上:類介面卡直接繼承了 Adaptee,使得介面卡不能和 Adaptee 的子類一起工作。物件介面卡允許一個 Adapter 和多個 Adaptee,包括 Adaptee 和它所有的子類一起工作;

  3. 從定義角度:類介面卡可以重定義 Adaptee 的部分行為,相當於子類覆蓋父類的部分實現方法。物件介面卡要重定義 Adaptee 很困難;

  4. 從開發角度:類介面卡僅僅引入了一個物件,並不需要額外的引用來間接得到 Adaptee。物件介面卡需要額外的引用來間接得到 Adaptee。

總的來說,建議使用物件介面卡方式。

介面卡模式使用注意事項

  1. 充當介面卡角色的類就是:實現已有介面的抽象類;

  2. 為什麼要用抽象類?此類是不要被例項化的。而只充當介面卡的角色,也就為其子類提供了一個共同的介面,但其子類又可以將精力只集中在其感興趣的地方。

  3. 介面卡模式中被適配的介面 Adaptee 和適配成為的介面 Target 是沒有關聯的,Adaptee 和 Target 中的方法既可以是相同的,也可以是不同的。

  4. 介面卡在適配的時候,可以適配多個 Apaptee,也就是說實現某個新的 Target 的功能的時候,需要呼叫多個模組的功能,適配多個模組的功能才能滿足新介面的要求。

  5. 介面卡有一個潛在的問題,就是被適配的物件不再相容 Adaptee 的介面,因為介面卡只是實現了 Target 的介面。這導致並不是所有 Adaptee 物件可以被使用的地方都能是使用介面卡,雙向介面卡解決了這個問題。

優點

介面卡模式也是一種包裝模式,它與裝飾模式同樣具有包裝的功能,此外,物件介面卡模式還具有委託的意思。總的來說,介面卡模式屬於補償模式,專用來在系統後期擴充套件、修改時使用。

缺點

過多的使用介面卡,會讓系統非常零亂,不易整體進行把握。比如,明明看到呼叫的是 A 介面,其實內部被適配成了 B 介面的實現,一個系統如果太多出現這種情況,無異於一場災難。因此如果不是很有必要,可以不使用介面卡,而是直接對系統進行重構。

介面卡模式應用場景

在軟體開發中,也就是系統的資料和行為都正確,但介面不相符時,我們應該考慮用介面卡,目的是使控制範圍之外的一個原有物件與某個介面匹配。介面卡模式主要應用於希望複用一些現存的類,但是介面又與複用環境要求不一致的情況。比如在需要對早期程式碼複用一些功能等應用上很有實際價值。適用場景大致包含三類:

1、已經存在的類的介面不符合我們的需求;

2、建立一個可以複用的類,使得該類可以與其他不相關的類或不可預見的類(即那些介面可能不一定相容的類)協同工作;

3、在不對每一個都進行子類化以匹配它們的介面的情況下,使用一些已經存在的子類。

結束語

本文對介面卡模式做了一些介紹,希望能夠幫助讀者對介面卡模式有進一步的瞭解。介面卡模式也是一種包裝模式,它與裝飾模式同樣具有包裝的功能,此外,物件介面卡模式還具有委託的意思。總的來說,介面卡模式屬於補償模式,專門用來在系統後期擴充套件、修改時使用,但要注意不要過度使用介面卡模式。