Eclipse Modeling Framework (EMF) -kehyksen yleiskuvaus

Viimeksi päivitetty: 16. kesäkuuta 2005 (helppokäyttötoimintojen päivitys)

Tässä artikkelissa esitetään yleiskuvaus EMF:n perusominaisuuksista ja sen koodin luontitoiminnon kaavoista. Voit lukea tarkemman kuvauksen kaikista EMF:n ominaisuuksista julkaisusta EMF: Eclipse Modeling Framework, Second Edition (Addison-Wesley Professional, 2008) tai katsoa lisätietoja itse kehyksen luokkien Javadoc-asiakirjoista.

Sisältö

Johdanto
EMF-mallin määritys
Java-toteutuksen luonti
Luotujen EMF-luokkien käyttö
Lisäaiheet

Johdanto

EMF on Java-kehys ja koodin luontitoiminon väline, jolla muodostetaan työkaluja ja muita sovelluksia rakenteisen mallin perusteella. Jos olet oliokeskeisen mallinnuksen kannattaja, EMF auttaa sinua muuntamaan mallit nopeasti tehokkaaksi, virheettömäksi ja helposti mukautettavaksi Java-koodiksi. Jos et ole täysin vakuuttunut muodollisten mallien arvosta, EMF on sinulle sopiva kehys, joka tuottaa samat edut hyvin alhaisilla alkukustannuksilla.

Mitä tarkoitamme, kun puhumme mallista? Mallinnuksen yhteydessä mieleen tulevat tavallisesti luokan kaaviot, yhteiskäyttökaaviot, tilakaaviot ja niin edelleen. UML (Unified Modeling Language) määrittää tällaisten kaavioiden normaalin esitysmuodon. UML-kaavioiden yhdistelmää käyttämällä voidaan määrittää sovelluksen täydellinen malli. Mallia voidaan käyttää pelkästään dokumentointia varten tai sopivien työkalujen avulla syötteenä, josta luodaan sovelluksen osa tai yksinkertaisissa tapauksissa koko sovellus.

Tämäntyyppinen mallinnus vaatii tavallisesti kalliita työkaluja oliokeskeisten analyysien laatimista ja suunnittelua varten (OOA/D-työkaluja). Saatat sen vuoksi pyytää meiltä vahvistusta siitä, että EMF:n aloituskustannukset ovat pienet. Ne ovat pienet sen vuoksi, koska EMF-malli vaatii vain pienen alijoukon niistä asioista, joita voit mallintaa UML:ssä. Se vaatii yksinkertaiset luokkien ja niiden määritteiden ja suhteiden määritykset, joihin ei tarvita täysimittaista graafista mallinnustyökalua.

EMF käyttää XMI (XML Metadata Interchange) -muotoa mallin määrityksen[1]  tavanomaisena muotona, johon voit saada mallisi usealla eri tavalla:

Ensimmäinen vaihtoehto on suorin, mutta tavallisesti sitä suosivat vain XML-menetelmään syvällisesti perehtyneet asiantuntijat. Toinen vaihtoehto on paras silloin, jos jo käytät täysimittaisia mallinnustyökaluja Kolmas vaihtoehto on puhdasta Java-ohjelmointia käyttäville edullinen tapa hyödyntää EMF-kehyksen ja sen koodin luontitoiminnon hyvät puolet käyttämällä Java-kehitysympäristön perusominaisuuksia (esimerkiksi Eclipsen Java-kehitystyökaluja). Viimeinen vaihtoehto soveltuu parhaiten sellaisen sovelluksen luomiseen, jonka on luettava tai kirjoitettava tietty XML-tiedostomuoto.

Kun määrität EMF-mallin, EMF-koodin luontitoiminto voi luoda vastaavan joukon Java-toteutusluokkia. Voit muokata näitä luotuja luokkia menetelmien ja ilmentymämuuttujien lisäämiseksi ja luoda silti uudelleen mallista tarvittaessa: tekemäsi lisäykset säilytetään uudelleenluonnin aikana. Jos lisätty koodi määräytyy jonkin mallissa tehdyn muutoksen mukaan, sinun on vielä päivitettävä koodi tekemiesi muutosten peilaamiseksi; muussa tapauksessa mallin muutokset ja uudelleenluonnit eivät vaikuta lainkaan koodiin.

Sen lisäksi, että tuottavuus paranee, sovelluksen muodostukseen EMF-kehyksen avulla liittyy useita muita etuja. Näitä ovat ilmoitukset mallin muutoksista, jatkuva tuki mukaan lukien oletusarvoinen XMI- ja skeemaperustainen XML- sarjoitus, mallin tarkistuskehys ja erittäin tehokkaasti reflektiivinen API EMF-objektien yleistä käsittelyä varten. Tärkeintä on, että EMF muodostaa perustan yhteentoimivuudelle muiden EMF-perustaisten työkalujen ja sovellusten kanssa.

EMF:ssä on kaksi peruskehystä: ydinkehys ja EMF.Edit. Ydinkehys toimittaa perusluontitoimintojen ja ajonaikaisen tuen Java-toteutusluokkien luomiseen mallille. EMF.Edit on ydinkehyksen laajennus, joka muodostetaan ydinkehyksen perustalle ja joka lisää tuen sovitinluokkien luomiseen. Sovitinluokat mahdollistavat mallin katselun ja komentoperustaisen (toteuttamiskelvottoman) muokkauksen sekä tuottavat lisäksi toimivan mallin perusmuokkausohjelman. Seuraavissa osissa on kuvattu EMF-ydinkehyksen pääominaisuudet. EMF.Edit-kehyksestä on esitetty kuvaus erillisessä artikkelissa nimeltä EMF.Edit-kehyksen yleiskuvaus. Ohjeita EMF-ydinkehyksen ja EMF.Edit-kehyksen ajamisesta on kohdassaOpetusohjelma: EMF-mallin luonti.

EMF-kehyksen suhde OMG MOF -välineeseen

Jos OMG (Object Management Group) MOF (Meta Object Facility) on sinulle tuttu, saatat miettiä, millainen EMF:n suhde siihen on. EMF oli itse asissa ensin MOF-määrityksen toteutus, josta se kehittyi suuren työkalujoukon toteutuksesta sen avulla saatujen kokemusten perusteella. EMF-kehystä voidaan pitää MOF API -liittymän ytimessä olevan alijoukon erittäin tehokkaana JAVA-toteutuksena. Sekaannuksen ehkäisemiseksi MOF-välinettä muistuttavaa ydinmetamallia kutsutaan EMF-kehyksessä Ecore-malliksi.

Nykyisessä ehdotuksessa MOF 2.0 -välineelle erotetaan samankaltainen MOF-mallin alijoukko, jota kutsutaan EMOF (Essential MOF) -välineeksi. Ecore- ja EMOF-välineiden välillä on pieniä, enimmäkseen nimityksiin liittyviä eroja. EMF pystyy kuitenkin lukemaan ja kirjoittamaan läpinäkyvästi EMOF:n sarjoituksia.

EMF-mallin määritys

EMF-kehyksen kuvaamiseksi tarkastelemme esimerkkiä, jossa meillä on tavanomainen yksiluokkainen malli, joka näyttää tältä:

One-class model: Book with title : String and pages : int

Malli näyttää yksittäisen luokan nimeltä Kirja ja seuraavat kaksi määritettä: merkkijono-lajisen otsikon ja int-lajisen sivut.

Mallin määritelmä on tavanomainen ja voidaan toimittaa EMF-koodin luontitoimintoon monella eri tavalla.

UML

Jos käytössäsi on mallinnustyökalu, joka toimii EMF:n[2] kanssa, voit yksinkertaisesti piirtää luokkakaavion edellä esitetyllä tavalla.

