Eclipse中類型擴展機制分析
朱興(zhu_xing@live.cn)
概要
在本篇文章中,將討論如下關(guān)鍵內(nèi)容:
1、 標(biāo)準(zhǔn)的適配器模式,包括類適配器模式和對象適配器模式,及其各自的優(yōu)缺點
2、 Eclipse runtime中的類型擴展機制,包括擴展類型的自動回調(diào)機制、已知擴展類型支持和未知擴展類型支持的討論
3、 插件開發(fā)過程中,合理的使用Eclipse runtime中的類型擴展機制需要注意的地方
說明:假設(shè)讀者大致了解適配器模式,對設(shè)計模式的使用有一點經(jīng)驗
標(biāo)準(zhǔn)適配器模式
適配器模式應(yīng)該是我們?nèi)粘K褂米疃嗟慕Y(jié)構(gòu)型模式之一,“適配器模式把一個類的接口變換為客戶端所期望的另一種接口,從而使原本因接口不匹配而無法在一起工作的兩個類能夠在一起工作”。
適配器模式的實現(xiàn)有兩種方式,借用《Java與模式》一書中的描述,將其稱之為類適配器模式和對象適配器模式,這兩種適配器模式也為我們很好地演繹了繼承和組合這兩種主要的代碼復(fù)用方式。與此同時,繼承和組合這兩種代碼的復(fù)用方式有其各自的適用場景(還是那句話,需求決定我們該怎么干 ~_~),而兩種適配器模式不恰當(dāng)使用本質(zhì)上也是和兩種代碼復(fù)用方式的不恰當(dāng)使用緊密相關(guān)的。
為了便于下面的討論,我們延用設(shè)計模式書籍中使用的三個角色:
Adaptee(源,待適配):需要適配的類型,行為需要被復(fù)用
Target(目標(biāo)):適配的方向,也就是我們期待的接口,客戶端定義的契約
Adapter(適配器):承擔(dān)類型匹配任務(wù)的具體類,Target類型實現(xiàn)類
說明:Adapter是需要實例化使用的,所以是具體類,而Target代表目標(biāo)類型的抽象
下面,我們就簡要看一下類適配器模式和對象適配器模式。具體細(xì)節(jié),可以參加《Java與模式》一書(當(dāng)然,GOF的《設(shè)計模式》也可以 ~_~)
類適配器模式

