EMF帮助文档翻译

EMF


EMF(Eclipse Modeling Framework)是一个Eclipse的一个开源项目,也是Eclipse建模框架的核心技术。个人认为它是一个定义模型的框架。对于生成类似rose或者是visio这样的建模工具必不可少。了解它从帮助文档入手貌似是一个不错的手段。

EMF英文帮助文档


EMF英文帮助文档可以从Eclipse Modeling开发环境中得到。Eclipse Modeling 开发环境比较好的版本是Luna,最新的Mars版本bug还挺多的,Luna版本中Eclipse Modeling 开发环境的下载地址为Eclipse Modeling Tools(Luna)。下载好开发环境后,打开Help->Help Contents即可找到EMF英文的帮助文档。

EMF开发者手册(翻译)


程序员指南


EMF概述


Eclipse 建模框架(EMF)概述

最后更新:2005年6月16日
本文介绍了EMF的基本情况以及EMF的代码生成机制。想要了解得到EMF全部功能的具体描述,可以参考EMF:Eclipse Modeling Framework, Second Edition (Addison-Wesley Professional, 2008)或者EMF框架中自带的Javadoc。
目录
介绍
定义EMF模型
生成Java代码
使用生成的EMF类
高级内容
介绍
EMF是构建基于结构化模型建模工具的一种Java框架,可自动生成代码。如果已经拥有了建模思想和面向对象特征的模型,EMF可以快速地将这些模型转换为高效率、准确地、易定制的Java代码。如果你现在还不是很热衷于模型驱动开发,EMF会给你提供一个低门槛的入门机会。
模型到底是什么?当谈论模型时,大多数人想起的是类图、协作图或者是状态机图。UML(统一建模语言)定义了这些图形的标准符号。通过使用不同的UML图形可以表述出完整的模型。这个模型可能仅仅用于产品的文档说明,也可能用于生成产品的部分代码。
这些模型通常需要昂贵的面向对象分析设计(OOAD/D)工具的支撑,EMF可提供低成本的入门机会。这是因为EMF中用到的模型是UML的一个子集,仅仅是类的定义、类的属性和类之间的关系,不需要使用标准的图形化建模工具。
EMF使用XMI(XML元模型交换)来定义模型的格式。通过以下方法可以得到XMI格式的EMF模型:
  • 直接创建XMI格式的文件,使用XML编辑器或者是文本编辑器。
  • 从类似于Rational Rose这样的建模工具中导出XMI格式的文件。
  • 使用带有模型特征标记的Java接口。
  • 使用XML框架来持久化模型的。
第一种方法最直接,但适用于XML大师。第二种方法适用于已经有了标准的建模工具。第三种方法为Java程序员提供了低成本的入门机会,仅仅需要Java开发环境(例如,Eclipse的java开发环境)。最后一种适用于开发必须读写XML文件的程序。
一旦定义了EMF模型,EMF生成器可以生成对应的Java类代码。可以根据需要编辑这些类,例如增加一些方法和变量。但要注意的是,做出的改变只能在重新生成代码时得到保存。如果手动增加的代码依赖于模型的变化,仍需要重新生成代码;否则,手动增加的代码将完全不受模型变化的影响。
EMF除了自动生成代码提高生产力之外,还有一些其它的好处,例如:模型改变通知机制、基于XMI的持久化机制、通过反射机制来高效地操作EMF对象。更为重要的,EMF是其它基于EMF工具的基础。
EMF包含有两个基础框架:核心框架和EMF.Edit。核心框架为模型自动生成Java代码提供了生成和运行支持。EMF.Edit扩展了核心框架,它提供模型浏览和编辑的支持。接下来的章节都是介绍核心框架的功能。EMF.Edit框架会在单独的章节EMF.Edit概述中介绍,对于EMF框架的具体使用,可以参考教程:生成EMF模型。
EMF与OMG MOF的关系
如果很了解OMG(对象管理组织)及MOF(元对象设施),就会发现它们和EMF之间存在着千丝万缕的关系。事实上,EMF是基于MOF标准的具体实现。可以把EMF想象为实现MOF核心元素java类实现的API。为了避免混淆,MOF在EMF中对应的元模型被叫做Ecore。
在MOF2.0中,MOF模型的子集EMOF被分离出来了。EMOF和Ecore有一些细微的差别;然而,EMF可以读写EMOF的序列化文件。
定义EMF模型
为了方便描述EMF,本文假设已经有了一个简单的、包含单独类的模型:

这个模型是一个简单的Book类,它有两个属性:类型为String的title,类型为int的pages。
对于这样一个简单的模型,可以从以下方法中得到:
UML
如果已经有了EMF支持的建模工具,可以使用工具直接编辑上述模型。
XMI
同样的,可以直接使用XMI格式的文件来描述上述模型:
<pre xml:space="preserve">  <ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore"
      name="library "nsURI="http:///library.ecore" nsPrefix="library">
    <eClassifiers xsi:type="ecore:EClass" name="Book">
      <eStructuralFeatures xsi:type="ecore:EAttribute" name="title"
          eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
      <eStructuralFeatures xsi:type="ecore:EAttribute" name="pages"
          eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EInt"/>
    </eClassifiers>
  </ecore:EPackage>
 XMI文件和类图包含有同样的信息,但不够简洁。在图形中的每一个类和属性都在XMI文件中被定义。 