XMI

Voimme vaihtoehtoisesti kuvata mallin suoraan XMI-asiakirjassa, mikä näyttäisi jokseenkin tältä:

  <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-asiakirja sisältää kaikki samat tiedot kuin luokan kaavio, mutta on hieman vähemmän tiivis. Kaikilla kaavion luokilla ja määritteillä on vastaava luokan tai määritteen määrite XMI-asiakirjassa.

Kommentein varustettu Java

Jos käytössäsi ei ole graafista mallinnustyökalua etkä ole kiinnostunut yrittämään koko XMI-syntaksin lisäystä käsin, voit kuvata mallin turvautumalla kolmanteen vaihtoehtoon. EMF-kehyksen luontitoiminto on koodia yhdistävä, joten luontitoiminto voi toimittaa osittaisia Java-liittymiä (joissa on tietoja mallista) etukäteen ja käyttää liittymiä luonnin metatietoina ja yhdistää luodun koodin toteutuksen muuhun osaan.

Olisimme voineet määrittää Book-malliluokan Javassa seuraavan näköisesti:

  /**
   * @model
   */
  public interface Book
  {
    /**
     * @model
     */
    String getTitle();

    /**
     * @model
     */
    int getPages();
  }

Tässä menetelmässä toimitamme kaikki mallin tiedot Java-liittymien muodossa käyttämällä normaaleja hakumetodeja[3]  määritteiden ja viittausten tunnistuksessa. Tunnistetta @model käytetään yksilöimään koodin luontitoiminnolle liittymät ja niiden osat, jotka vastaavat mallin elementtejä ja edellyttävät siten koodin luontia.

Yksinkertaisessa esimerkissämme kaikki mallin tiedot ovat todellisuudessa käytettävissä liittymän Java-itsekuvaavuuden kautta eikä mallin lisätietoja tarvita. Yleisesti kuitenkin tunnistetta @model voivat seurata lisätiedot mallin elementistä. Jos esimerkiksi haluaisimme, että sivujen määrite on vain luku -tilassa (joukon menetelmää ei luoda), meidän olisi lisättävä kommenttiin seuraava:

  /**
   * @model changeable="false"
   */
  int getPages();

Vain tiedot, jotka poikkeavat oletusarvosta, on määritettävä. Kommentit voidaan siten pitää yksinkertaisina ja suppeina.

XML-skeema

Joskus saatat haluta kuvata mallin käyttämällä skeemaa, joka määrittää sen, miltä sarjoitusten pitäisi näyttää. Tämä voi olla kätevää sellaisen sovelluksen kirjoituksessa, jonka integrointi aiemmin luotuun sovellukseen tai yhdenmukaisuus standardin kanssa edellyttää XML:n käyttöä. Määrittäisimme yksinkertaista Book-mallia vastaavan skeeman seuraavasti:

  <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>

Tämä menetelmä poikkeaa jossakin määrin kolmesta muusta vaihtoehdosta lähinnä sen vuoksi, että EMF-kehyksen on otettava käyttöön tietyt rajoitukset sarjoituksessa, jota se lopulta käyttää, yhdenmukaisuuden varmistamiseksi skeeman kanssa. Tämän tuloksena skeemalle luotu malli näyttää hieman erilaiselta kuin jollakin muista tavoista määritetty malli. Näitä eroja ei ole tarkasteltu tässä yleiskuvauksessa.

Tämän artikkelin loppuosassa käytämme UML-kaavioita, koska ne ovat selkeitä ja suppeita. Kaikki mallinnuskonseptit, joita kuvaamme, voidaan kirjoittaa myös käyttämällä kommentoitua Javaa tai suoraan XMI:tä. Useimmille niistä on vastine XML-skeemassa. Toimitettiinpa tiedot miten tahansa, EMF-kehyksellä luotu koodi on aina sama.

Java-toteutuksen luonti

Mallin kaikille luokille luodaan Java-liittymä ja vastaava toteutusluokka. Esimerkissämme kirjalle luotu liittymä näyttää tältä:

  public interface Book extends EObject
  {
    String getTitle();
    void setTitle(String value);

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

Kaikki luodut liittymät sisältävät haku- ja asetusmetodeja kaikille vastaavan malliluokan määritteille ja viitteille.

Book-liittymä on EObject-perusliittymän laajennus. EObject on EMF-vastine java.lang.Object-objektille eli kaikki EMF-luokat perustuvat siihen. EObject ja sitä vastaava toteutusluokka EObjectImpl (jota tarkastelemme jäljempänä) toimittaa suhteellisen kevyen perusluokan, jonka ansiosta Book voi osallistua EMF-ilmoitus- ja pysyvyyskehyksiin. Ennen kuin tarkastelemme sitä, mitä EObject tarkkaan ottaen tuo yhdistelmään, käsittelemme vielä kirjan luontia EMF-kehyksessä.

Kaikki luodut toteutusluokat sisältävät haku- ja asetusmetodien toteutuksia, jotka on määritetty vastaavassa liittymässä, sekä joitakin muita menetelmiä, joita EMF-kehys vaatii.

BookImpl-luokka sisältää muun muassa otsikon ja sivujen käsittelyobjektien toteutukset. Esimerkiksi sivut-määritteen luotu toteutus näyttää tältä:

  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));
    }

    ...
  }

Luotu hakumetodi on mahdollisimman tehokas. Se yksinkertaisesti palauttaa ilmentymämuuttujan, joka edustaa määritettä.

Asetusmetodi on hieman monimutkaisempi, mutta myös melko tehokas. Ilmentymämuuttujasivujen asettamisen lisäksi asetusmetodin on lähetettävä muutosilmoitus kaikille objektia mahdollisesti kuunteleville tarkkailijoille kutsumalla eNotify()-metodia. Tapaus, jossa ei ole tarkkailijoita (esimerkiksi eräsovelluksessa), optimoidaan valvomalla ilmoitusobjektin (ENotificationImpl) rakennetta ja eNotify()-kutsua esittämällä kutsu eNotificationRequired(). Oletusarvon mukaisena toteutuksena eNotificationRequired() yksinkertaisesti tarkastaa, onko objektiin liitettynä tarkkailijoita (sovittimia). Kun EMF-objekteja käytetään ilman tarkkailijoita, eNotificationRequired()-kutsu ei tuota muuta kuin tehollisen paikanvarausosoittimen tarkistuksen, joka muunnetaan koodiksi JIT-kääntäjää käytettäessä.

Luodut käsittelyobjektikaavat muunlajisille määritteille, kuten stringi-lajiselle otsikkomääritteelle, poikkeavat hieman sivuille näytetyistä otsikkomääritteistä. Ne ovat kuitenkin perustaltaan samanlaisia[4] .

Viitteille luodut käsittelyobjektit, erityisesti kaksisuuntaiset, ovat hieman tätä monimutkaisempia ja alkavat näyttää EMF-luontitoiminnon todellisen arvon.

Yksisuuntaiset viitteet

Laajennamme seuraavaksi esimerkkimallia Writer-luokalla, josta on liitäntä Book-luokkaan:

Yksisuuntainen viite: Book-luokalla on yksittäinen Writer-luokan tekijä, joka sisältää nimen: String

Kirjan ja kirjoittajan välinen liitäntä on tässä esimerkissä yksittäinen yksisuuntainen viite. Viitteen (roolin) nimi, jonka avulla käytetään Writer-luokkaa Book-luokasta, on tekijä.

