EMF介绍系列(三、定制应用程序的基本方法)
借助EMF的帮助,不用亲自编写一行代码就可以生成一个完整的应用程序,你是不是对EMF有些感兴趣了?不过生成的应用程序看起来都像是从同一个模子里出来的,即 一个多页编辑器,一个大纲视图和属性页,这当然无法满足所有人的需求。不用耽心,只要了解EMF的机制,按照我们的要求修改这个应用程序并不是一件很困难的 事情。
首先大概的看一下EMF为我们生成了哪些东西吧。按照前文的操作,EMF应该一共生成了四个插件项目:com.my.shop、 com.my.shop.edit、com.my.shop.editor和com.my.shop.tests,其中最后一个项目是方便我们编写单元测 试的框架代码,这里我们先不管它,暂时把注意力集中在前三个项目上。
第一个项目是模型部分,主要包含你定义的ecore模型里各类型(EClass,在ecore元模型里类型称为EClass,属性称为 EAttribute)对应的java接口和缺省的实现类代码,例如Product.java和ProductImpl.java,它们分别被放置在 com.my.shop和com.my.shop.impl包里;一个工厂类(ShopFactory)使用工厂模式创建新的模型实例;一个 Package类(ShopPackage)维护关于元模型的信息,提供了一堆静态成员变量;此外还生成了ShopAdapterFactory和 ShopSwitch这两个类,它们是可选的,它们俩的作用这里卖个关子暂时先不说。
第二个项目是.edit部分,这里面包含了一系列ItemProvider类,用来为在jface的各种查看器(Viewer)里显示这些模型对象 提供便利,以CategoryItemProvider为例,它实现了IStructuredItemContentProvider、 ITreeItemContentProvider和IItemLabelProvider这些接口,注意把这些接口名字中的"Item"去掉就是 jface里需要的Provider,可以把这些带有"Item"字样的Provider想象成对jface相应Provider的包装。有了这些 Provider,在应用程序里使用jface的TreeViewer、TableViewer等查看器就很方便了。(前面讲GEF的一个帖子里曾利用 EMF构造模型,当时使用的就仅仅是模型部分,因为并未用到jface查看器。所以视你的应用程序而定,可以只生成模型部分来用,.edit部分依赖模型部分,而反之不然。)
第三个项目是编辑器,这个部分依赖.edit部分,主要包含了几个UI方面的类。EMF为我们生成的这个编辑器有两个用途:一是让我们可以不用从零开始,而是在这个编辑器的基础上进行修改得到自己的编辑器; 二是通过这些代码展示怎样在应用程序里使用前两个项目里的那些类,编辑器包含那么多Tab正是为了演示各种查看器的用法。下面来说一下怎样定制生成的应用 程序。
一、修改ecore模型和genmodel模型
在ecore模型和genmodel模型里我们可以通过修改一些属性改变所生成的代码,例如希望新创建的类别和产品的名称不是空字符串而是" (Unnamed)",就可以在类图里修改NamedElement类的name属性的"Default Value Literal"属性(没错,属性的属性。见图1)。修改ecore模型后,必须更新genmodel模型,方法是在Package Explorer里右键单击shop.genmodel文件,在弹出菜单里选择"Reload...",这样genmodel会从修改后的ecore模型 里获得修改过的信息。之后,再次从genmodel模型生成一遍代码,这样得到的程序运行后,类别和产品的名称缺省就是"(Unnamed)"了。在 genmodel模型里则可以定制更多属性,例如所生成的每个项目的id、生成类所在的包名(本例中为com.my)、类名前缀(本例中为Shop)、是否生成图标、标准属性页里各属性的分类等等。
图1 在EclipseUML类图里修改属性的缺省值
有人对 ecore模型和genmodel模型各自的用途搞不清楚,其实因为JET是直接通过genmodel生成代码的,所以像是否生成图标这些信息放在 genmodel里是比较合适的,而ecore模型里定义的是各个类型以及之间的关系,所以像属性缺省值这样的信息是应该放在ecore里没错的。
二、直接修改生成的代码
前面的方式是让EMF生成我们想要的程序,好处是非常直观和方便,缺点是只有有限的信息可被定制,而一个复杂的应用程序是不太可能仅仅通过填写一些 属性就被生成出来的,所以我们需要代码级的定制。有一个事实你需要知道:无论对ecore还是genmodel模型定制,产生的结果最终都要反映到生成的 代码。让我们对比一下修改名称缺省值前后的代码(因为类图里NamedElement被定义为抽象类,所以这段代码的位置在 CategoryImpl.java和ProductImpl.java里),这是之前生成的代码:
* The default value of the '{@link #getName() <em>Name</em>}' attribute.
* <!-- begin-user-doc -->
* <!-- end-user-doc -->
* @see #getName()
* @generated
* @ordered
*/
protected static final String NAME_EDEFAULT = null;
下面是定制之后的代码:
* The default value of the '{@link #getName() <em>Name</em>}' attribute.
* <!-- begin-user-doc -->
* <!-- end-user-doc -->
* @see #getName()
* @generated
* @ordered
*/
protected static final String NAME_EDEFAULT = "(Unnamed)";
可以看到只是NAME_EDEFAULT的值发生了变化。也就是说,如果我们不修改ecore模型,而是手工修改这段代码也能达到同样的目的。对于熟悉 EMF的开发人员来讲,修改代码很多时候甚至比修改ecore/genmodel模型更加快速(因为省去了reload genmodel和生成代码这两步),但一定要记住,在修改代码的同时修改这部分代码前的javadoc。因为在每次重新生成代码的时候,JET通过原有 代码前的javadoc判断是否覆盖这段代码,如果javadoc里包含一行"@generated"则覆盖,否则保持这部分代码不变。为了让我们手工的 修改不在以后的生成中消失(这可是很大的损失),可以直接删除@generated这一行,更好的办法是将其改为一个统一的字符串,例如 "@generated NOT",以便将来通过javadoc搜索找到自己都在哪些地方做了手工修改。
下面再通过一个例子演示怎样修改EMF生成的代码。假设现在有个需求是让所有价格大于等于100元的产品自动打95折,而小于5元的商品一律按5元算,那么我们需要修改ProductImpl的getPrice()方法,修改前的代码如下:
* <!-- begin-user-doc -->
* <!-- end-user-doc -->
* @generated
*/
public double getPrice() {
return price;
}
这是修改后的代码,再次提醒一定别忘记修改@generated那行注释:
* <!-- begin-user-doc -->
* 所有价格大于等于100元的产品自动打95折,而小于5元的商品一律按5元算。
* <!-- end-user-doc -->
* @generated NOT
*/
public double getPrice() {
if (price >= 100)
return price * 0.95;
if (price < 5)
return 5d;
return price;
}
至于使用哪种方式进行定制,我个人建议能通过修改ecore/genmodel模型解决的问题就不要直接修改代码,尽量减少手工修改的地方有利于保持思路清晰,其实在实际使用中大部分定制还是需要修改代码才能实现的。