Java标记
如果你没有建模工具,同时也不了解XMI语法,第三种方法也许能描述模型。因为EMF生成器是code-merging生成器,使用java标记就可以自动生成元数据,并且能和其它的描述形式进行整合。
定义Book类如下:
/**
   * @model
   */
  public interface Book
  {
    /**
     * @model
     */
    String getTitle();

    /**
     * @model
     */
    int getPages();
  }
通过使用Java接口的形式可以描述模型的信息,该接口通过标准的get方法来定义属性和引用。@model标记可以确定生成代码的接口,以及接口的部分属性,对应了模型元素,可以根据标记生成代码。
在这个简单的例子中,所有的模型信息都可以通过这个简单Java接口描述,这因为模型中不需要额外的信息。但一般情况下,@model标记可以表现出更多的模型细节。例如,如果pages属性是只读的(这是因为没有生成set方法),需要增加如下标记:
 /**
   * @model changeable="false"
   */
  int getPages();
因为只有需要表现的模型信息和默认的不同时才会去修改默认的标记,所以标记通常都是非常简洁的。
XML 架构
有时,你可能想描述一个模型的序列化。通过整合现有应用程序或者依赖标准的XML来表达模型的描述是非常有效的,这里是使用XML架构来描述这个简单的book模型:
<xsd:schema targetNamespace="http:///library.ecore"
      xmlns="http:///library.ecore" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <xsd:complexType name="Book">
      <xsd:sequence>
        <xsd:element name="title" type="xsd:string"/>
        <xsd:element name="pages" type="xsd:integer"/>
      </xsd:sequence>
    </xsd:complexType>
  </xsd:schema>
这种方法不同于另外三种,主要是因为EMF必须遵从一定的限制来实现序列化。因此使用XML架构描述的模型和其它三种方法描述的模型会有细微的差别。在概述中不会介绍这些差别的细节。
在本文的剩余部分中,我们将使用UML图形来描述模型。所有的模型都可用Java标记或者XMI表达,大部分也有对应的XML架构。无论模型的描述方式是什么,EMF通过该模型生成的代码是一致的。
生成Java代码
对于模型的每一个类,都可以转化为一个Java接口。在本文的例子中,生成的Book接口如下:
 public interface Book extends EObject
  {
    String getTitle();
    void setTitle(String value);

    int getPages();
    void setPages(int value);
  }
每一个生成的接口都有setter和getter方法,这些方法对应于模型中的属性和引用。
Book接口继承基础接口EObject。EMF中的EObject相当于java.lang.Object,因此,EObject是所有EMF class的基类。EObject接口和其对应的实现类EObjectImpl(将会在后面看到)是轻量级的基类,但却让Book可以使用EMF的通知机制以及持久化框架。在我们介绍EObject到底能带来什么之前,先来看EMF如何生成Book类。
每一个生成的java类都包含getter和setter方法,同时也会增加一些EMF框架需要的方法。
BookImpl类将包括title和pages的访问方法。其中pages属性将会是如下形式:
  public class BookImpl extends EObjectImpl implements Book
  {
    ...
    protected static final int PAGES_EDEFAULT = 0;
    protected int pages = PAGES_EDEFAULT;

    public int getPages()
    {
      return pages;
    }

    public void setPages(int newPages)
    {
      int oldPages = pages;
      pages = newPages;
      if (eNotificationRequired())
        eNotify(new ENotificationImpl(this, Notification.SET, ..., oldPages, pages));
    }

    ...
  }
生成的get方法简单但有效。它返回当前属性的一个实例。
生成的set方法有一些复杂。除了设置变量pages的值,set方法同样会将改变通知给其观察者,观察者通过调用eNotify()方法来监听当前属性。在一些情况下可能没有观察者(例如,在批处理程序中),通知的内容(ENotificationImpl)和调用方法 eNotify() 都需要被eNotificationRequired()控制。在默认的情况下,生成的eNotificationRequired()仅仅会检查是否有观察者(适配者)。因此当EMF中没有观察者时,eNotificationRequired()会返回空。
对于其它类型的属性,例如String类型的title属性,和pages属性生成的方法本质上是相同的。
引用生成的代码,特别是双向引用,有一些复杂,但同样可以显示出EMF生成代码的真正价值。
单向引用
接下来为扩展示例模型,添加一个Writer类,同时为Writer类添加和Book类之间的引用