Mallin ajo EMF-luontitoiminnolla tuottaa uuden Writer-liittymän ja WriterImpl-toteutusluokan luomisen lisäksi hankinnan ja asetuksen lisämetodit Book-liittymässä:

  Writer getAuthor();
  void setAuthor(Writer value);

Tekijäviite on yksisuuntainen, joten setAuthor()-metodin toteutus muistuttaa läheisesti yksinkertaista tiedon asettajaa, kuten aiemmin setPages()-metodille:

  public void setAuthor(Writer newAuthor)
  {
    Writer oldAuthor = author;
    author = newAuthor;
    if(eNotificationRequired())
      eNotify(new ENotificationImpl(this, ...));
  }

Ainoa ero on, että tässä me asetamme objektinosoittimen pelkän yksinkertaisen tietokentän asemesta.

Käsittelemme objektiviitettä, minkä vuoksi getAuthor()-metodi on hieman monimutkaisempi. Tähän on syynä se, että tietynlajisten viitteiden, kuten tekijän lajisten viitteiden, hakumetodin on käsiteltävä mahdollisuus, että viiteobjekti (tässä tapauksessa Writer) voi olla lähdeobjektiin (tässä tapauksessa Book) perustuvassa eri resurssissa (asiakirjassa). EMF-pysyvyyskehys käyttää tarvittaessa latautuvaa skeemaa, joten objektiosoitin (tässä tapauksessa tekijä) voi jonakin ajankohtana olla objektille välityspalvelin todellisen viiteobjektin sijasta[5] . Tämän tuloksena getAuthor()-metodi näyttää tältä:

  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;
  }

Tekijän ilmentymämuuttujan yksinkertaisen palutuksen asemesta kutsumme ensin perityn kehysmetodin eIsProxy(), joka tarkistaa, onko viite välityspalvelin ja esittää myönteisessä tapauksessa kutsun eResolveProxy(). Jälkimmäinen metodi esittää kutsun EcoreUtil.resolve() eli kutsuu staattisen apuohjelmametodin, joka yrittää ladata kohdeobjektin asiakirjan ja siten myös objektin käyttämällä välityspalvelimen URI-tunnistetta. Onnistuminen palauttaa selvitetyn objektin. Jos asiakirja ei kuitenkaan onnistu latautumaan, se palaa jälleen takaisin välityspalvelimeksi[6] .

Kaksisuuntaiset viitteet

Tiedämme nyt, miten välityspalvelimen selvitys vaikuttaa tietynlajisten viitteiden hakukaavaan. Voimme siten tarkastella sitä, miten asetettu kaava muuttuu, kun liitäntä tehdään kaksisuuntaisena. Muutetaan yksisuuntainen tekijän liitäntä tämän näköiseksi:

Kaksisuuntainen viite: tekijä voi olla kirjoittanut 0 kirjaa tai sitä enemmän kirjoja, mutta kirjalla voi olla vain yksi tekijä

Liitäntä on nyt kaksisuuntainen, minkä ilmaisee liitäntärivin Writer-päästä puuttuva nuolenpää. Roolin nimi, jota käytetään kirjojen käyttämiseen Writer-luokasta, on books.

Jos luomme uudelleen mallimme, getAuthor()-metodi ei muutu, mutta setAuthor() näyttää nyt tältä:

  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
  }

Kuten huomaat, tekijän kaltaisen kaksisuuntaisen viitteen asetuksessa viitteen toinen pää on myös asetettava (esittämällä eInverseAdd()-kutsu). Meidän on lisäksi poistettava mahdollinen aiempi purettu tekijä (esittämällä kutsu eInverseRemove()), koska mallissamme tekijäviite on yksikössä (kirjalla voi olla vain yksi tekijä)[7]  eikä kirja sen vuoksi voi olla kuin yhden kirjoittajan books-viitteessä. Lopuksi asetamme tekijäviitteen kutsumalla toisen luodun metodin (basicSetAuthor()), joka näyttää tältä:

  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;
  }

Tämä metodi näyttää hyvin samanlaiselta kuin yksisuuntainen viitteen asetusmetodi. Jos kuitenkin msgs-argumentti on muu kuin null, ilmoitus lisätään siihen sen sijaan, että se toteutettaisiin suoraan[8] . Kaksisuuntaisen asetustoiminnon aikana tehtyjen välitystoimien/käänteisten lisäysten/poistojen perusteella enintään neljä (tässä esimerkissä kolme) eri ilmoitusta voidaan luoda. Kaikki yksilölliset ilmoitukset kerätään NotificationChain-luokan avulla siten, että niiden toteutusta voidaan lykätä siihen asti, että kaikki tilamuutokset on tehty. Jonoon asetetut ilmoitukset lähetetään esittämällä kutsu msgs.dispatch(), kuten edellä esitetyssä setAuthor()-metodissa.

Kertaluku-monta-viitteet

Olet ehkä huomannut esimerkistä, että kirjojen liitäntä (Writer-luokasta Book-luokkaan) on kertaluku-monta (toisin sanoen 0..*). Yksi kirjailija on toisin sanoen voinut kirjoittaa monta kirjaa. Kertaluku-monta-viitteitä (kaikkia viitteitä, jossa yläraja on suurempi kuin 1) käsitellään EMF-kehyksessä käyttämällä kokoelman API-liittymää. Siten vain hakumetodi luodaan liittymässä:

  public interface Writer extends EObject
  {
    ...
    EList getBooks();
  }

Huomaa, että getBooks() palauttaa EList-luettelon java.util.List-luettelon asemesta. Ne ovat todellisuudessa lähes samat. EList on java.util.List-luettelon EMF-aliluokka, joka lisää kaksi siirtomenetelmää API-liittymään. Voit muilta osin käsitellä sitä normaalina Java-luettelona työaseman näkökulmasta. Voit esimerkiksi lisätä kirjan kirjojen liitäntään esittämällä yksinkertaisesti seuraavan kutsun:

  aWriter.getBooks().add(aBook);

Tai voit myös iteroida ne tekemällä jotakin tältä näyttävää:

  for (Iterator iter = aWriter.getBooks().iterator(); iter.hasNext(); )
  {
    Book book = (Book)iter.next();
    ...
  }

Työaseman näkökulmasta kertaluku-monta-viitteiden käsittelyyn käytetty API ei ole mitenkään erikoinen. Books-viite on osa kaksisuuntaista liitäntää (purettu Book.author-menetelmästä), joten meidän on vielä tehtävä hieno purkukättely, jonka näytimme setAuthor()-menetelmälle. GetBooks()-metodin toteutus WriterImpl-luokassa näyttää meille, miten kertaluku-monta-tapaus käsitellään:

  public EList getBooks()
  {
    if (books == null)
    {
      books = new EObjectWithInverseResolvingEList(Book.class, this,
                    LibraryPackage.WRITER__BOOKS, LibraryPackage.BOOK__AUTHOR);
    }
    return books;
  }

GetBooks()-metodi palauttaa erityisen toteutusluokan EObjectWithInverseResolvingEList, joka on muodostettu käyttämällä kaikkia tietoja, joita luokka tarvitsee purkukättelyyn lisäys- ja poistokutsujen aikana. EMF-kehys toimittaa 20 erilaista erikoistunutta EList-toteutusta[9] , joilla voidaan tehokkaasti toteuttaa kaikenlajiset kertaluku-monta-ominaisuudet. Yksisuuntaisissa liittymissä (ilman purkua olevissa) käytämme EObjectResolvingEList-toteutusta. Jos viite ei vaadi välityspalvelimen selvittämistä, käytämme esimerkiksi EObjectWithInverseEList- tai EObjectEList-toteutusta.

