Eclipse中类型扩展机制分析
朱兴(zhu_xing@live.cn)
概要
在本篇文章中,将讨论如下关键内容:
1、 标准的适配器模式,包括类适配器模式和对象适配器模式,及其各自的优缺点
2、 Eclipse runtime中的类型扩展机制,包括扩展类型的自动回调机制、已知扩展类型支持和未知扩展类型支持的讨论
3、 插件开发过程中,合理的使用Eclipse runtime中的类型扩展机制需要注意的地方
说明:假设读者大致了解适配器模式,对设计模式的使用有一点经验
标准适配器模式
适配器模式应该是我们日常所使用最多的结构型模式之一,“适配器模式把一个类的接口变换为客户端所期望的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作”。
适配器模式的实现有两种方式,借用《Java与模式》一书中的描述,将其称之为类适配器模式和对象适配器模式,这两种适配器模式也为我们很好地演绎了继承和组合这两种主要的代码复用方式。与此同时,继承和组合这两种代码的复用方式有其各自的适用场景(还是那句话,需求决定我们该怎么干 ~_~),而两种适配器模式不恰当使用本质上也是和两种代码复用方式的不恰当使用紧密相关的。
为了便于下面的讨论,我们延用设计模式书籍中使用的三个角色:
Adaptee(源,待适配):需要适配的类型,行为需要被复用
Target(目标):适配的方向,也就是我们期待的接口,客户端定义的契约
Adapter(适配器):承担类型匹配任务的具体类,Target类型实现类
说明:Adapter是需要实例化使用的,所以是具体类,而Target代表目标类型的抽象
下面,我们就简要看一下类适配器模式和对象适配器模式。具体细节,可以参加《Java与模式》一书(当然,GOF的《设计模式》也可以 ~_~)
类适配器模式
我们的适配器Adapter同时继承了目标类型Target(当然,这是必须的)和源Adaptee。
缺点:
1.如果源Adaptee有一系列类型(有共同的顶级父类型),那我们需要对这一系列类型
中的每个类型(源及其子类型)都产生一个适配器类实现,也就是说,会引入大量的类型,带来类型膨胀的问题。
2.如果Target是一个抽象类,那么这样做在不支持多继承的编程语言(例如java、c#)中是行不通的。(注意,既有代码很有可能是以抽象类来扮演Target类型体系的顶级类型,毕竟很多开发者并不能非常清晰的来判断如何区别使用接口和抽象类)。
优点:
由于是继承了源,所以可以定制源类型的默认行为。注意,第一只能定制源类型体系中的某一特定类型,第二,这带来了实现细节的耦合,而且定制有可能带来用户的疑惑,因为特定源类型的默认行为被修改了。
对象适配器模式
我们的适配器Adapter继承了目标类型Target(当然,这是必须的),组合了源Adaptee。
缺点:
无法定制Adaptee的行为。(其实也可以定制,需要引入另外的一个特殊的中间类型)
优点:
1. 由于采用了组合的方式,所以可以一次性的将引用的源类型及其子类型都适配到目标接口。这避免了从一种类型体系适配到另一种类型体系的过程中,产生大量的类型,避免了类型膨胀的问题(这也是Eclipse想极力避免的)。
2. 最根本的优势是,适合我们开发过程所面对的绝大多数适配需求。因为在设计良好的OO系统中,适配需求往往是将一个类型体系适配到另外一个类型适配体系,而每个类型体系往往都遵循了开闭原则,只暴露了抽象部分给客户端(桥模式能够很好的分离抽象和实现并使其独立变化)。
示例演示和进一步讨论
下面我们接着讨论一下,类型体系适配需求示意如下:
(说明:上图中的红色部分,就是我们所说的内部实现,不希望对用户可见)
【客户端模拟需求】
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构造函数接收的是一个Interface1抽象类型,理论上我们可以用这一个Adapter类完成Interface1类型体系到Interface2类型体系的适配。
【疑问和进一步讨论?】
1、我们的适配能够被自动识别并使用吗?
分析一下上面的实例演示我们发现,在Java语言中并没有相应的机制来判断一个类型是否是可以适配的:上面的客户端Customer需要的目标类型是Interface2类型,如果直接给它一个Interface1类型,Customer并不能判断出来Interface1是否本身是可以被适配到其他类型。
既然Java语言机制中没有默认提供一种判断特定类型是否可以适配到其他类型的机制,那么考虑到用户的使用,一些类型在作为API暴露的同时,也需要将其对应的适配器作为API的一部分进行暴露。例如,上面例子中的Interface1作为API暴露,如果不暴露对应的适配器Interface2Adapter(作为内部实现隐藏),那么Interface1的用户在面对Customer(接受的目标类型是Interface2)需求的时候遇到麻烦,他们不知道Interface1可以适配到Interface2,他们就可能需要自行开发对应的适配器。所以,我们开发一般的Java应用的时候,也确实是这么做的,将需要暴露的类型和默认提供的一些适配器也一起暴露。但是如果进一步想一下,如果类型的适配需求特别广泛(注明:很多场景可能需要特定类型提供额外的服务,而这些服务并不能算是这种类型需要提供的核心服务),那么可能就需要提供并暴露大量的适配器类型给用户(这当然是有点不优雅的)。
有什么更好的办法吗?联想一下,在Java语言中提供了一些类似的解决办法可以供参考,例如java.long.Cloneable接口就声明了一种类型可以被克隆的等等。那么如果我们也定义类似的接口,就叫做IAdaptable,来声明一种类型可以被适配,再在接口中提供一个操作getAdapter来获取对应的适配器实例,这样的话我们就可以将我们的适配器类型进行隐藏。这样,只需要将类型的核心服务暴露给用户,如果用户想请求额外的服务,可以调用getAdapter来获取对应的目标类型(当然,我们在类型的API文档中告诉用户我们的类型可以默认被是适配为那些类型)。按照这个思路我们的代码修改如下:
//声明类型具有可以被适配为其他类型的能力
publicinterface IAdaptable {
public Object getAdapter(Class adapter);
}
//修改我们的Interface1类型接口定义,声明其可以被适配为别的类型
publicinterface Interface1 extends IAdaptable{
publicvoid operation();
}
//修改我们的Interface1的类型实现,实现IAdaptable.getAdapter逻辑
1 publicclass Interface1Impl implements Interface1{
2
3 public void operation() {
4 //todo:
5 }
6
7 public Object getAdapter(Class adapter) {
8 //实现适配逻辑,适配到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
说明:现在我们的客户端接受的是一个弱类型了(其实应该是半弱类型,有相应的类型检查)
2、在能够被自动识别和使用的基础上,能否允许用户参与提供适配的过程?
//接着修改我们的Interface1Impl实现
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: 调用别人贡献的适配逻辑
12 return AdapterManager.getAdapter(this, adapter)
13 }
14 }
15
上面的代码仅仅是示意性的代码,AdapterManager类型也是暂时杜撰出来的。我们可以假设目前AdapterManager的作用是管理别人贡献的适配逻辑,用只需要将对应的适配逻辑注册到AdapterManager中,就可以参与到Interface1Impl的适配逻辑中,太完美了~_~。
Eclipse中的类型扩展机制
Eclipse平台本身是一个微内核(micro kernel)加核心插件(core plug-ins)的结构,微内核是指Ecllipse的OSGI实现Equinox(当然包含了扩展点机制的支持),这里的核心插件就是指:runtime、resource、workbench。而这里的平台运行时为我们提供的主要的特性是:类型扩展支持(IAdaptable、IAdapterFactory、IAdapterManager)和线程支持(Job、ISchedulingRule)。我们今天要讨论的就是类型扩展支持。
Eclipse中的类型扩展需求
在Eclipse中,一个特性类型经常会收到这样的请求:请提供额外的服务。例如,用户在一个视图(提供了workbench seleciton service服务,)中选中了一个元素,这时候视图会发送选择事件出去,告诉其他视图用户选中了一个元素,属性视图(Properties View)接受到了这个选择事件,就会向选中的对象发送请求:请提供能够在属性视图显示的服务。请注意,这种场景在Eclipse太普遍不过了,根源于Eclipse定位为一个可扩展的平台,便于扩展和集成是Eclipse开发者的最大需求之一!!!
我们接着分析一下这种额外的服务。假设目前我们在定义一种类型,我们可能会将类型提供的服务划分为两类,以org.eclipse.core.resources.IResource为例:
核心服务:提供对底层资源的句柄代理(提供move、delete、copy、getFullPath等操作),直接以API的方式暴露给用户。
额外服务:需要类型扩展。进一步将额外服务划分为两类:已知额外服务(类型定义时提供)和未知额外服务(类型发布之后,用户参与贡献的额外服务)。对应的扩展类型我们将其称之为已知扩展类型和未知扩展类型。对IResource类型来说,由于资源管理是Eclipse底层的核心模块,在这个层面上面,它不可能为上层的功能模块提供类型扩展,因为这违反了模块分层划分的原则。但是,IResource作为一个底层类型,肯定是要求可以扩展的,所以Eclipse针对IResource类型采用了邀请外部扩展(允许外部提供未知类型的适配器)的方式,详细信息会在下面分析。
Eclipse中的类型扩展机制
【Eclipse类型扩展机制的核心本质】
Eclipse中的类型扩展机制,核心内容就是补充了Java语言的缺陷提供了自定义的类型扩展机制,基本上就是回答了上面的两个问题:
1、我们的适配能够被自动识别并使用吗?
Eclipse提供了org.eclipse.core.runtime.IAdaptable,用来声明特定类型是否是可以
被适配的(adaptable)。并提供了默认适配器类org.eclipse.core.runtime.PlatformObject。
在Eclipse中,只要是继承自以上接口或者抽象类的类型,就被视为“可扩展类型”。
2、在能够被自动识别和使用的基础上,能否允许用户参与提供适配的过程?
Eclipse为我们提供了IAdapterFactory(org.eclipse.core.runtime.IAdapterFactory)
和IAdapterManager(org.eclipse.core.runtime.IAdapterManager)。用可以将自定义的
适配逻辑放入IadapterFactory中,然后注册到IadapterManager中,注册方式如下:
1、 通过代码静态注册(一般选择在插件启动时)
IAdapterFactory.registerAdapters(IAdapterFactory factory, Class adaptable);
2、 通过扩展点方式动态挂入
org.eclipse.core.runtime.adapters扩展点(详细信息参加Eclipse help)
【已知类型扩展和未知类型扩展】
再接下来的讨论之前,我们先来区分两个概念(这两个概念是俺大致起的,凑合着看吧~_~)。我们修改一下Interface1Impl实现如下:
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
已知扩展类型:类型定义者默认提供了对应的适配器实现,例如Interface2适配的Interface2Adapter。未知扩展类型:以IAdapterFactory方式提供的类型统称为未知扩展类型,通过IAdapterManager#getAdapter方法查询使用。可能有人会问,我在定义一个类型的时候,就把类型适配逻辑放入到一个adapter factory中注册到了IAdapterManager,这应该算是已知扩展类型啊?但是对于Eclipse来说,其默认会遵守公平法则,即一个IAdapterFactory无论是那个开发者提供的,它并不关心,都是平等的。
使用Eclipse中的类型扩展机制注意点
【已知类型扩展和未知类型扩展】
已知扩展类型对于我们来说是很有把握的,既考虑到了灵活性又兼顾到了稳定性。而对于未知扩展类型来说,虽然有很大的灵活性,但是也带来了不稳定性,毕竟这个适配类型不是自己提供的。这边就需要有两个事情需要考虑:
1、 是否需要提供对未知扩展类型的支持
Eclipse底层定义的很多类型都提供了对未知扩展类型的兼容,咱们自己定义的类型倒不一定。如果你能确定自定义的类型没有类型扩展的需求,那么就不要继承IAdaptable接口或者PlatformObject类。如果你能确定自定义的类型有非常明确的类型扩展需求,并且能确定需要适配到那几种类型,那就只提供已知类型扩展。在确定自定义的类型有广泛的类型扩展需求的情况下,再兼容未知扩展类型,这种类型一般来说是处于你应用中的底层模块中。
2、 明确已知扩展类型和未知扩展类型之间的优先级
建议是将已知扩展类型设为高优先级,对IAdapterManager#getAdapter查询得到的未知扩展类型设为低优先级别。注意,外部提供的未知扩展类型之际是不存在显示的优先级别的,IAdapterManager#getAdapter会对注册的IAdapterFacotry进行深度优先查找,查询到第一个合适的就直接返回。
【如何提供类型扩展】
1、 不能破坏模块分层原则,基础模块不应该为上层功能模块的类型提供适配服务。
上层功能模块是建立在基础模块的基础之上的,如果让基础模块为上层功能模块中的类型提供适配服务,那肯定会破坏模块分层的原则,也就没有所谓的基础模块和上层功能模块的概念了。
如果基础模块中的类型需要适配到特定上层模块中的类型,也应该是该上层模块自己以IAdapterFactory的方式注册,上层模块自己创建对应的IAdapterFactory和Adapter实现。(当然,这就需要待适配的基础类型本身兼容外部贡献的未知扩展类型,PlatformObject的默认实现就是兼容外部贡献的未知扩展类型的)。
如果上层模块中的类型需要适配到底层模块中的类型,那就在上层模块中提供适配逻辑,这是很自然的事情。Eclipse的开发者法则中就有一条:IResource适配法则,鼓励开发者提供自定义类型到IResource系列类型的是适配逻辑。但是,同样要避免一点:非UI模块不应该为UI类型提供适配,否则会导致核心功能和UI的紧耦合。
如果是平行层面的模块呢?例如两个上层功能模块之间可能需要进行类型适配,这个问题就是仁者见仁、智者见智了。个人的建议是,不要轻易的做这种适配,应该首先考虑一下是否可以通过底层模块作为桥梁,例如首先将模块A中的类型适配为底层模块中的类型,然后再将底层模块中的类型适配为上层模块B中的类型。如果还行不通,那就请反思一下现有模块的设计划分是否有问题,例如这两个上层功能模块是否划分的过细。如果还行不通,那怎么做就…
看到过有些插件应用,里面也有几十个插件工程。按照需求分析一下,肯定是有基础模块和上层功能模块的概念,但是实际的代码中去基本上反应不出来,可能对于很多人来说,管他基础模块还是上层模块呢,代码能运行就OK了。如果您现在的插件应用已经做了较好的底层模块和上层功能规模的划分,千万不要因为误用了Eclipse类型扩展机制而导致破坏了分层,那就非常得不偿失了。~_~
2、 不能破坏Eclipse分层原则,非UI插件中定义的类型不应该提供UI类型适配服务。
一定要遵守,否则会直接破坏Eclipse倡导的分层原则,否则会造成核心功能和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类型扩展机制的几个缺陷】
未知类型的冲突问题:前面我们分析过,对多个未知扩展类型之间不存在明显的优先级别,IAdapterManager#getAdapter会对注册的IAdapterFacotry进行深度优先查找,查询到第一个合适的就直接返回。如果想获取高的优先级,评估一下是否可以做为已知扩展类型提供,或者直接由定义该类型的插件直接提供IAdapterFactory注册。关于优先级的问题,一般也没有什么特别好的解决办法。如果Eclipse提供了优先级的处理,那么可能既增加了用户使用的复杂度,同时也并不能解决冲突的问题。
IAdapterFactory的有效性问题:IadapterManager提供了两种getAdapter和loadAdapter两种接口。如果以org.eclipse.core.runtime.adapters扩展点的方式挂入了IAdapterFactory实现,getAdapter的方式不会去强制启动你的插件,而loadAdapter的方式会去强制启动你的插件。如果是以代码的方式注册的,那也直接取决于所在插件是否启动(你的IAdapterFactory注册代码是否被执行了)。这一点,很多时候会给人造成很大的疑惑
【如何降低Eclipse类型扩展机制所带来的编程复杂度】
Eclipse平台运行时的类型扩展机制为开发者带来了很大的灵活性,但是同时肯定也带来编程复杂度,尤其是对于经验很少的Eclipse插件开发人员。个人经验,那就是如果你的类型实现了IAdaptable,请在API文档中说明,提供内容如下:
1、 提供了那些已知扩展类型。
2、 是否邀请外部用户贡献适配逻辑。
例如org.eclipse.core.resources.IResource接口的API文档中有如下信息:
Resources implement the <code>IAdaptable</code> interface;
extensions are managed by the platform's adapter manager.
总结
写这篇文章的最大目的是希望解释两个事情:
Eclipse的类型扩展机制是怎么来的
使用Eclipse的类型扩展机制应该注意什么
希望对大家有所帮助!
本博客中的所有文章、随笔除了标题中含有引用或者转载字样的,其他均为原创。转载请注明出处,谢谢!