在book和writer之间的引用是一个单向引用。这个引用(角色)的名字可以认为是Book的作者。
通过EMF框架对该模型生成代码,会增加一个新的Writer接口和WriterImpl实现类,在Book接口中会增加get和set方法:
Writer getAuthor();
  void setAuthor(Writer value);
因为author引用是单向的,setAuthor()方法和普通属性的setter方法没什么不同,和前面介绍的setPages()非常形似:
 public void setAuthor(Writer newAuthor)
  {
    Writer oldAuthor = author;
    author = newAuthor;
    if(eNotificationRequired())
      eNotify(new ENotificationImpl(this, ...));
  }
唯一的不同就是,需要设置一个对象引用而不是简单的基本数据类型。
因为我们处理的是一个对象引用,getAuthor()方法略微复杂。这是因为这个get方法需要处理引用对象(例子中是Writer)来自于不同源对象(例子中是Book)的情况。在EMF持久化框架中采用的是延迟加载方案,一个对象指针(例子中是author)在某些情况下可能仅仅是一个代理,而不是实际的引用对象。因此,getAuthor()方法如下:
  public Writer getAuthor()
  {
    if (author != null && author.eIsProxy())
    {
      Writer oldAuthor = author;
      author = (Writer)eResolveProxy((InternalEObject)author);
      if (author != oldAuthor)
      {
        if (eNotificationRequired())
          eNotify(new ENotificationImpl(this, Notification.RESOLVE, ...));
      }
    }
    return author;
  }
不仅仅是返回author的一个实例变量,首先调用框架方法elsProxy()来检查该引用是否是一个代理,如果它是调用eResolveProxy()。接下里会调用EcoreUtil.reslve(),一个静态的使用类方法,它会加载目标对象。如果成功,会返回处理后对象。如果加载失败,仍会返回代理。
双向引用
现在已经介绍了代理是如何影响get方法的,接下来会看到双向引用是如何影响set方法的。下面修改单向引用模型:

现在连接变成双向的了,与Writer相连的箭头消失了。从Writer类访问Book类可以称之为books。
如果已经通过模型生成代码,getAuthor()方法不会受到影响,但setAuthor()将会变成这样:
public void setAuthor(Writer newAuthor)
  {
    if (newAuthor != author)
    {
      NotificationChain msgs = null;
      if (author != null)
        msgs = ((InternalEObject)author).eInverseRemove(this, ..., msgs);
      if (newAuthor != null)
        msgs = ((InternalEObject)newAuthor).eInverseAdd(this, ..., msgs);
      msgs = basicSetAuthor(newAuthor, msgs);
      if (msgs != null) msgs.dispatch();
    }
    else if (eNotificationRequired())
      eNotify(new ENotificationImpl(this, ...)); // send "touch" notification
  }
可以看到,当为author设置了双向引用,另一端的引用同样需要设置(调用elnverseAdd()),同样需要移除先前的author(调用elnverseRemove()),在我们的模型中author是唯一的(这是因为book只能有一个author),因此当前这本book也只能是一个Writer的book引用。最终,通过另一个自动生成的方法(basicSetAuthor())来设置author:

public NotificationChain basicSetAuthor(Writer newAuthor, NotificationChain msgs)
  {
    Writer oldAuthor = author;
    author = newAuthor;
    if (eNotificationRequired())
    {
      ENotificationImpl notification = new ENotificationImpl(this, ...);
      if (msgs == null) msgs = notification; else msgs.add(notification);
    }
    return msgs;
  }
这个方法和单向引用中的set方法非常类似,但当msgs参数不为空时,通知要被加载到msgs上,代替了直接替换的形式。这是因为正向/反向/增加/删除会产生四种(本例子中有三个)不同的通知。一个notificationChain会收集不同的通知,它们要被替换只有当所有的状态都被改变。排队等待的通知会条用msg.dispatch()方法,在setAuthor()方法中可以找到。
多值引用
可以注意到在例子中books引用(从Writer到Book)是多值的(这里是0...*)。换句话说,一个writer可以写很多本books。多值引用(就是,任何引用的上限值大于1)在EMF中需要使用collection的API,所以在接口中只会生成一个get方法:
 public interface Writer extends EObject
  {
    ...
    EList getBooks();
  }
注意到getBooks()返回一个Elist类型的值,Elist和java.util.List对应,事实上,它们几乎完全一致。EList在EMF中是java.util.Lis的一个子类,但添加了两个move方法。除了这个,从用户的角度来看,就可以把它看成为Java中的List。例如,增加为books连接添加一本book,可以这样调用:
aWriter.getBooks().add(aBook);
或者像这样可以遍历:
for (Iterator iter = aWriter.getBooks().iterator(); iter.hasNext(); )
  {
    Book book = (Book)iter.next();
    ...
  }