Esimerkissämme books-viitteen toteutukseen käytetty luettelo luodaan argumentilla LibraryPackage.BOOK__AUTHOR (luotu staattinen int-vakio, joka edustaa purkuominaisuutta). Tätä käytetään add()-kutsun aikana eInverseAdd()-menetelmän kutsumiseen kirjalle samalla tavalla kuin eInverseAdd()-menetelmää kutsuttiin kirjoittajalle setAuthor()-metodin aikana. eInverseAdd() näyttää tältä BookImpl-luokassa:

  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:
          ...
      }
    }
    ...
  }

Tässä kutsutaan ensin eInverseRemove(), joka poistaa kaikki edelliset tekijät (aiemmin setAuthor()-metodin tarkastelussa kuvatulla tavalla) ja kutsuu sitten basicSetAuthor()-metodin todella asettamaan viitteen. Esimerkissämme on vain yksi kaksisuuntainen viite, mutta eInverseAdd() käyttää switch-lausetta, joka sisältää tapauksen kaikille kaksisuuntaisille viitteille, jotka ovat käytettävissä Book-luokassa[10] .

Sisällytysviitteet

Lisätään uusi luokka Library, joka toimii Books-luokan säilönä.

Sisällytysviite: Library sisältää 0 kirjaa tai sitä enemmän kirjoja

Sisällytysviite ilmaistaan mustalla vinoneliöllä liitännän Library-päässä. Liitäntä ilmaisee täydellisenä, että Library koostuu arvon mukaan 0:sta tai sitä useammasta kirjasta. Liittymät, joissa koonti tapahtuu arvon mukaan (sisällytys), ovat erityisen tärkeitä, koska ne tunnistavat päätason tai kohdeilmentymän omistajan, joka viittaa objektin fyysiseen sijaintiin ollessaan pysyvä.

Sisällytys vaikuttaa luotuun koodin monella tavalla. Ensinnäkin sisältyvä objekti on taatusti samassa resurssissa kuin säilönsä, joten välityspalvelimen selvitystä ei tarvita. Luotu hakumetodi käyttää LibraryImpl-luokassa muuta kuin selvittävää EList-toteutusluokkaa:

  public EList getBooks()
  {
    if (books == null)
    {
      books = new EObjectContainmentEList(Book.class, this, ...);
    }
    return books;
  }

Välityspalvelimen selvityksen ajamatta jättämisen lisäksi EObjectContainmentEList toteuttaa contains()-toiminnon hyvin tehokkaasti (muuttumattomassa ajassa verrattuna lineaariseen aikaan yleisessä tapauksessa). Tämä on erityisen tärkeää sen vuoksi, että kaksoismerkintöjä ei sallita EMF-viiteluetteloissa. Siten contains()-toiminto kutsutaan myös add()-toimintojen aikana.

Objektilla voi olla vain yksi säilö. Jos objekti lisätään sisällytysliitäntään, se on silloin poistettava muista säilöistä, joissa se mahdollisesti on, todellisesta liitännästä riippumatta. Jos esimerkiksi kirja lisätään kirjaston kirjaluetteloon, se voidaan joutua poistamaan muiden kirjastojen kirjaluettelosta. Tämä ei poikkea muista kaksisuuntaisista liitännöistä, joissa purkamisen kertaluku on 1. Oletetaan kuitenkin, että Writer-luokalla on lisäksi sisällytysliitäntä Book-luokkaan nimellä ownedBooks. Jos määritetty kirjailmentymä on jonkin Writer-luokan ownedBooks-luettelossa ja jos se se lisätään kirjaston books-viitteeseen, se on ensin poistettava Writer-luokasta.

Jotta tällainen voidaan toteuttaa tehokkaasti, perusluokalla EObjectImpl on EObject-lajinen ilmentymämuuttuja (eContainer), jota se käyttää säilön yleiseen säilytykseen. Tämän tuloksena sisällytysviitteet ovat aina implisiittisesti kaksisuuntaisia. Voit käyttää kirjastoa Book-luokasta kirjoittamalla jotakin tältä näyttävää:

  EObject container = book.eContainer();
  if (container instanceof Library)
    library = (Library)container;

Jos haluat ehkäistä siirron alaspäin, voit muuttaa liitännän sen sijaan ekplisiittisesti kaksisuuntaiseksi:

Kaksisuuntainen sisällytysviite: kirjasto sisältää 0 kirjaa tai sitä enemmän kirjoja; kirjat sisältyvät kirjastoon

Anna EMF-kehyksen luoda kätevä ja lajin suhteen turvallinen hakumetodi puolestasi:

  public Library getLibrary()
  {
    if (eContainerFeatureID != LibraryPackage.BOOK__LIBRARY) return null;
    return (Library)eContainer;
  }

Huomaa, että eksplisiittinen hakumetodi käyttää eContainer-muuttujaa EObjectImpl-luokasta luodun ilmentymämuuttujan asemesta, kuten näimme aiemmin muiden kuin säilöviitteiden (kuten getAuthor() kohdalla aiemmin)[11] .

Luettelointimääritteet

Olemme tähän mennessä tarkastelleet, miten EMF käsittelee yksinkertaisia määritteitä ja erilajisia viitteitä. Luettelointi on toinen yleisesti käytetty määritelaji. Luettelointi-lajiset määritteet toteutetaan käyttämällä lajiturvallista Java-luettelointikaavaa[12] .

Voimme lisätä luettelointimääritteen, luokan, Book-luokkaan:

Luettelointimäärite ja määritelmä: kirjalla on BookCategory-luokan luokka, joka on luettelo Mystery, ScienceFiction ja Biography

Luo uudelleen toteutusluokat. Book-liittymä sisältää nyt haku- ja asetusmetodit luokalle:

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

Luodussa liittymässä luokkamenetelmät käyttävät lajiturvallista numerointiluokkaa nimeltä BookCategory. Tämä luokka määrittää staattiset vakiot luetteloinnin arvoille ja muille helpoutta lisääville menetelmille tämän näköisesti:

  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);
    }
  }

Esitetyllä tavalla luettelointiluokka toimittaa staattiset int-vakiot luetteloinnin arvoille sekä staattiset vakiot luetteloinnin singleton-literaaliobjekteille itselleen. Int-vakioiden nimet ovat samat kuin mallin literaalinimet[13] . Literaalivakioiden nimet ovat samat, minkä lisäksi niihin on liitetty _LITERAL.

Vakiot ovat helppo tapa käyttää literaaleja esimerkiksi kirjan luokan asetuksen yhteydessä:

  book.setCategory(BookCategory.SCIENCE_FICTION_LITERAL);

BookCategory-konstruktori on yksityinen, minkä vuoksi ainoat luettelointiluokan ilmentymät, jotka ovat koskaan olemassa, ovat ne, joita käytetään staattisissa literaaleissa MYSTERY_LITERAL, SCIENCE_FICTION_LITERAL ja BIOGRAPHY_LITERAL. Tämän tuloksena yhtäläisyysvertailuja (eli .equals()-kutsuja) ei tarvita koskaan. Literaaleja voidaan aina verrata luotettavasti käyttämällä yksinkertaisempaa ja tehokkaampaa operaattoria == , joka näyttää tältä:

  book.getCategory() == BookCategory.MYSTERY_LITERAL

Kun vertailu tehdään monia arvoja vasten, int-arvoja käyttävä switch-lause on vielä parempi:

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

Tilanteissa, joissa on käytettävissä vain literaalinimi (String) tai arvo (int), myös helppoutta parantavat get()-menetelmät, joita voidaan käyttää vastaavan literaaliobjektin noutamiseen, luodaan luettelointiluokassa.

Factoryt ja paketit

Malliliittymien ja toteutusluokkien lisäksi EMF luo vähintään kaksi muuta liittymää (ja toteutusluokkaa): factoryn ja paketin.