我們的適配器Adapter同時繼承了目標(biāo)類型Target(當(dāng)然,這是必須的)和源Adaptee。
缺點:
1.如果源Adaptee有一系列類型(有共同的頂級父類型),那我們需要對這一系列類型
中的每個類型(源及其子類型)都產(chǎn)生一個適配器類實現(xiàn),也就是說,會引入大量的類型,帶來類型膨脹的問題。
2.如果Target是一個抽象類,那么這樣做在不支持多繼承的編程語言(例如java、c#)中是行不通的。(注意,既有代碼很有可能是以抽象類來扮演Target類型體系的頂級類型,畢竟很多開發(fā)者并不能非常清晰的來判斷如何區(qū)別使用接口和抽象類)。
優(yōu)點:
由于是繼承了源,所以可以定制源類型的默認(rèn)行為。注意,第一只能定制源類型體系中的某一特定類型,第二,這帶來了實現(xiàn)細(xì)節(jié)的耦合,而且定制有可能帶來用戶的疑惑,因為特定源類型的默認(rèn)行為被修改了。
對象適配器模式

我們的適配器Adapter繼承了目標(biāo)類型Target(當(dāng)然,這是必須的),組合了源Adaptee。
缺點:
無法定制Adaptee的行為。(其實也可以定制,需要引入另外的一個特殊的中間類型)
優(yōu)點:
1. 由于采用了組合的方式,所以可以一次性的將引用的源類型及其子類型都適配到目標(biāo)接口。這避免了從一種類型體系適配到另一種類型體系的過程中,產(chǎn)生大量的類型,避免了類型膨脹的問題(這也是Eclipse想極力避免的)。
2. 最根本的優(yōu)勢是,適合我們開發(fā)過程所面對的絕大多數(shù)適配需求。因為在設(shè)計良好的OO系統(tǒng)中,適配需求往往是將一個類型體系適配到另外一個類型適配體系,而每個類型體系往往都遵循了開閉原則,只暴露了抽象部分給客戶端(橋模式能夠很好的分離抽象和實現(xiàn)并使其獨立變化)。
示例演示和進一步討論
下面我們接著討論一下,類型體系適配需求示意如下:

(說明:上圖中的紅色部分,就是我們所說的內(nèi)部實現(xiàn),不希望對用戶可見)
【客戶端模擬需求】
1 publicclass Customer {
2
3 /**
4
5 *模擬使用場景
6
7 *
8
9 *@paraminstanceInterface2類型的實例
10
11 */
12
13 publicstaticvoid run(Interface2 instance) {
14
15 instance.operation();
16
17 }
18
19 }
20
【Interface2類型對象適配器】
1 publicclass Interface2Adapter implements Interface2{
2
3 private Interface1 interface1Instance;
4
5
6
7 public Interface2Adapter(Interface1 instance) {
8
9 this.interface1Instance = instance;
10
11 }
12
13
14
15 /* (non-Javadoc)
16
17 * @see Interface2#operation()
18
19 */
20
21 publicvoid operation() {
22
23 this.interface1Instance.operation();
24
25 }
26
27 }
28
29
【適配器使用】
1 public class Test {
2
3 public static void main(String[] args) {
4 Interface2 instance1 = new Interface2Adapter(new Interface1Impl());
5 Customer.run(instance1);
6
7
8 Interface2 instance2 = new Interface2Adapter(new Interface1Imp2());
9 Customer.run(instance2);
10 }
11 }
12
我們可以看到,我們的Interface2Adapter構(gòu)造函數(shù)接收的是一個Interface1抽象類型,理論上我們可以用這一個Adapter類完成Interface1類型體系到Interface2類型體系的適配。
【疑問和進一步討論?】
1、我們的適配能夠被自動識別并使用嗎?
分析一下上面的實例演示我們發(fā)現(xiàn),在Java語言中并沒有相應(yīng)的機制來判斷一個類型是否是可以適配的:上面的客戶端Customer需要的目標(biāo)類型是Interface2類型,如果直接給它一個Interface1類型,Customer并不能判斷出來Interface1是否本身是可以被適配到其他類型。
既然Java語言機制中沒有默認(rèn)提供一種判斷特定類型是否可以適配到其他類型的機制,那么考慮到用戶的使用,一些類型在作為API暴露的同時,也需要將其對應(yīng)的適配器作為API的一部分進行暴露。例如,上面例子中的Interface1作為API暴露,如果不暴露對應(yīng)的適配器Interface2Adapter(作為內(nèi)部實現(xiàn)隱藏),那么Interface1的用戶在面對Customer(接受的目標(biāo)類型是Interface2)需求的時候遇到麻煩,他們不知道Interface1可以適配到Interface2,他們就可能需要自行開發(fā)對應(yīng)的適配器。所以,我們開發(fā)一般的Java應(yīng)用的時候,也確實是這么做的,將需要暴露的類型和默認(rèn)提供的一些適配器也一起暴露。但是如果進一步想一下,如果類型的適配需求特別廣泛(注明:很多場景可能需要特定類型提供額外的服務(wù),而這些服務(wù)并不能算是這種類型需要提供的核心服務(wù)),那么可能就需要提供并暴露大量的適配器類型給用戶(這當(dāng)然是有點不優(yōu)雅的)。
有什么更好的辦法嗎?聯(lián)想一下,在Java語言中提供了一些類似的解決辦法可以供參考,例如java.long.Cloneable接口就聲明了一種類型可以被克隆的等等。那么如果我們也定義類似的接口,就叫做IAdaptable,來聲明一種類型可以被適配,再在接口中提供一個操作getAdapter來獲取對應(yīng)的適配器實例,這樣的話我們就可以將我們的適配器類型進行隱藏。這樣,只需要將類型的核心服務(wù)暴露給用戶,如果用戶想請求額外的服務(wù),可以調(diào)用getAdapter來獲取對應(yīng)的目標(biāo)類型(當(dāng)然,我們在類型的API文檔中告訴用戶我們的類型可以默認(rèn)被是適配為那些類型)。按照這個思路我們的代碼修改如下:
//聲明類型具有可以被適配為其他類型的能力
publicinterface IAdaptable {
public Object getAdapter(Class adapter);
}
//修改我們的Interface1類型接口定義,聲明其可以被適配為別的類型
publicinterface Interface1 extends IAdaptable{
publicvoid operation();
}
//修改我們的Interface1的類型實現(xiàn),實現(xiàn)IAdaptable.getAdapter邏輯
1 publicclass Interface1Impl implements Interface1{
2
3 public void operation() {
4 //todo:
5 }
6
7 public Object getAdapter(Class adapter) {
8 //實現(xiàn)適配邏輯,適配到Interface2類型
9 if (Interface2.class == adapter)
10 return new Interface2Adapter(this);
11 return null;
12 }
13 }
14
15
//修改我們的客戶端代碼
1 public class Customer {
2 public static void run(Object source) {
3 //首先檢查本身是否為Interface2類型
4 if (source instanceof Interface2)
5 ((Interface2) source).operation();
6
7 //檢查類型是否可以被適配,如果可以,嘗試往Interface2類型適配
8 else if (source instanceof IAdaptable) {
9 IAdaptable adaptable = ((IAdaptable)source);
10 Object instance = adaptable.getAdapter(Interface2.class);
11 if (instance != null)
12 ((Interface2) instance).operation();
13 }
14 }
15 }
16
說明:現(xiàn)在我們的客戶端接受的是一個弱類型了(其實應(yīng)該是半弱類型,有相應(yīng)的類型檢查)
2、在能夠被自動識別和使用的基礎(chǔ)上,能否允許用戶參與提供適配的過程?
//接著修改我們的Interface1Impl實現(xiàn)
1 public class Interface1Impl implements Interface1{
2 public void operation() {
3 //todo:
4 }
5
6 public Object getAdapter(Class adapter) {
7 //自定義適配邏輯(已知擴展),適配到Interface2類型
8 if (Interface2.class == adapter)
9 return new Interface2Adapter(this);
10
11 //TODO: 調(diào)用別人貢獻的適配邏輯
12 return AdapterManager.getAdapter(this, adapter)
13 }
14 }
15
上面的代碼僅僅是示意性的代碼,AdapterManager類型也是暫時杜撰出來的。我們可以假設(shè)目前AdapterManager的作用是管理別人貢獻的適配邏輯,用只需要將對應(yīng)的適配邏輯注冊到AdapterManager中,就可以參與到Interface1Impl的適配邏輯中,太完美了~_~。
Eclipse中的類型擴展機制
Eclipse平臺本身是一個微內(nèi)核(micro kernel)加核心插件(core plug-ins)的結(jié)構(gòu),微內(nèi)核是指Ecllipse的OSGI實現(xiàn)Equinox(當(dāng)然包含了擴展點機制的支持),這里的核心插件就是指:runtime、resource、workbench。而這里的平臺運行時為我們提供的主要的特性是:類型擴展支持(IAdaptable、IAdapterFactory、IAdapterManager)和線程支持(Job、ISchedulingRule)。我們今天要討論的就是類型擴展支持。
Eclipse中的類型擴展需求
在Eclipse中,一個特性類型經(jīng)常會收到這樣的請求:請?zhí)峁╊~外的服務(wù)。例如,用戶在一個視圖(提供了workbench seleciton service服務(wù),)中選中了一個元素,這時候視圖會發(fā)送選擇事件出去,告訴其他視圖用戶選中了一個元素,屬性視圖(Properties View)接受到了這個選擇事件,就會向選中的對象發(fā)送請求:請?zhí)峁┠軌蛟趯傩砸晥D顯示的服務(wù)。請注意,這種場景在Eclipse太普遍不過了,根源于Eclipse定位為一個可擴展的平臺,便于擴展和集成是Eclipse開發(fā)者的最大需求之一!!!
我們接著分析一下這種額外的服務(wù)。假設(shè)目前我們在定義一種類型,我們可能會將類型提供的服務(wù)劃分為兩類,以org.eclipse.core.resources.IResource為例:
核心服務(wù):提供對底層資源的句柄代理(提供move、delete、copy、getFullPath等操作),直接以API的方式暴露給用戶。
額外服務(wù):需要類型擴展。進一步將額外服務(wù)劃分為兩類:已知額外服務(wù)(類型定義時提供)和未知額外服務(wù)(類型發(fā)布之后,用戶參與貢獻的額外服務(wù))。對應(yīng)的擴展類型我們將其稱之為已知擴展類型和未知擴展類型。對IResource類型來說,由于資源管理是Eclipse底層的核心模塊,在這個層面上面,它不可能為上層的功能模塊提供類型擴展,因為這違反了模塊分層劃分的原則。但是,IResource作為一個底層類型,肯定是要求可以擴展的,所以Eclipse針對IResource類型采用了邀請外部擴展(允許外部提供未知類型的適配器)的方式,詳細(xì)信息會在下面分析。
Eclipse中的類型擴展機制
【Eclipse類型擴展機制的核心本質(zhì)】
Eclipse中的類型擴展機制,核心內(nèi)容就是補充了Java語言的缺陷提供了自定義的類型擴展機制,基本上就是回答了上面的兩個問題:
1、我們的適配能夠被自動識別并使用嗎?
Eclipse提供了org.eclipse.core.runtime.IAdaptable,用來聲明特定類型是否是可以
被適配的(adaptable)。并提供了默認(rèn)適配器類org.eclipse.core.runtime.PlatformObject。
在Eclipse中,只要是繼承自以上接口或者抽象類的類型,就被視為“可擴展類型”。
2、在能夠被自動識別和使用的基礎(chǔ)上,能否允許用戶參與提供適配的過程?
Eclipse為我們提供了IAdapterFactory(org.eclipse.core.runtime.IAdapterFactory)
和IAdapterManager(org.eclipse.core.runtime.IAdapterManager)。用可以將自定義的
適配邏輯放入IadapterFactory中,然后注冊到IadapterManager中,注冊方式如下:
1、 通過代碼靜態(tài)注冊(一般選擇在插件啟動時)
IAdapterFactory.registerAdapters(IAdapterFactory factory, Class adaptable);
2、 通過擴展點方式動態(tài)掛入
org.eclipse.core.runtime.adapters擴展點(詳細(xì)信息參加Eclipse help)
【已知類型擴展和未知類型擴展】
再接下來的討論之前,我們先來區(qū)分兩個概念(這兩個概念是俺大致起的,湊合著看吧~_~)。我們修改一下Interface1Impl實現(xiàn)如下:
1 public abstract class PlatformObject implements IAdaptable {
2 public Object getAdapter(Class adapter) {
3 return AdapterManager.getDefault().getAdapter(this, adapter);
4 }
5
6 }
7
1 public class Interface1Impl extends PlatformObject{
2 public void operation() {
3 //todo:
4 }
5
6 /*
7 * 已知擴展類型:Interface2
8 * 未知擴展類型:遵循Eclipse”邀請法則“,兼容未知擴展類型
9 * @see PlatformObject#getAdapter(java.lang.Class)
10 */
11
12 public Object getAdapter(Class adapter) {
13 //已知擴展類型,適配到Interface2類型
14 if (Interface2.class == adapter)
15 return new Interface2Adapter(this);
16
17 // 兼容未知擴展類型,通過PlatformObject.getAdapter查詢
18 return super.getAdapter(adapter);
19 }
20 }
21
22
已知擴展類型:類型定義者默認(rèn)提供了對應(yīng)的適配器實現(xiàn),例如Interface2適配的Interface2Adapter。未知擴展類型:以IAdapterFactory方式提供的類型統(tǒng)稱為未知擴展類型,通過IAdapterManager#getAdapter方法查詢使用。可能有人會問,我在定義一個類型的時候,就把類型適配邏輯放入到一個adapter factory中注冊到了IAdapterManager,這應(yīng)該算是已知擴展類型啊?但是對于Eclipse來說,其默認(rèn)會遵守公平法則,即一個IAdapterFactory無論是那個開發(fā)者提供的,它并不關(guān)心,都是平等的。
使用Eclipse中的類型擴展機制注意點
【已知類型擴展和未知類型擴展】
已知擴展類型對于我們來說是很有把握的,既考慮到了靈活性又兼顧到了穩(wěn)定性。而對于未知擴展類型來說,雖然有很大的靈活性,但是也帶來了不穩(wěn)定性,畢竟這個適配類型不是自己提供的。這邊就需要有兩個事情需要考慮:
1、 是否需要提供對未知擴展類型的支持
Eclipse底層定義的很多類型都提供了對未知擴展類型的兼容,咱們自己定義的類型倒不一定。如果你能確定自定義的類型沒有類型擴展的需求,那么就不要繼承IAdaptable接口或者PlatformObject類。如果你能確定自定義的類型有非常明確的類型擴展需求,并且能確定需要適配到那幾種類型,那就只提供已知類型擴展。在確定自定義的類型有廣泛的類型擴展需求的情況下,再兼容未知擴展類型,這種類型一般來說是處于你應(yīng)用中的底層模塊中。
2、 明確已知擴展類型和未知擴展類型之間的優(yōu)先級
建議是將已知擴展類型設(shè)為高優(yōu)先級,對IAdapterManager#getAdapter查詢得到的未知擴展類型設(shè)為低優(yōu)先級別。注意,外部提供的未知擴展類型之際是不存在顯示的優(yōu)先級別的,IAdapterManager#getAdapter會對注冊的IAdapterFacotry進行深度優(yōu)先查找,查詢到第一個合適的就直接返回。
【如何提供類型擴展】
1、 不能破壞模塊分層原則,基礎(chǔ)模塊不應(yīng)該為上層功能模塊的類型提供適配服務(wù)。
上層功能模塊是建立在基礎(chǔ)模塊的基礎(chǔ)之上的,如果讓基礎(chǔ)模塊為上層功能模塊中的類型提供適配服務(wù),那肯定會破壞模塊分層的原則,也就沒有所謂的基礎(chǔ)模塊和上層功能模塊的概念了。
如果基礎(chǔ)模塊中的類型需要適配到特定上層模塊中的類型,也應(yīng)該是該上層模塊自己以IAdapterFactory的方式注冊,上層模塊自己創(chuàng)建對應(yīng)的IAdapterFactory和Adapter實現(xiàn)。(當(dāng)然,這就需要待適配的基礎(chǔ)類型本身兼容外部貢獻的未知擴展類型,PlatformObject的默認(rèn)實現(xiàn)就是兼容外部貢獻的未知擴展類型的)。
如果上層模塊中的類型需要適配到底層模塊中的類型,那就在上層模塊中提供適配邏輯,這是很自然的事情。Eclipse的開發(fā)者法則中就有一條:IResource適配法則,鼓勵開發(fā)者提供自定義類型到IResource系列類型的是適配邏輯。但是,同樣要避免一點:非UI模塊不應(yīng)該為UI類型提供適配,否則會導(dǎo)致核心功能和UI的緊耦合。
如果是平行層面的模塊呢?例如兩個上層功能模塊之間可能需要進行類型適配,這個問題就是仁者見仁、智者見智了。個人的建議是,不要輕易的做這種適配,應(yīng)該首先考慮一下是否可以通過底層模塊作為橋梁,例如首先將模塊A中的類型適配為底層模塊中的類型,然后再將底層模塊中的類型適配為上層模塊B中的類型。如果還行不通,那就請反思一下現(xiàn)有模塊的設(shè)計劃分是否有問題,例如這兩個上層功能模塊是否劃分的過細(xì)。如果還行不通,那怎么做就…
看到過有些插件應(yīng)用,里面也有幾十個插件工程。按照需求分析一下,肯定是有基礎(chǔ)模塊和上層功能模塊的概念,但是實際的代碼中去基本上反應(yīng)不出來,可能對于很多人來說,管他基礎(chǔ)模塊還是上層模塊呢,代碼能運行就OK了。如果您現(xiàn)在的插件應(yīng)用已經(jīng)做了較好的底層模塊和上層功能規(guī)模的劃分,千萬不要因為誤用了Eclipse類型擴展機制而導(dǎo)致破壞了分層,那就非常得不償失了。~_~
2、 不能破壞Eclipse分層原則,非UI插件中定義的類型不應(yīng)該提供UI類型適配服務(wù)。
一定要遵守,否則會直接破壞Eclipse倡導(dǎo)的分層原則,否則會造成核心功能和UI的緊密耦合,示意圖如下:

如上圖所示:
1、 如果上層模塊A中的非UI類型需要適配到底層模塊core中的非UI類型,請在A.core插件中提供適配邏輯
2、 如果上層模塊A中的非UI類型需要適配為底層模塊UI中的UI類型,請在A.UI插件中提供適配邏輯(避免非UI和UI耦合)
3、 如果底層模塊Core中的非UI類型需要適配到上層模塊A.UI中的UI類型,請在A.UI插件中提供適配邏輯
4、 如果…,請注意一般UI類型到UI類型的適配基本上情況很少
【Eclipse類型擴展機制的幾個缺陷】
未知類型的沖突問題:前面我們分析過,對多個未知擴展類型之間不存在明顯的優(yōu)先級別,IAdapterManager#getAdapter會對注冊的IAdapterFacotry進行深度優(yōu)先查找,查詢到第一個合適的就直接返回。如果想獲取高的優(yōu)先級,評估一下是否可以做為已知擴展類型提供,或者直接由定義該類型的插件直接提供IAdapterFactory注冊。關(guān)于優(yōu)先級的問題,一般也沒有什么特別好的解決辦法。如果Eclipse提供了優(yōu)先級的處理,那么可能既增加了用戶使用的復(fù)雜度,同時也并不能解決沖突的問題。
IAdapterFactory的有效性問題:IadapterManager提供了兩種getAdapter和loadAdapter兩種接口。如果以org.eclipse.core.runtime.adapters擴展點的方式掛入了IAdapterFactory實現(xiàn),getAdapter的方式不會去強制啟動你的插件,而loadAdapter的方式會去強制啟動你的插件。如果是以代碼的方式注冊的,那也直接取決于所在插件是否啟動(你的IAdapterFactory注冊代碼是否被執(zhí)行了)。這一點,很多時候會給人造成很大的疑惑
【如何降低Eclipse類型擴展機制所帶來的編程復(fù)雜度】
Eclipse平臺運行時的類型擴展機制為開發(fā)者帶來了很大的靈活性,但是同時肯定也帶來編程復(fù)雜度,尤其是對于經(jīng)驗很少的Eclipse插件開發(fā)人員。個人經(jīng)驗,那就是如果你的類型實現(xiàn)了IAdaptable,請在API文檔中說明,提供內(nèi)容如下:
1、 提供了那些已知擴展類型。
2、 是否邀請外部用戶貢獻適配邏輯。
例如org.eclipse.core.resources.IResource接口的API文檔中有如下信息:
Resources implement the <code>IAdaptable</code> interface;
extensions are managed by the platform's adapter manager.
總結(jié)
寫這篇文章的最大目的是希望解釋兩個事情:
Eclipse的類型擴展機制是怎么來的
使用Eclipse的類型擴展機制應(yīng)該注意什么
希望對大家有所幫助!
本博客中的所有文章、隨筆除了標(biāo)題中含有引用或者轉(zhuǎn)載字樣的,其他均為原創(chuàng)。轉(zhuǎn)載請注明出處,謝謝!