可以看到,从一个用户角度来说,API操控多值引用的方法并不特殊。然而,因为books引用是双向连接(它是Books.author的反方向)的一部分,仍需要在setAuthor()方法做一些花哨的握手机制,查看WriteImpl类中的getBooks()方法,可以考到多值在get方法中是怎样被操控的:
public EList getBooks()
  {
    if (books == null)
    {
      books = new EObjectWithInverseResolvingEList(Book.class, this,
                    LibraryPackage.WRITER__BOOKS, LibraryPackage.BOOK__AUTHOR);
    }
    return books;
  }
getBooks()方法返回一个特殊的实现类,EObjectWithInverseResolvingEList,这个特殊的类包含有反握手的所有信息,反握手发生在添加和删除被调用时。EMF提供了20几种特殊的EList实现,主要为了有效的实现多种类型的多值特征。对于单向引用(没有反向)使用EObjectResolvingEList。如果引用不需要使用代理,直接使用EObjectWithInverseEList或者EObjectEList。
在本例中,list实现books引用需要用到参数LibraryPackage.BOOK__AUTHOR(是自动生成的常量值,用来表示反转)。这个参数会在Book类中的add()方法调用elnverseAdd()方法时调用,和Writer类用setAuthor()调用elnverseAdd()方法类似。在BookImple类中elnverseAdd()方法如下:
public NotificationChain eInverseAdd(InternalEObject otherEnd, int featureID,
                                       Class baseClass, NotificationChain msgs)
  {
    if (featureID >= 0)
    {
      switch (eDerivedStructuralFeatureID(featureID, baseClass))
      {
        case LibraryPackage.BOOK__AUTHOR:
          if (author != null)
            msgs = ((InternalEObject)author).eInverseRemove(this, .., msgs);
          return basicSetAuthor((Writer)otherEnd, msgs);
        default:
          ...
      }
    }
    ...
  }
首先调用eLnverseRemove()来移除以前的author(在前面看到setAuthor()方法中已经有了描述),然后调用basicSetAuthor()方法来真正地设置引用。但本例中只有一个双向引用,eLnverseAdd()方法会使用switch语句来包含所有在Book类出现的各种双向引用。
聚合引用
增加一个新的类,Library,把它当作包含Books的类。
聚合引用是一个结尾为黑色菱形的连接。连接表示一个Library由0个或者多个Books组成。聚合引用的价值体现在它们定义了目标实例的父类或者拥有者。指出了对象持久化的物理位置。
聚合引用会通过几种方式影响最终生成的代码。首先,被包含的对象应该和包含它的对象拥有同样的类型,不需要代理。因此,在LibraryImpl类中生成的get方法将会使用一个non-resolving类型的EList实现类:
 public EList getBooks()
  {
    if (books == null)
    {
      books = new EObjectContainmentEList(Book.class, this, ...);
    }
    return books;
  }
由于没有采用代理模型,EObjectContainmentELis实现contains()方法非常有效率(一般情况下,在线性时间内)。这非常重要因为在EMF引用表中不允许重复的条目,所以contains()方法会在调用add()方法时被使用。
因为一个对象只能拥有一个包含者,增加一个对象到聚合引用中也意味着必须删除把它从当前的包含者中删除。例如,增加一个Book到Library的books表,就要从其它的Library的books表中删除。这个其它的双向关联中反向一端的值大于1的情况一样。假设,Writer类拥有一个到Book类的聚合引用,叫做ownedBooks。这样如果一本book的实例出现在某些Writer的ownedBooks表中,当把它添加到Library的books引用时,就需要把它从Writer中移除。
想有效率的实现上述功能,EObjectImpl基类中拥有一个EObject类型的实例变量(eContainer),实例变量用来储存包含者。所以聚合引用是隐式的双向引用。从Book类中得到Library,可以进行如下实现:
 EObject container = book.eContainer();
  if (container instanceof Library)
    library = (Library)container;
如果你想优雅的得到Library,可以使用显式的双向:
然后让EMF生成一个类型安全的get方法:
 public Library getLibrary()
  {
    if (eContainerFeatureID != LibraryPackage.BOOK__LIBRARY) return null;
    return (Library)eContainer;
  }
注意到显式的get方法是用eContainer变量来代替生成一个实例变量,这个前面没有包含的引用(像getAuthor()方法)生成get方法机理一致。
枚举属性

到目前为止,已经介绍了EMF如何控制简单类型属性和变换类型的引用。另一个经常使用的属性是枚举。在EMF枚举类型的属性直接使用java中安全的枚举类型。

如果要增加一个枚举属性,category,这个枚举属性可用在Book上:


然后生成代码,Book接口将会为category实现get,set方法:

BookCategory getCategory();
void setCategory(BookCategory value);

在生成的接口中,category方法使用了一个类型安全的枚举类,叫做BookCategory。这个类定义了枚举值的静态常量值和其它一些方法,如下:

public final class BookCategory extends AbstractEnumerator
  {
    public static final int MYSTERY = 0;
    public static final int SCIENCE_FICTION = 1;
    public static final int BIOGRAPHY = 2;

    public static final BookCategory MYSTERY_LITERAL =
      new BookCategory(MYSTERY, "Mystery");
    public static final BookCategory SCIENCE_FICTION_LITERAL =
      new BookCategory(SCIENCE_FICTION, "ScienceFiction");
    public static final BookCategory BIOGRAPHY_LITERAL =
      new BookCategory(BIOGRAPHY, "Biography");
  
    public static final List VALUES = Collections.unmodifiableList(...));

    public static BookCategory get(String name)
    {
      ...
    }

    public static BookCategory get(int value)
    {
      ...
    }
  
    private BookCategory(int value, String name)
    {
      super(value, name);
    }
  }

可以看到,enumeration类为enumeration的value指定了具体的整形常量。为enumeration的单例文字对象提供了静态常量值。整形常量值和模型中的文字的名字相同。静态常量值后加上_LITERAL后缀就是文字对象的名字。

BookCategory的构造方法是私有的,因此这个类的实例只能在该类自身中创建,使用这个构造方法的有MYSTERY_LITERAL, SCIENCE_FICTION_LITERAL, 和BIOGRAPHY_LITERAL。所以,相等比较(就是调用.equals()方法)是没有必要的。文字值之间的相等比较可以使用更简单和有效率的==,如下:

book.getCategory() == BookCategory.MYSTERY_LITERAL

当需要操作多个values,switch语句使用values的int值是更高的方法:

switch (book.getCategory().value()) {
    case BookCategory.MYSTERY:
      // do something ...
      break;
    case BookCategory.SCIENCE_FICTION:
      ...
  }
For situations 

只有当文字的name(String)或者value(int)是有效的,返回值为文字对象的get()方法才会在enumeration类中生成。

工厂和包

为了补充模型接口和模型接口实现类,EMF至少又生成了两个接口(和对应的接口实现类):工厂和包。

工厂,就和它名字的意义一样,用来生成model类,包提供了一些静态变量(例如,生成的办法需要用到的feature变量)还提供了一些便捷你访问模型元数据的方法。

下面的代码是book类的工厂接口:

public interface LibraryFactory extends EFactory
  {
    LibraryFactory eINSTANCE = new LibraryFactoryImpl();

    Book createBook();
    Writer createWriter();
    Library createLibrary();

    LibraryPackage getLibraryPackage();
  }

可以看到,生成的factory为模型中的每一个类提供了工厂方法(create),还有一个模型包的访问器,一个静态的常量引用(eINSTANCE)。

LibraryPackage接口为访问模型的元数据提供了便利的访问形式:

 public interface LibraryPackage extends EPackage
  {
    ...
    LibraryPackage eINSTANCE = LibraryPackageImpl.init();
    
    static final int BOOK = 0;
    static final int BOOK__TITLE = 0;
    static final int BOOK__PAGES = 1;
    static final int BOOK__CATEGORY = 2;
    static final int BOOK__AUTHOR = 3;
    ...
    
    static final int WRITER = 1;
    static final int WRITER__NAME = 0;
    ...
    
    EClass getBook();
    EAttribute getBook_Title();
    EAttribute getBook_Pages();
    EAttribute getBook_Category();
    EReference getBook_Author();

    ...
  }

可以看到,元数据有两种形式:int常量和Ecore元对象。int常量提供了分发元信息的最有效的方式。可以注意到在生成的方法实现中会应用到这些int常量。在接下来的EMF适配器如何实现的章节中,将会看到int常量在确定改变的具体对象时非常有效。同时,和factory一样,生成的包接口提供了静态常量引用来实现单例。

通过继承实现类

接下来,实现Book类的一个子类,如下:

EMF生成器是单一继承:生成的接口继承父接口:

public interface SchoolBook extends Book

implementation类继承父类:

public class SchoolBookImpl extends BookImpl implements SchoolBook

在java中,支持多继承接口,但每一个EMF类只能继承一个实现类。因此,如果一个模型继承多个接口,就需要指定继承哪一个具体的实现类。其它的接口被看做是混合接口,它们实现类的方法会混合到实现类中。

考虑下面的例子:


这里让SchoolBook继承两个类:Book和Asset。指定Book类为实现基类(extend)。模型如果重新生成代码,接口SchoolBook类将会继承这两个接口:

public interface SchoolBook extends Book, Asset

实现类和以前的类相似,只是包含有两个混合的方法getValue()和setValue():

public class SchoolBookImpl extends BookImpl implements SchoolBook
  {
    public float getValue()
    {
      ...
    }

    public void setValue(float newValue)
    {
      ...
    }
    
    ...
  }
定制生成的类

可以为生成的代码增加行为(方法或者实例变量),而且不必担心修改模型重新代码会丢失这些改变。例如,为Book类添加了一个方法,isRecommender(),仅仅只是在Book接口中直接添加新方法:
 public interface Book ...
  {
    boolean isRecommended();
    ...
  }
然后在BookImpl添加实现:
 public boolean isRecommended()
  {
    return getAuthor().getName().equals("William Shakespeare");
  }