Factorya käytetään nimensä mukaisesti malliluokkien ilmentymien luomiseen, kun paketti toimittaa joitakin staattisia vakioita (esimerkiksi luotujen metodien käyttämät ominaisuusvakiot) ja mallin metatietojen käyttöä helpottavat metodit[14] .

Tässä on Book-esimerkin factory-liittymä:

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

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

    LibraryPackage getLibraryPackage();
  }

Luotu factory toimittaa kuvatulla tavalla factory-menetelmän (create) kaikille mallissa määritetyille luokille, käsittelyobjektin mallin paketille ja staattisen vakioviitteen (eINSTANCE) factory singleton -metodiin.

LibraryPackage-liittymän ansiosta kaikkia mallin metatietoja voi käyttää helposti:

  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();

    ...
  }

Metatiedot ovat käytettävissä kahdessa muodossa: int-vakioina ja Ecore-metaobjekteina itsessään. Int-vakioilla voi tehokkaimmin siirtyä metatiedoissa. Olet ehkä huomannut, että luodut metodit käyttävät näitä vakioita toteutuksissaan. Käsittelemme jäljempänä sitä, miten EMF-kehyksen sovittimet voidaan toteuttaa. Näet silloin, että vakioilla voi myös tehokkaimmin määrittää, mitä ilmoitusten käsittelyssä on muuttunut. Factoryn tavoin luotu pakettiliittymä toimittaa staattisen vakioliitteen singleton-toteutukseen.

Luokkien luominen yliluokilla

Haluamme ehkä luoda Book-malliluokan SchoolBook-nimisen aliluokan, joka näyttää tältä:

Yksittäinen periytyminen: SchoolBook on Book-luokan laajennus

EMF-kehyksen luontitoiminto käsittelee yksittäistä periytymistä odottamallasi tavalla: luotu liittymä on yliliittymän laajennus:

  julkinen SchoolBook-liittymä on Book-luokan laajennus

ja toteutusluokka on ylitoteutusluokan laajennus:

  julkinen SchoolBookImpl-luokka on BookImpl-luokan laajennus ja toteuttaa SchoolBook-luokan

Samoin kuin Javassa itsestään useiden liittymien periytymistä tuetaan. EMF-luokka voi kuitenkin olla aina vain yhden perustoteutusluokan laajennus. Jos meillä on moniperiytyvä malli, meidän on tunnistettava useista perusluokista ne, joita pitää käyttää perustoteutusluokkana. Muita kohdellaan sen jälkeen yksinkertaisesti yhdistyvinä liittyminä, joiden toteutukset luodaan johdettuun toteutusluokkaan.

Oletetaan esimerkiksi, että käytät seuraavaa koodia:

Moniperiytyminen: SchoolBook on Book- ja Asset-luokan laajennus (Asset-luokan arvo: liukuluku)

Tässä SchoolBook on johdettu kahdesta luokasta: Book ja Asset. Olemme tunnistaneet Book-luokan perustoteutusluokaksi (laajennettu) esitetyllä tavalla[15] . Jos luomme mallin uudelleen, SchoolBook-liittymästä tulee kahden liittymän laajennus:

  julkinen SchoolBook-liittymä on Book- ja Asset-luokkien laajennus

Toteutusluokka näyttää samalta kuin aiemmin. Nyt se kuitenkin sisältää yhdistettyjen metodien getValue() ja setValue() toteutukset:

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

    public void setValue(float newValue)
    {
      ...
    }
    
    ...
  }

Luotujen toteutusluokkien mukautus

Voit lisätä toimintatavan (metodit ja ilmentymämuuttujat) luotuihin Java-luokkiin joutumatta pelkäämään muutosten menettämistä silloin, jos myöhemmin päätät muuttaa mallia ja tehdä sitten uudelleenluonnin. Voimme esimerkiksi lisätä isRecommended()-metodin Book-luokkaan. Sitä varten sinun tarvitsee yksinkertaisesti vain lisätä uusi metodin allekirjoitus Javan Book-liittymään:

  public interface Book ...
  {
    boolean isRecommended();
    ...
  }

Sen toteutus BookImpl-luokassa:

  public boolean isRecommended()
  {
    return getAuthor().getName().equals("William Shakespeare");
  }

EMF-kehyksen luontitoiminto ei tyhjennä muutosta, koska se ei alun perin ole luotu metodi. Kaikki EMF-kehyksen luomat metodit sisältävät Javadoc-kommentin, joka sisältää @-generoidun tunnisteen, joka näyttää tältä:

  /**
   * ...
   * @generated
   */
  public String getTitle()
  {
    return title;
  }

Kaikki tiedostoon sisältyvät metodit, jotka eivät sisällä tätä tunnistetta (kuten isRecommended()), pysyvät koskemattomina uudelleenluonnin aikana. Jos haluamme muuttaa luodun metodon toteutusta, voimme tehdä niin poistamalla @-generoidun tunnisteen kohteesta it[16] :

  /**
   * ...
   * @generated // (removed)
   */
  public String getTitle()
  {
    // mukautettu toteutuksemme ...
  }

Puuttuvan @-generoidun tunnisteen vuoksi getTitle()-metodi katsotaan käyttäjäkoodiksi; jos luomme mallin uudelleen, luontitoiminto havaitsee törmäyksen ja yksinkertaisesti hylkää metodista luodun version.

Ennen luodun metodin hylkäämistä luontitoiminto tarkastaa ensin, onko tiedostossa toista samannimistä luotua metodia, jonka nimessä olisi lisäksi Gen. Jos sellainen löytyy, luontitoiminto ei hylkää juuri luotua metodin versiota, vaan ohjaa tulosteen edelleen siihen. Jos esimerkiksi haluamme luoda luodusta getTitle()-toteutuksesta laajennuksen sen sijaan, että hylkäisimme sen kokonaan, voimme yksinkertaisesti nimetä sen uudelleen seuraavan näköisesti:

  /**
   * ...
   * @generated
   */
  public String getTitleGen()
  {
    return title;
  }

Sen jälkeen voimme lisätä ohituksen käyttäjämetodina, joka tekee, mitä haluamme:

  public String getTitle()
  {
    String result = getTitleGen();
    if (result == null)
      result = ...
    return result;
  }

Jos teemme mallin uudelleenluonnin nyt, luontitoiminto havaitsee törmäyksen getTitle()-metodin käyttäjäversion kanssa. Olemme kuitenkin luoneet myös @-generoidun getTitleGen()-metodin luokassa, joten luontitoiminto ohjaa juuri luodun toteutuksen edelleen siihen hylkäämättä toteutusta.

Käyttö EMF-malleissa

Määritteiden ja viitteiden lisäksi voit lisätä käyttötoimintoja malliluokkiin. Jos teet niin, EMF-kehyksen luontitoiminto luo niiden allekirjoituksen liittymään ja metodirakenteen toteutusluokkaan. EMF ei mallinna toimintapaa, joten toteutus täytyy toimittaa käyttäjän kirjoittamalla Java-koodilla.

Tämä voidaan tehdä poistamalla @-generoitu tunniste luodusta toteutuksesta edellä kuvatulla tavalla ja lisäämällä koodi suoraan sinne. Vaihtoehtoisesti Java-koodi voidaan sisällyttää suoraan malliin.Rose-mallissa voit lisätä sen toiminnon erittelyn valintaikkunassa merkitysopin välilehdessä olevaan tekstikenttään. Koodi tallennetaan sen jälkeen EMF-mallissa kommenttina toimintoon[17]  ja luodaan sen runkoon.

Luotujen EMF-luokkien käyttö

