天天看點

謹慎使用Marker Interface

  之是以寫這篇文章,源自于組内的一些技術讨論。實際上,Effective Java的Item 37已經詳細地讨論了Marker Interface。但是從整個Item的角度來看,其對于Marker Interface所提供的一系列優點及特殊特性實際上是持肯定态度的。是以很多人,包括我的同僚,都将該條目中的一些結論當作是準則來去執行,卻忽略了得到這些結論時的前提,進而導緻了一定程度的誤用。

  當然,我并不是在反對Effective Java的Item 37。說實話,我也沒有這個資本。隻是我個人在技術上略顯保守,是以希望通過這篇文章闡述一下Marker Interface可能帶來的一系列問題,進而使大家更為謹慎而且準确地使用Marker Interface。

Marker Interface簡介

  或許有些讀者并不了解什麼是Marker Interface。那麼首先讓我們來看看JDK中Set接口的實作:

1 public interface Set<E> extends Collection<E> {
2 }      

  細心的讀者會發現,實際上Set較Collection沒有添加任何接口函數。那為什麼JDK還要為其定義一個額外的接口呢?

  相信您很快就能答出來:“這是因為Set中所包含的資料中不會有重複的元素,而Collection接口作為集合類型接口的根接口,其沒有添加這種限制。”

  是的。JDK提供一個額外的Set接口的确就是出于這個目的。而且這種不添加任何新成員的接口實際上就是Marker Interface。而且在JDK中,Marker Interface還不少。另一個非常著名的Marker Interface就是Clonable接口:

1 public interface Cloneable {
2 }      

  隻是這一次,Marker Interface所受到的禮遇并不相同:無論是在對Prototype模式的講解中還是在其它日常讨論中,其都是作為反面教材來诠釋什麼是一個不良的設計。

硬币的正反面

  那Marker Interface到底是好還是不好呢?如果沒有分析,我們就不會知道為什麼Marker Interface在不同的情況下得到如此不同的評價,也更不會知道如何正确地使用Marker Interface。是以我們先不說結論,而是從接口Set及Clonable兩個截然不同的情況來分析Marker Interface表現出如此差異的原因。

  正能量先行。我們先來分析Set這個Marker Interface表現良好的原因。當使用者看到Set這個接口的時候,他首先想到的就是它是一個集合,而且該集合具有不會存在重複元素這樣一個性質。在對該接口執行個體進行操作的時候,軟體開發人員可以直接通過調用Set接口所繼承過來的各個成員函數來操作它。這些接口所定義的操作需要由Set接口的實作類來定義。是以Set的這種不存在重複元素的性質實際上是由接口的實作類所保證的。如在添加一個元素的時候,我們不必擔心目前是否該元素是否已經在集合中存在了:

1 Set<Item> itemSet = …
2 itemSet.add(item);      

  而對于其它類型的集合,如List,我們就需要檢查元素是否已經在集合中存在,否則其内部将存在着對該元素的重複引用:

1 List<Item> itemList = …
2 if (!itemList.contains(item)) {
3     itemList.add(item);
4 }      

  反過來,另一個Marker Interface Clonable則是臭名昭著的。具體原因已經在Effective Java中的Item 17中已經講得很清楚了。實際上,建立該接口的思路和建立Set接口的思路原本是一緻的:該接口用來标示實作了該接口的類型是可以被拷貝的。其中的一個問題在于,Object類型的clone()函數是受保護的。進而使得使用者代碼不能調用Clonable接口的clone()函數。這樣就要求使用者通過其它方法來實作Clonable接口所表示的語義。進而在代碼中産生了大量的如下代碼:

1 if (obj instanceof Clonable) {
2     ……
3 } else {
4     ……
5 }      

  這樣,如果一個執行個體實作了特定的接口,如Clonable,我們就對它進行特殊的處理。這正是Marker Interface被大量誤用的一種情況:通過判斷一個執行個體是否實作了特定Marker Interface來決定對其進行處理的邏輯。這種對Marker Interface進行使用的代碼實際上破壞了封裝性:Marker Interface執行個體無法通過成員函數等方法控制外部系統對執行個體的使用方式。反過來,實作了Marker Interface的類型到底是被如何處理的則是由使用者代碼決定的。而Marker Interface僅僅是建議使用者代碼對其進行操作。也就是說,Marker Interface擁有了它的使用者相關的資訊,是以其與目前系統中的使用者在邏輯上是互相耦合的,進而使得實作了Marker Interface的類型無法在其它系統中重用。

  而這也就是Effective Java的Item 37所強調的:通過Marker Interface來定義一個類型。我們知道,在定義一個類型的時候,我們不僅僅需要指定表示該類型所需要的資料,更為重要的則是為該類型抽象出用于操作該類型的接口。這些接口規定了該類型的操作方式,進而隔離了該類型的内部實作和使用者代碼。如果我們需要在這些接口之外通過判斷是否是特定類型來執行特殊的處理,那麼也就表示該Marker Interface所定義的類型從語義上來講是并不合适的。

  而且從上面對Set接口以及Clonable接口的比較中可以看出,如果就像Effective Java的Item 37一樣通過Marker Interface來定義類型,那麼對類型進行定義的方式主要分為兩種:從一個接口派生以使得Marker Interface擁有較父接口多出的特殊性質。而如果Marker Interface沒有一個父接口,那麼其應該是Object類所具有的一種特殊性質,并可以通過Object類所提供的各個組成來按該性質進行操作,就像Serializable接口那樣。

  從一個接口派生來定義Marker Interface是比較常見的情況,但是也較容易出錯。一個比較經典的示例仍然是基于長方形為正方形定義一個接口。假設一個系統中已經擁有了一個用來表示長方形的接口:

1 public interface Rectangle {
2     void setWidth(double width);
3     void setHeight(double height);
4     double getArea();
5 }      

  由于正方形是長方形的長和寬都相等的一種特殊情況,是以我們常常認為正方形是一種特殊的長方形。對于這種情況,軟體開發人員就可能決定通過從長方形接口派生來定義一個正方形:

1 public interface Square extends Rectangle {
2 }      

  但是在使用過程中,他會别扭得要死。原因就是因為實際上對長方形所定義的接口,如setWidth(),setHeight()等對于正方形而言完全沒有意義。正方形所需要的是能夠設定它的邊長。是以一個正确定義Marker Interface的前提就是原有接口中的各個成員對于Marker Interface所定義的概念仍然具有明确的意義。

  OK,相信您在看到長方形和正方形這個示例的時候首先想到的就是裡氏替換原則(Liskov Substitution Principle)。但請不要使用裡氏替換原則來判斷一個Marker Interface的定義是否合适。這是因為裡氏替換原則實際上是使用在對象之間的:如果S是T的子類型,那麼S對象就應該能在不改變任何抽象屬性的情況下替換所有的T對象。畢竟,無論如何我們建立的都應該是一個類型的執行個體,而不能直接建立接口的執行個體(基于匿名類的除外)。

  例如對于Set接口,如果我們将所有對Collection接口的使用都替換為對Set接口的使用,那麼至少對下面的語句進行替換時會導緻編譯器報出編譯錯誤:

1 Collection<Item> itemCollection = new ArrayList<Item>();      

  是以,使用裡氏替換原則來判斷一個Marker Interface是否合适實際上真沒有太多意義,這在stackoverflow上也有頗多讨論。

Marker Interface vs. Annotation

  在前面的章節中已經提到過,Marker Interface表示實作該接口的類型具有特殊的性質。也就是說,Marker Interface是該類型的一個特性,也即是該類型的一個中繼資料。而在Java中,另一個可以用來表示類型中繼資料的Java組成是标記。在處理相似問題的情況下,不同的類庫選擇了不同的解決方案。例如Java中的序列化支援實際上是通過Serializable這個Marker Interface來完成的:

1 public class Employee implements java.io.Serializable
2 {
3     public String name;
4     public String address;
5     public transient int SSN;
6     public int number;
7 }      

  而在JPA中,用來對持久化到資料庫這一功能的控制是通過标記來完成的:

1 @Entity
 2 @Table(name = "employee")
 3 public class Employee {
 4     @Column(name = "name", unique = false, nullable = false, length = 40)
 5     private String name;
 6 
 7     @Column(name = "address", unique = false, nullable = false, length = 200)
 8     private String address;
 9 
10     @Column(name = "number", unique = false, nullable = false)
11     private int number;
12 
13     @Transient
14     private float percentageProcessed;
15     ......
16 }      

  随之而來的一個問題就是:我們應該在什麼情況下使用Marker Interface,又在什麼情況下使用标記呢?了解何時使用的前提就是了解兩者之間的優劣。由于兩者是完全不同的兩種文法結構,是以它們之間的差別就顯得非常明顯:

  首先從Marker Interface說起。該方法較标記的好處則在于,通過instanceof就直接能探測一個執行個體是否是一個特定接口的執行個體,而标記則需要通過反射等方法來判斷特定執行個體上是否有特定的标記。除了這個原因之外,對一個執行個體是否實作了某個接口可以在編譯時就可以進行檢查,而一個執行個體是否有某個标記則在運作時才能進行。在使用instanceof的時候,實際上我們是在探測某個執行個體是否是某個類型。是以對于Marker Interface來說,其首先需要有一定的實際意義。

  标記較Marker Interface的好處則在于:其粒度更細。可以說,Marker Interface隻能施行在類型上,而标記則可以施行在多種類型組成上,是以Marker Interface實際上是作為整體行為的一種考慮,而标記則更注重具體細節。一個定義良好的細粒度API可以提供更大的靈活性。而且相較于接口,标記的後續發展能力更強,畢竟在一個接口中添加一個成員函數是一個非常麻煩的事情。

  其實Marker Interface以及标記之間擁有如此大的混淆的很大一部分原因則是兩者在功能上有重複,而且在Java演化過程中出現的時機并不相同,導緻在一些地方仍然擁有Marker Interface的不正當使用。實際上,像Clonable這種值得商榷的Marker Interface在JDK中還有很多很多。之是以在JDK裡面會出現那麼多的Marker Interface,其中一個原因也是因為Java對标記的支援比較晚的緣故。

轉載請注明原文位址并标明轉載:http://www.cnblogs.com/loveis715/p/5094367.html

商業轉載請事先與我聯系:[email protected]