EMF生成器不会擦除这些改变,因为这些改变并不是生成的。EMF生成的每一个方法 都会有一个@generated标记:
 /**
   * ...
   * @generated
   */
  public String getTitle()
  {
    return title;
  }
不包含这个标记(像isRecommended()方法)在重新生成代码时不会被触及。事实上,如果想改变生成的实现类代码,只需把@generated标记去掉即可。
  /**
   * ...
   * @generated // (removed)
   */
  public String getTitle()
  {
    // our custom implementation ...
  }
现在,因为没有了@generated标记,getTitle()方法被认为是用户自己的代码;如果重新生成代码,生成器会检测冲突并会舍弃该方法。
事实上,在丢弃该方法之前,生成器首先检查在文件中是否有另一个方法有同样的名字,但同时有Gen后缀。如果找到了, 就不舍弃而是直接将其输出。例如,如果为getTitle()实现扩展一些行为,代替全部丢弃的方式,可以将它重命名:
 /**
   * ...
   * @generated
   */
  public String getTitleGen()
  {
    return title;
  }
然后重写它,可以添加任何想要的内容:
 public String getTitle()
  {
    String result = getTitleGen();
    if (result == null)
      result = ...
    return result;
  }
如果重新生成代码,生成器会检测到getTitle()冲突,但因为拥有@generated标记和getTitleGen名称,生成器会直接将新的实现输出,而不是丢弃它。
EMF模型中的方法
除了可以添加属性和引用之外,可以为模型添加方法。EMF生成器会在接口中生成方法的签名,在实现类中生成方法的骨架。EMF不是建模,所以具体的实现需要让用户用java代码实现。
也可以在生成的实现类代码中移除@generated标记,正由前文所述,然后再为其添加正确的代码。另外,也支持在模型中写java代码。在Rose中,可以再文本框中添加java代码。代码将会储存到EMF模型中,会在生成的代码中作为注释出现。
使用生成的EMF类
创建和访问实例
使用生成的类,用户 程序通过简单的java语句创建和初始化Book类:
LibraryFactory factory = LibraryFactory.eINSTANCE;

  Book book = factory.createBook();

  Writer writer = factory.createWriter();
  writer.setName("William Shakespeare");

  book.setTitle("King Lear");
  book.setAuthor(writer);
因为Book和Writer之间的关联(author)是双向的,反向引用(books)会被自动初始化。可以通过迭代books引用来验证:
 System.out.println("Shakespeare books:");
  for (Iterator iter = writer.getBooks().iterator(); iter.hasNext(); )
  {
    Book shakespeareBook = (Book)iter.next();
    System.out.println("  title: " + shakespeareBook.getTitle());
  }
这段程序会输出:
Shakespeare books:
    title: King Lear
保存和加载资源
创建一个文件并命名为mylibrary.xmi,用来保存上述模型,需要在程序的开始创建EMF资源,将book和writer放到资源中,并在结尾保存
 // Create a resource set.
  ResourceSet resourceSet = new ResourceSetImpl();

  // Register the default resource factory -- only needed for stand-alone!
  resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().put(
    Resource.Factory.Registry.DEFAULT_EXTENSION, new XMIResourceFactoryImpl());

  // Get the URI of the model file.
  URI fileURI = URI.createFileURI(new File("mylibrary.xmi").getAbsolutePath());

  // Create a resource for this file.
  Resource resource = resourceSet.createResource(fileURI);

  // Add the book and writer objects to the contents.
  resource.getContents().add(book);
  resource.getContents().add(writer);

  // Save the contents of the resource to the file system.
  try
  {
    resource.save(Collections.EMPTY_MAP);
  }
  catch (IOException e) {}
创建了资源,如果是独立的情况,需要注册一个资源的实现。同时,需要确保模型的包也在包注册表中被注册,资源需要包含元数据和工厂,并在模型加载时调用。通过访问生成package接口的eINSTANCE域来确保包已经被注册。
例子中第二个部分是save(),需要使用OutputStream,这样可以在控制台上打印序列化的信息。
讲一个模型分散到多个文件中,可以在它们之间添加引用,这是非常简单的。如果想序列化books和writers,在上述的保存实例中,可以使用分散的文件,需要创建第二个资源:
Resource anotherResource = resourceSet.createResource(anotherFileURI);
然后为其添加writer,代替第一种方法:
  resource.getContents().add(writer); // (replaced)
  anotherResource.getContents().add(writer);
将会产生两个资源,每一个资源包含有一个对象,两个资源之间是一个跨文档的引用。
需要注意包含引用中被包含对象必须和包含者使用同样的资源。例如,假设创建了Library实例,Library和Book之间有包含引用Books,就会自动地将Book从资源的内容中移除,从这个意义上来说,就和包含引用是类似的。如果接下来将Library增加到资源中,book将会隐含的属于这个资源,它的细节将会在资源中被序列化。
如果想把对象序列化成其它的XMI格式,可以自主控制,但需要提供自己的序列化和解析代码。创建自己的resource类(作为ResourceImpl的子类)来实现自己更想要的序列化格式,不管是在资源集注册中局部注册,还是因为在模型中一直使用的缘由将其注册到全局工厂中,这两种行为都是可行的。
观察(改编)EMF对象