Ilmentymien luonti ja käyttö

Luotujen luokkien avulla työasemaohjelma voi luoda ja alustaa Book-luokan seuraavilla yksinkertaisilla Java-lauseilla:

  LibraryFactory factory = LibraryFactory.eINSTANCE;

  Book book = factory.createBook();

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

  book.setTitle("King Lear");
  book.setAuthor(writer);

Kirja kirjoittajaan -liitäntä (tekijä) on kaksisuuntainen, joten purkuviite (books) alustetaan automaattisesti. Voimme tarkistaa tämän iteroimalla books-viitteen tämän näköisesti:

  System.out.println("Shakespeare books:");
  for (Iterator iter = writer.getBooks().iterator(); iter.hasNext(); )
  {
    Book shakespeareBook = (Book)iter.next();
    System.out.println("  title: " + shakespeareBook.getTitle());
  }

Ohjelman ajo tuottaisi seuraavan näköisen tulosteen:

  Shakespeare books:
    title: King Lear

Resurssien tallennus ja lataus

Voimme luoda mylibrary.xmi-nimisen asiakirjan, joka sisältää edellä esitetyn mallin, yksinkertaisesti luomalla EMF-resurssin ohjelman alussa, viemällä kirjan ja kirjoittajan resurssiin ja esittämällä save()-kutsun lopussa:

  // Luo resurssijoukko.
  ResourceSet resourceSet = new ResourceSetImpl();

  // Rekisteröi oletusarvon mukainen resurssin factory -- tarpeen vain itsenäiselle!
  resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().put(
    Resource.Factory.Registry.DEFAULT_EXTENSION, new XMIResourceFactoryImpl());

  // Nouda mallitiedoston URI.
  URI fileURI = URI.createFileURI(new File("mylibrary.xmi").getAbsolutePath());

  // Luo resurssi tälle tiedostolle.
  Resource resource = resourceSet.createResource(fileURI);

  // Lisää kirja- ja kirjoittaja-objektit sisältöihin.
  resource.getContents().add(book);
  resource.getContents().add(writer);

  // Tallenna resurssin sisällöt tiedostojärjestelmään.
  try
  {
    resource.save(Collections.EMPTY_MAP);
  }
  catch (IOException e) {}

Huomaa, että resurssijoukkoa (ResourceSet-liittymä) käytetään EMF-resurssin luomiseen. EMF-kehys käyttää resurssijoukkoa sellaisten resurssien hallinnassa, joissa voi olla asiakirjan ristiviitteitä. Rekisterin (liittymän Resource.Factory.Registry) avulla se luo oikeanlajisen resurssin annetulle URI-osoitteelle, joka perustuu sen skeemaan, tiedoston tunnisteeseen tai muihin mahdollisiin ehtoihin. Tässä rekisteröimme XMI-resurssin toteutuksen oletuksena tälle resurssijoukolle[18] . Latauksen aikana resurssijoukko myös hallitsee asiakirjan ristiviitteiden tarpeen mukaan tapahtuvaa latausta.

Ohjelman ajo tuottaa mylibrary.xmi-tiedoston, jonka sisältö näyttää jokseenkin tältä:

  <xmi:XMI xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI"
      xmlns:library="http:///library.ecore">
    <library:Book title="King Lear" author="/1"/>
    <library:Writer name="William Shakespeare" books="/0"/>
  </xmi:XMI>

Voimme ladata mylibrary.xmi-tiedoston, joka tallennettiin edellä, asettamalla resurssijoukon ja lataamalla tarpeen mukaan siihen resurssin seuraavasti:

   // Luo resurssijoukko.
   ResourceSet resourceSet = new ResourceSetImpl();

  // Rekisteröi oletusarvon mukainen resurssin factory -- tarpeen vain itsenäiselle!
  resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().put(
    Resource.Factory.Registry.DEFAULT_EXTENSION, new XMIResourceFactoryImpl());

  // Rekisteröi paketti -- tarpeen vain itsenäiselle!
  LibraryPackage libraryPackage = LibraryPackage.eINSTANCE;

   // Nouda mallitiedoston URI.
   URI fileURI = URI.createFileURI(new File("mylibrary.xmi").getAbsolutePath());

   // Lataa resurssi tarpeen mukaan tiedostolle.
   Resource resource = resourceSet.getResource(fileURI, true);

   // Tulosta resurssin sisältö kohteeseen System.out.
   try
   {
     resource.save(System.out, Collections.EMPTY_MAP);
   }
   catch (IOException e) {}

Luomme resurssijoukon ja rekisteröimme itsenäisessä tapauksessa oletusarvon mukaisen resurssin toteutuksen. Meidän on lisäksi varmistettava, että paketti rekisteröidään pakettirekisterissä, jota resurssi käyttää asianmukaisten metatietojen ja factoryn noutamiseen ladattavalle mallille. Luodun pakettiliittymän eINSTANCE-kentän käyttö riittää varmistamaan sen rekisteröinnin.

Tässä esimerkissä on käytössä toinen save()-muoto, joka käyttää OutputStream-virtaa sarjoituksen tulostuksessa konsoliin.

Malli on helppo jakaa moneen asiakirjaan, joiden välillä on ristiviitteitä. Voisimme sarjoittaa kirjat ja kirjailijat edellä esitetyssä tallennusesimerkissä erillisiin asiakirjoihin luomalla toisen resurssin:

  Resource anotherResource = resourceSet.createResource(anotherFileURI);

Lisää kirjailija siihen ensimmäisen asemesta:

  resource.getContents().add(writer); // (replaced)
  anotherResource.getContents().add(writer);

Tämä tuottaisi kaksi resurssia, joista kumpikin sisältäisi yhden objektin ja ristiviitteen toiseen asiakirjaan.

Sisällytysviite edellyttää aina, että sisältyvä objekti on samassa resurssissa kuin sen säilö. Tarkastellaan esimerkkiä, jossa luodaan ilmentymä kirjastosta, joka sisältää kirjan kirjojen sisällytysviitteen kautta. Silloin kirja poistetaan automaattisesti resurssin sisällöstä. Myös resurssi toimii silloin tässä mielessä sisällytysviitteen tavoin. Jos kirjasto lisätään sen jälkeen resurssiin, kirja kuuluisi implisiittisesti myös resurssiin ja sen tiedot sarjoitettaisiin uudelleen resurssissa.

Voit myös sarjoittaa objektit muussa muodossa kuin XMI. Sinun on toimitettava oma sarjoitus ja jäsentelykoodi. Luo oma resurssiluokka (ResourceImpl-luokan aliluokkana), joka toteuttaa ensisijaisena pitämäsi sarjoitusmuodon. Rekisteröi se sen jälkeen paikallisesti käyttämällä resurssijoukkoa tai globaalia factory-rekisteriä, jos haluat, että sitä käytetään aina yhdessä mallin kanssa.

EMF-objektien tarkkailu (mukautus)

Kun aiemmin tarkastelimme asetusmetodeja luoduissa EMF-luokissa, havaitsimme, että ilmoitukset lähetetään aina silloin, kun määritettä tai viitettä muutetaan. Esimerkiksi BookImpl.setPages()-metodi sisälsi seuraavan rivin:

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

Kaikki EObject-objektit voivat ylläpitää luetteloa tarkkailijoista (joihin viitataan myös sovittimina), joille ilmoitetaan aina tilan muutoksista. Kehyksen eNotify()-metodi iteroi luettelon läpi ja lähettää ilmoituksen eteenpäin tarkkailijoille.

Tarkkailija voidaan liittää mihin tahansa EObject-objektiin (esimerkiksi kirjaan) lisäämällä eAdapters-luetteloon tämän näköisesti:

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

Yleisempää kuitenkin on, että sovittimet lisätään EObjects-objekteihin käyttämällä sovittimen factorya. Tarkkailijan roolin lisäksi sovittimia käytetään vielä yleisemmin niiden objektien toimintatavan laajentamiseen, joihin ne ovat liitettyinä. Työasemaohjelma kytkee tavallisesti tällaisen toimintatavan laajennuksen pyytämällä sovittimen factorya mukauttamaan objektin, jonka tunniste on oikeanlainen. Tyypillisesti se näyttää jokseenkin tältä:

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

Tavallisesti requiredType edustaa jotakin sovittimen tukemaa liittymää. Argumentti voi esimerkiksi olla todellinen java.lang.Class valitun sovittimen liittymälle. Palautettu sovitin voidaan sen jälkeen siirtää alaspäin pyydettyyn liittymään tämän näköisesti:

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

Sovittimia käytetään usein tällä tavoin laajentamaan objektin toimintatapa ilman aliluokkien luomista.

Ilmoitusten käsittelemiseksi sovittimessa meidän on ohitettava eNotifyChanged()-metodi, jota eNotify() kutsuu kaikilla rekisteröidyillä sovittimilla. Tyypillinen sovitin toteuttaa eNotifyChanged()-menetelmän ajaakseen joitakin toimintoja joillekin tai kaikille ilmoituksille niiden lajin perusteella.

Joskus sovittimet on suunniteltu mukauttamaan tietty luokka (esimerkiksi Book-luokka). Tässä tapauksessa notifyChanged()-metodi voi näyttää jokseenkin tältä:

  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 ...
    }
  }

Kutsu notification.getFeatureID() välitetään argumentille Book.class käsittelemään mahdollisuus, että mukautettu objekti ei ole luokan BookImpl ilmentymä, vaan moniperintäisen aliluokan ilmentymä, jolloin aliluokan Book ei ole ensisijainen (ensimmäinen) liittymä. Tässä tapauksessa ominaisuuden tunnus, joka välitetään ilmoitukseen, on toiseen luokkaan liittyvä numero. Sitä on sen vuoksi muutettava ennen BOOK__-vakioiden avulla tapahtuvaa siirtymistä. Yksittäisen periytymisen tilanteissa tämä argumentti ohitetaan.

Toinen tavallisenlajinen sovitin ei ole sidottu mihinkään tiettyyn luokkaan, vaan käyttää sen sijaan reflektiivistä EMF API -liittymää toiminnon ajoon. Kutsun getFeatureID() esittämisen asemesta ilmoituksella se voi esittää kutsun getFeature(). Tämä palauttaisi todellisen Ecore-ominaisuuden (objektin metamallissa, joka edustaa ominaisuutta).

Reflektiivisen API-liittymän käyttö

Kaikkia luotuja malliluokkia voidaan myös käsitellä käyttämällä reflektiivistä API-liittymää, joka on määritetty EObject-liittymässä:

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

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

Reflektiivisen API-liittymän avulla voimme asettaa tekijän nimen tämän näköisesti:

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

Nimi voidaan noutaa myös tämän näköisesti:

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

Metatieto, joka noudetaan kirjastopaketin singleton-ilmentymästä, tunnistaa käytetyn ominaisuuden.

Reflektiivisen API-liittymän käyttö on hieman vähemmän tehokasta kun luotujen getName()- ja setName()-metodien kutsuminen suoraan[19] , mutta se avaa mallin kokonaan yleiskäytölle. Esimerkiksi EMF.Edit-kehys käyttää reflektiivisiä metodeja toteuttaakseen täyden joukon yleisiä komentoja (esimerkiksi AddCommand, RemoveCommand ja SetCommand), joita voidaan käyttää kaikkien mallien kanssa. Katso lisätietoja kohdasta EMF.Edit-kehyksen yleiskuvaus.

Metodien eGet()- ja eSet() lisäksi reflektiivinen API-liittymä sisältää kaksi muuta liittyvää metodia: eIsSet() ja eUnset(). Metodia eIsSet() voidaan käyttää tutkimaan, onko määrite asetettu vai ei[20] . Metodia eUnset() voidaan käyttää poistamaan (tai nollaamaan) määritteen asetus. Esimerkiksi yleinen XMI-sarjoittaja käyttää eIsSet()-metodia sen määrittämiseen, mitkä määritteet on sarjoitettava resurssin tallennustoiminnon aikana.

Lisäaiheet

Luonnin ohjausliput

Mallin ominaisuudelle voidaan asettaa monia lippuja ohjaamaan kyseiselle ominaisuudelle luotua koodikaaviota. Tyypillisesti lippujen oletusasetukset ovat tarkkoja, joten niitä ei tarvitse muuttaa kovin usein.

Tietolajit

Aiemmin mainitulla tavalla kaikki mallissa määritetyt luokat (esimerkiksi Book, Writer) johtuvat implisiittisesti EMF-kehyksen EObject-perusluokasta. Kaikki mallin käyttämät luokat eivät kuitenkaan ole välttämättä EObjects-luokkia. Esimerkissä haluamme lisätä malliin määritteen, jonka laji on java.util.Date. Ennen sitä meidän on määritettävä EMF-kehyksen tietolaji, joka vastaa ulkoista lajia. UML:ssä tähän tarkoitukseen käytetään luokkaa yhdessä tietolajin stereotyyppi kanssa:

Tietolajin määritelmä: tietolaji JavaDate on luokasta javaclass java.util.Date

Esitetyllä tavalla tietolaji on yksinkertaisesti vain nimetty elementti mallissa, joka toimii välityspalvelimena joissakin Java-luokissa. Todellinen Java-luokka toimitetaan määritteenä yhdessä javaclass-stereotyypin kanssa. Stereotyypin nimi on esitettävä täysin kelvollinen luokka. Kun määritettynä on tämä tietolaji, voimme nyt esittää lajin java.util.Date määritteet tältä näyttävällä tavalla:

Määrite, jossa tietolaji on määritelajina: Book-luokalla on publicationDate: JavaDate