前面,当查看EMF生成类的set方法时,通知会在属性或引用被改变时发送。例如,BookImpl类的setPages()方法包含有下述代码:

eNotify(newENotificationImpl(this, ..., oldPages, pages));

每一个EObject都有一系列观察者(对于引用来说是适配者),当状态改变时会得到通知。框架中eNotify()方法遍历列表,并为列表上的观察者发送通知。

一个观察者可以依附于任何EObject(例如,book),这是通过添加到eAdapters列表来实现的:

Adapter bookObserver = ...
  book.eAdapters().add(bookObserver);

更普遍的做法是,适配者通过适配工厂增加到EObjects中。另外根据观察者规则,适配者需要扩展依附对象的行为。通过条用适配工厂来扩展行为,这样就根据需要扩展了对象。通常它像这样:

EObject someObject = ...;
  AdapterFactory someAdapterFactory = ...;
  Object requiredType = ...;
  if(someAdapterFactory.isFactoryForType(requiredType))
  {
    Adapter theAdapter = someAdapterFactory.adapt(someObject, requiredType);
    ...
  }

通常,requiredType代表着适配者支持的接口,例如,它可能是选择适配者接口的具体java.lang.Class。返回的适配者转换成需要的接口类型:

MyAdapter theAdapter = (MyAdapter)someAdapterFactory.adapt(someObject, MyAdapter.class);

适配者通常使用这种方法来扩展对象的行为,这种方法不是继承。

为了控制适配者中的通知需要重写eNotifyChanged()方法,这个方法会被所有的注册适配者通过eNotify()调用。典型的适配者实现eNotifyChanged()来实现通知中的部分或者全部的行为,这取决于通知的类型。

有时,适配者会改变一个具体的类(例如,Book)。在这种情况下,NotifyChanged()如下:

public void notifyChanged(Notification notification)
  {
    Book book = (Book)notification.getNotifier();
    switch (notification.getFeatureID(Book.class))
    {
      case LibraryPackage.BOOK__TITLE:
        // book title changed
        doSomething();
        break;
      caseLibraryPackage.BOOK__CATEGORY:
        // book category changed
        ...
      case ...
    }
  }

调用notification.getFeatureID()来控制适配对象不是BookImpl类的实例,但却是一个多继承子类同时Book不是第一个接口的实例出现的所有情况。在这种情形下,通知中使用的feature ID是一个与类相关的数字,同时需要在使用BOOK_常量之前被调整,在单继承情况下,这个参数会被忽略。

另一个常用的适配者不和任何类绑定,通过EMF API中的反射来实现功能。代替调用getFeatureID()方法,它调用getFeature(),返回真实的Ecore feature(在元模型中中代表功能的对象)。

使用反射API

可以使用EObject接口的反射API来操作生成的模型类:

public interface EObject ...
  {
    ..
    Object eGet(EStructuralFeature feature);
    void eSet(EStructuralFeature feature, Object newValue);

    boolean eIsSet(EStructuralFeature feature);
    void eUnset(EStructuralFeature feature);
  }

使用反射API,需要设定author的名字:

writer.eSet(LibraryPackage.eINSTANCE.getWriter_Name(), "William Shakespeare");

或者使用下列代码得到名字:

String name = (String)writer.eGet(LibraryPackage.eINSTANCE.getWriter_Name());

注意到名字通过library package的单例得到。

使用反射API和调用getName()和setName()方法相比有些许低效,但她开发了访问的权限。例如,EMF.Edit框架使用反射方法来实现一系列的commands(例如,AddCommand,RemoveCommand,SetCommand)并可应用到任何模型上。具体的细节查看EMF.Edit Overview。

对于eGet()和eSet(),反射API包括两个相关方法:elsSet()和eUnset()。elsSet()方法可以确定属性是否被设置,eUnset()用来复原(或者重置)。一般的XMI序列化器,例如,在进行资源持久化时,使用elsSet()来决定哪个属性需要被序列化。

高级主题
生成控制标记

在模型的feature上设置一些标记值就可以控制feature的生成代码模式。一般来说,这些标记的默认设置已经足够,不需要频繁地更改。

  • Unsettable(默认为false)

声明为unsettable的feature是明确的没有设置或者是无值状态。例如,非unsettable属性是一个bool类型,只能被设置为两个值:ture或者false。但unsettable的属性可以有三个值:ture,false,unset。

一个没有被设置的feature的get方法将会返回一个默认值,但对于unsettable的feature来说,这种情况和显式的设置默认值是不同的。因为unset状态超出了set允许设置的值,需要生成额外的方法让feature变成unset状态。例如,Book类中的pages属性被声明为unsettable,就会得到两个新增的方法:

  boolean isSetPages();
  void unsetPages();