Jos teemme uudelleenluonnin, publicationDate-määrite tulee näkyviin Book-liittymässä:

  import java.util.Date;

  public interface Book extends EObject
  {
    ...

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

Kuten huomasit, tätä Date-lajin määritettä käsitellään melko samalla tavalla kuin mitä tahansa muuta määritettä. Itse asiassa kaikkien määritteiden, kuten String- ja int-lajisten määritteiden, lajina on tietolaji. Ainoa erityinen asia normaaleissa Java-lajeissa on se, että niiden vastaavat tietolajit on määritetty ennalta Ecore-mallissa. Niitä ei sen vuoksi tarvitse määrittää uudelleen kaikissa malleissa, jotka käyttävät niitä.

Tietolajin määritelmällä on vielä toinen vaikutus luotuun malliin. Tietolajit vastaavat jotakin vapaasti määritettävää luokkaa, joten yleinen sarjoittaja ja jäsentäjä (esimerkiksi oletusarvon mukainen XMI-sarjoittaja) ei voi mitenkään tietää, miten senlajisen määritteen tila tallennetaan. Pitäsikö sen esitää toString()-kutsu? Se on kohtuullinen oletus, mutta EMF-kehys ei halua vaatia sitä. Se luo sen vuoksi kaksi lisämetodia factoryn toteutusluokassa jokaiselle mallissa määritetylle tietolajille:

  /**
   * @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);
  }

Oletusarvon mukaan nämä metodit yksinkertaisesti kutsuvat yliluokan toteutukset, jotka toimittavat kohtuulliset, mutta tehottomat oletusarvot: convertToString() esittää yksinkertaisesti toString()-kutsun instanceValue-menetelmälle. Menetelmä createFromString() yrittää kuitenkin Java-heijastusta käyttämällä kutsua String-konstruktonin tai siinä epäonnistuessaan staattisen valueOf()-metodin, jos sellainen on. Tyypillisesti nämä metodit on otettava haltuun (poistamalla @-generoidut tunnisteet) ja vaihdettava sopiviin mukautettuihin toteutuksiin:

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

Ecore-malli

Tässä on täydellinen Ecore-mallin luokkahierarkia (varjostetut ruudut ovat abstrakteja luokkia):

Ecore-luokkahierarkia

Hiearkia sisältää luokat, jotka edustavat tässä artikkelissa kuvattuja EMF-mallin elementtejä: luokkia (ja niiden määritteitä, viitteitä ja toimintoja), tietolajeja, luettelointeja, paketteja ja factoryita.

EMF:n toteutus Ecore-mallista on luotu käyttämällä EMF-kehyksen luontitoimintoa. Siten sen toteutus on samalla tavalla kevyt ja tehokas tämän artikkelin edellisissä osissa kuvatulla tavalla.



[1] Todellisuudessa EMF-metamalli on itsessään EMF-malli, jonka oletusarvon mukainen sarjoitettu muoto on XMI.

[2] EMF tukee tällä hetkellä tuontia Rational Rose -työkalusta, mutta luontitoiminnon arkkitehtuuria voi helposti valmistella myös muita mallinnustyökaluja varten.

[3] EMF käyttää JavaBean-kaavojen alijoukkoa ominaisuuden käsittelyobjektin yksinkertaisessa nimeämisessä.

[4] Luotuja kaavoja voidaan muuttaa monilla käyttäjän määritettävissä olevilla asetuksilla. Kuvailemme niistä joitakin jäljempänä (katso Luonnin ohjausliput jäljempänä tässä asiakirjassa).

[5] Sisällytysviitteiden, jotka kuvailemme jäljempänä (katso Sisällytysviitteet ), laajuuteen eivät voi kuulua asiakirjat. Lisäksi käytettävissä on lippu, jonka käyttäjät voivat asettaa viitteen metatietoihin ilmaisemaan, että selvitystä ei tarvitse kutsua, koska viitettä ei koskaan käytetä asiakirjojen välisissä viitteissä (katso Luonnin ohjausliput). Näissä tapauksissa luotu hakumetodi yksinkertaisesti palauttaa osoittimen.

[6] Sovellusten, joiden on käsiteltävä katkenneet linkit, on esitettävä eIsProxy()-kutsu objektilla, jonka hakumetodi palauttaa sen tarkastamiseksi, onko se selvitetty vai ei (esimerkiksi book.getAuthor().eIsProxy()).

[7] Tämä epäonnistuu selvästi sallimisessa usealle tekijälle, mutta säilyttää esimerkkimallin yksinkertaisena.

[8] Syy, miksi ylipäänsä delegoimme basicSet()-metodille, on se, että sitä tarvisevat myös eInverseAdd()- ja eInverseRemove()-metodit. Tarkastelemme niitä hieman myöhemmin.

[9] Todellisuudessa kaikki konkreettiset EList-toteutukset ovat yksinkertaisia aliluokkia, joiden yliluokka on hyvin toimiva ja tehokas perustoteutusluokka EcoreEList.

[10] Menetelmässä eInverseAdd() kutsutaan järjestelmään kuuluvan ominaisuuden tunnuksen yksinkertaisen kytkennän asemesta ensin eDerivedStructuralFeatureID(featureID, baseClass). Tällä metodilla on yksinkertaisille yksittäisille periytymismalleille oletusarvon mukainen toteutus, joka ohittaa toisen argumentin ja palauttaa välitetyn featureID-tunnuksen. Malleille, jotka käyttävät moniperiytymistä, eDerivedStructuralFeatureID()-menetelmässä voi olla luotu ohitus, joka muokkaa ominaisuuden tunnuksen suhteessa yhdistyvään luokkaan (eli baseClass-luokkaan) ominaisuuden tunnukseksi suhteessa ilmentymän konkreettiseen johdettuun luokkaan.

[11] EObjectImpl-luokalla on lisäksi int-lajinen eContainerFeatureID-ilmentymämuuttuja, joka seuraa, mitä viitteitä on käytössä eContainer-luokassa.

[12] Katso kohta Korvaa Enum-lajit luokilla.

[13] Kunnon Java-ohjelmointityylin noudattamiseksi staattiset vakionimet muunnetaan isoksi kirjaimiksi, jos mallinnetun luetteloinnin literaalinimet eivät jo ole isoina kirjaimina.

[14]  Ohjelmaa ei vaadita ehdottomasti käyttämään Factory- tai Package-liittymiä, mutta EMF kannustaa työasemaohjelmia käyttämään factorya ilmentymien luomisessa luomalla suojattuja konstruktoreita malliluokissa ja estämällä siten sinua yksinkertaisesti kutsumasta uusia ilmentymien luomiseksi. Voit kuitenkin muuttaa käyttöoikeudet manuaalisesti julkisiksi luoduissa luokissa, jos todella haluat sitä. Tekemiäsi asetuksia ei korvata, jos päätät myöhemmin luoda luokat uudelleen.

[15] Todellisuudessa ensimmäinen perusluokka Ecore-mallissa on perusluokka, jota käytetään toteutuksen perusluokkana. UML-kaaviossa stereotyypin <<laajennusta>> tarvitaan ilmaisemaan, että Book-luokan on oltava ensimmäinen Ecore-esityksessä.

[16] Jos tiedät jo etukäteen, että haluat toimittaa oman mukautetun toteutuksen joillekin ominaisuuksille, parempi tapa tehdä se on mallintaa määrite muuttuvana. Tämä ohjaa luontitoiminnon luomaan vain rakenteen metodin rungon, jonka sinun odotetaan sen jälkeen toteuttavan.

[17] EMF sisältää yleisen mekanismin metamallin objektien varustamiseen kommenteilla lisätietojen lisäämiseksi. Mekanismia voidaan käyttää myös liittämään käyttäjän dokumentaatio mallin elementteihin. Kun malli on luotu XML-kaaviosta, EMF jättää sen vastuulle sellaisten sarjoitustietojen keruun, joita ei voida kirjoittaa suoraan käyttämällä Ecore-mallia.

[18] Edellä esitetyn koodin toinen rivi tarvitaan vain itsenäisissä ajoissa (kutsu esitetään suoraan JVM-ympäristössä ja vaaditut EMF JAR -tiedostot ovat luokan polulla). Sama rekisteröinti tehdään automaattisesti yleisessä resurssin factoryn rekisterissä, kun EMF ajetaan Eclipsen sisällä.

[19] Reflektiivisten metodien toteutukset luodaan myös kaikille malliluokille. Ne kytkevät ominaisuuden lajin ja yksinkertaisesti kutsuvat sopivasti luodut lajiturvalliset metodit.

[20] Katso ei-määriteltävää lippua käsittelevästä kohdasta osan Luonnin ohjausliput kohdasta, mikä muodostaa asetetun määritteen.

[21] Harkitse tarkkaan, ennen kuin esittelet ominaisuuden sellaisena, ettei se selvitä välityspalvelimia. Vaikka sinun ei itsesi tarvitsisi käyttää viitettä asiakirjojen ristiviitteenä, joku toinen malliasi käyttävä voi haluta tehdä niin. Ominaisuuden esitteleminen siten, ettei se selvitä välityspalvelimia, on kuin esittelisi Java-luokan lopullisena.