除此之外,两个原生方法:

  int getPages();
  void setPages(int value);

当feature显式的被设置,isSet方法返回true。当属性被设置为unset状态时unset方法被执行。

当unsettable为false时,不会生成isSet或者unset方法,但可以通过反射得到get实现:elsSet()和eUnset()(每一个EObject必须实现)。对于非unsettable属性,当前值与默认值不同时,elsSet()返回true,eUnset()设置为默认值(和重置很像)。

  • ResolveProxies(默认为true)

ResolveProxies在非包含引用上应用。ResolveProxies表示引用是跨文件的,因此在get方法中需要一定的检查策略和解决策略,在前文中已经有了介绍。

可以优化生成引用的get方法,当引用已经明确不会被应用到跨文件场景中时,可以讲resolveProxies设置为false。在这种情况中,生成的get方法具有最佳性能。

  • Unique(默认为true)

Unique只能应用到大量的属性上,大量的属性表示该属性不包含一样的对象。引用总是被认为是unique。

  • Changeable(默认为true)

没有被声明为changeable的feature不会生成set方法,当尝试eSet()方法时将会抛出异常。在双向关系中最好在一端声明为非changeable,这样就会强制用户在另一端设置引用,但也提供便捷的导航方法。在单向引用或者属性中设置为非changeable通常预示着这个feature会被其它代码(手写)设置或改变。

  • Volatile(默认为false)

声明为Volatile的feature的生成不需要存储区域,同时生成空的实现方法,需要自己去填写。Volatile通常应用于feature中的值来源于其它feature的情况,或者feature使用不同的存储实现模式的情况。

  • Derived(默认为false)

Feature的derived的值通过其它features计算得来,所以它并不是附加的对象状态。框架类,例如EcoreUtil.Copier,赋值模型对象不会赋值Volatile的feature。通过deriver标记生成的代码不受影响,除了包中初始化模型中元数据的实现类。Derived的feature通常也被标记为volatile和transient。

  • Transient(默认为false)

Transient的feature来表示(模型的)数据的全生命周期中不会跨应用调用,因此不需要被持久化。(默认的XMI)序列化不会保存声明为Tansient的feature。和derived类似,transient也仅仅是影响包中关于初始化元数据的实现类。

数据类型

正如前文所提到的,在模型中定义的类(例如,Book,Writer)隐式地从EMF中的基类EObject继承。然而,所有模型使用的类不一定都是EObjects。例如,假设像增加一个类型为java.util.Date的属性到模型中,在前面的介绍中,需要定义一个EMF的DataType类来表示这种类型。在UML中,可以使用指定元类型的方式达到目的:


可以看到,数据类型是模型中被命名的元素,它可以看做java类的一个代理。真实的java类是javaclass元类型的属性,它的名字是一个严格的类。通过定义这个数据类型,可以讲属性的类型声明为java.util.Date:

生成代码,在Book接口中会显示属性的声明类型:

import java.util.Date;

  public interface Book extends EObject
  {
    ...

    Date getPublicationDate();
    void setPublicationDate(Date value);
  }

可以看到,数据类型的属性可以被很优雅的控制。事实上,所有的属性都有类型,包括String,int等等。唯一特殊的地方就是标准的java类型是在Ecore模型中被提前定义的,所以不需要在模型中被再次定义。

数据类型的定义会在生成的模型中照成另外一些影响。因为数据类型代表着任意的类,普通的序列化和解析(例如,默认的XMI序列化器)不会知道如何保存属性的类型,调用toString()?这貌似是一个合理的默认方法。但这不是EMF框架支持的,所以在生成的工厂实现类中生成两个额外的方法:

/**
   * @generated
   */
  public Date createJavaDateFromString(EDataType eDataType, String initialValue)
  {
    return (Date)super.createFromString(eDataType, initialValue);
  }

  /**
   * @generated
   */
  public String convertJavaDateToString(EDataType eDataType, Object instanceValue)
  {
    return super.convertToString(eDataType, instanceValue);
  }

默认的,这些方法简单的调用父类的实现,这合乎情理,但比较低效,默认:convertToString()调用对象的toString()方法,createFormString()尝试使用java反射,来调用String接口或者,一个存在的静态valueOf()方法。通常需要管控这些方法(通过移除@generated标记)并更改来定制合适的实现:

 /**
   * @generated // (removed)
   */
  public String convertJavaDateToString(EDataType eDataType, Object instanceValue)
  {
    return instanceValue.toString();
  )
Ecore模型

下图为完整的Ecore模型的类图(带有阴影的方块是抽象类)

上述层次结构中包括文中描述的EMF模型元素的类:类(属性,引用,方法)数据类型,枚举,包和工厂。
Ecore的实现通过EMF自己的生成器来来生成,它和前文介绍的实现一样轻量并高效。










评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值