Copyright (C) 2003 Bolour Computing
 Eclipse Corner Article

 

Eclipseプラグイン・アーキテクチャーに関するノート

Notes on the Eclipse Plug-in Architecture

要約
Eclipseプラグインは、構成部品からアプリケーションを作成するためのアーキテクチャー・パターンを具体的な形で例示しています。この記事では、Eclipse workbenchのインスタンス内に見ることができるアーキテクチャー・パターンに関与するオブジェクトの役割と協調動作について考察します。プラグインを理解し、プラグイン拡張の定義方法と処理内容を示すことが目的であり、Eclipse workbenchを使ったプラグインの作成方法は扱いません。

Azad Bolour, Bolour Computing
2003年 7月

目次
1. 概要
2. Eclipseプラグイン・モデル
3. 拡張処理
4. 実例:拡張可能な演算関数サービス
5. リスナー拡張とオブザーバー・パターン
6. まとめと結論


1. 概要

Eclipseは、IDE(Integrated Development Environment:統合開発環境)を作成するための拡張可能なプラットフォームであり、プログラミング作業をサポートするために、協力して動作するツールを制御するための基本サービスを提供します。ツール作成者は、作成したツールを組み込み可能なコンポーネントでラップして、Eclipse platformに与えます(contribute)。組み込み可能なコンポーネントはEclipseプラグイン(Eclipse plug-in)と呼ばれ、Eclipseプラグイン契約に準じます。Eclipseにおける拡張性の基本メカニズムは、新規プラグインはすでに存在するプラグインに新しい処理要素を追加できるということです。Eclipseはこのプロセスを開始するために、基本プラグイン・セットを備えています。このサイトで入手可能な文献に、拡張可能なIDEプラットフォームとしての全般的な紹介があります。

Eclipse platformはIDE作成用に特化していますが、基本となる概念と機能は、複数のベンダーが開発した構成部品からアプリケーションを作成するための一般的なモデルをサポートします。Eclipseにおけるシステム構成の一般モデルは、他のソフトウェアパターンに触れるときに説明します。この記事は、プラグインの作成方法に精通しているかどうかにかかわらず、プラグインの動作原理の全体像を理解したいと思っている新規プラグインの開発者を対象にしています。

この記事は、次のように構成されています。第2節では、Eclipseプラグイン・モデル、プラグインの宣言的仕様、およびモデル内でのプラグイン間の相互関係を概説し、第3節で、プラグイン開発者による拡張可能なプラグインのプログラミング方法を説明します。第4節では、拡張可能プラグインと他のプラグインによる拡張機能の完全に動作する実例を提供し、第5節で、Eclipseプラグイン・モデルと参考文献[1]にあるより単純なオブザーバー・パターン(observer pattern)を対比します。第6節で、Eclipseプラグインで使用されている主要なアーキテクチャー上の概念を要約することで記事を締めくくっています。

ここでは、Eclipseプラグイン開発環境(Plug-in Development Environment:PDE)を使ったプラグインの作成、テスト方法については、一切触れていません。BeckとGamma[2]、Shavor、他[3]、オンライン・ヘルプ・マニュアル、およびこのサイトにある記事に、PDEを使ってプラグインを作成し、テストするのに必要な手順が説明されています。

この記事に掲載されているプラグインのサンプルは、companionプラグインzipファイルに含まれています。サンプルのインストール方法は、この記事の最後に掲載しています。サンプルAPIリファレンスも、この記事の理解に役立ちます。

2. Eclipseプラグイン・モデル

Eclipseにおいてプラグインは、Eclipse workbenchのコンテキスト内で特定のサービスを提供するコンポーネントです。ここでいうコンポーネントとは、インストール時にシステムに組み込まれるオブジェクトのことです。Eclipse runtimeは、協調動作するプラグイン・セットをアクティブにし操作するための基盤を備え、開発作業にシームレスな環境を提供します。実行中のEclipseインスタンス内で、プラグインはプラグイン・ランタイム・クラス(plug-in runtime class)(略してプラグイン・クラス(plug-in class))のインスタンスとして作成されます。プラグイン・クラスは、プラグイン・インスタンスの構成と管理をサポートし、org.eclipse.core.runtime.Pluginクラスを拡張する必要があります。このクラスは、プラグインを管理するための一般的な機能を提供する抽象クラスです。

個々のプラグインは、Eclipseをインストールした場所にあるpluginsフォルダーの下の独自のフォルダーにインストールされます。プラグインの仕様は、プラグイン・フォルダーにあるplugin.xmlと呼ばれるXMLマニフェスト・ファイルに記述されます。マニフェスト・ファイルは、Eclipse runtimeにプラグインをアクティブにするのに必要な情報を提供します。

プラグイン・マニフェスト・ファイルの構文解析された内容は、プラグイン・レジストリーAPI(plug-in registry API)を通してプログラムから利用でき、構文解析されたプラグイン仕様は、プラグイン・レジストリー(plug-in registry)と呼ばれるメモリー内のリポジトリーにキャッシュされます。Eclipse runtimeは、プラグイン・レジストリーAPIを使って各プラグインをインスタンス化します。プラグイン・レジストリーAPIは、プラグインに関する情報を得るためにプロバイダー提供のプラグイン・コードで使用されることもあります。

最小限必要なプラグイン・マニフェスト・ファイルは、以下のようになります。

<?xml version="1.0" encoding="UTF-8"?>
<plugin
  name="JUnit Testing Framework"
  id="org.junit"
  version="3.7"
  provider-name="Eclipse.org">
  <runtime>
    <library name="junit.jar">
      <export name="*"/>
    </library>
  </runtime>
</plugin>
    リスト2.1. 最小限必要なプラグイン・マニフェスト・ファイル

このマニフェスト・ファイルでは、Eclipse workbenchにJUnitテスト基盤を追加するプラグインが記述されています(ここでは、読みやすくするために、マニフェスト・ファイルの内容は英語にローカライズされているということに注意してください)。

Eclipse Platform Plug-in Manifest Specificationには、プラグイン定義で使用されるXML要素と属性の記述があります。この仕様書は、Platform Plug-In Developer Guide/Other reference information/Plug-in manifestの下にあるEclipse platformヘルプ・マニュアルでも利用できます。

現時点では、プラグイン・マニフェスト・ファイルの内容については詳しく触れません。ただし、各プラグインは固有の識別子(XMLのid属性)をもっているということに留意してください。固有の識別子は、他のプラグイン・マニフェスト・ファイル内でプラグインを参照するのに使用され、また以下のように、プラグインの実行インスタンスにアクセスするために、プロバイダー提供のプラグイン・コードでも使用できます。

    Plugin p = Platform.getPlugin(pluginID);

プラグイン・インスタンスはEclipse runtimeで管理され、上記のように、Eclipse platformを使ってアクセスされます。アプリケーション・プログラムがプラグイン・インスタンスを作成することはありません。

2.1. プラグインのインストールとアクティブ化

Eclipseにプラグインをインストールするということは、プラグインを構成するリソース(マニフェスト・ファイル、jarファイル、および他のリソース)を、インストール先のpluginsディレクトリーの下にある個々のプラグイン・フォルダーにコピーするということです。インストールされたプラグインは、その機能が必要になるとEclipse runtimeによってアクティブ化(activate)されます。プラグインのアクティブ化とは、ランタイム・クラスをメモリーにロードし、インスタンス化してそのインスタンスを初期化するということです。

プラグイン・クラスの主な機能は、プラグインのアクティブまたは非アクティブ時に、たとえばリソースの割り当てや解放などの特別な処理を行なうことです。上記のJUnitプラグインのように単純なプラグインでは、アクティブまたは非アクティブ時の処理は特に必要ないので、プラグイン設計者は独自のプラグイン・クラスを用意する必要はありません。このようなときは、Eclipse runtimeがプラグイン・インスタンス用のデフォルト・プラグイン・クラスを自動的に提供します。

プラグインのアクティブまたは非アクティブ時に独自の処理を行なう必要があれば、プラグイン設計者はorg.eclipse.core.runtime.Pluginクラスをサブクラス化し、それぞれstartupshutdownと呼ばれるアクティブ化、非アクティブ化メソッドをオーバーライドします。そして、対応するプラグイン・マニフェスト・ファイルの中で、class属性の値にこの独自のプラグイン・サブクラスの完全修飾名を指定します。

Eclipseは、Eclipse platformまたはEclipse runtimeと呼ばれるプラグイン管理カーネルと、すべてのEclipseのインストールにみられるいくつかのコアなプラグインを含んでいます。これらのコアなプラグインの識別子は、Eclipse platform内にハードコーディングされているので、platformはEclipseの各実行インスタンス内でこのようなプラグインをアクティブにすることができます。一方、コアでないプラグインは、次に示すように、他のプラグインから要求されたときにアクティブになります。

Eclipseモデルでは、プラグインは次に示す関係のいずれかによって他のプラグインと関連付けられます。

このような関係は、プラグイン・マニフェスト・ファイル内でXMLのrequires要素とextension要素を通して宣言的に指定されます(後の節で詳しく説明します)。

Eclipseにインストールされているコアでないプラグインは、そのプラグインが依存関係と拡張関係の組合せによってコアなプラグインから推移的に関連していれば、Eclipseの実行インスタンス内でアクティブ化されます。このようなプラグインは、プラグインの機能が他のプラグインの機能をサポートしたり、拡張するのに必要になったときにアクティブになります。プラグインがインストールされていても、依存関係および拡張関係を通してコアなプラグインから到達できないと、プラグインの観点からはインストールされていないのと同じです。もちろん到達可能なプラグインであっても、ユーザー・アクションや他のイベントがプラグインを起動するきっかけを作らなければ、一定期間(またはインスタンスのライフタイム中)実行インスタンス内で非アクティブのままです。

2.2. 依存関係

プラグインが他のプラグインの機能に依存するとき、依存関係はプラグイン・マニフェスト・ファイル内でrequires要素によって指定されます。ここに実例を示します。

<?xml version="1.0" encoding="UTF-8"?>
<plugin
   id="com.bolour.sample.eclipse.demo"
   name="Extension Processing Demo"
   version="1.0.0">
   <runtime>
      <library name="demo.jar"/>
   </runtime>
   <requires>
      <import plugin="org.eclipse.ui"/>
   </requires>
</plugin>
     リスト2.2. プラグイン依存関係の指定

この例では、サンプル・プラグインcom.bolour.sample.eclipse.demoは、基本Eclipse UIプラグインorg.eclipse.uiに依存している(すなわち使用している)と、宣言しています。

プラグイン・マニフェスト・ファイル内で定義される依存関係は、ランタイム時とコンパイル時の両方に指示を与えます。ランタイム時には、依存プラグインがアクティブになるときは、必要な必須プラグインが利用可能であることを保証する必要があります。コンパイル時には、依存プラグインをコンパイルするのに必要な必須プラグインのすべてのjarファイルをclasspathに追加する必要があります。

2.3. 拡張関係

ユーザーが直接プラグインの機能を利用するには、複数のユーザー・インターフェイス要素を基本Eclipse workbenchに追加する必要があります。たとえば、workbenchのヘルプ・プラグインを利用するには、ヘルプ・メニュー項目をworkbenchユーザー・インターフェイスに追加します。

プラグインに処理要素を追加するプロセスは、拡張機能(extension)と呼ばれます。ただし、このプロセスはUI要素に限定されません。プラグインは、処理要素の追加によってすべてのプラグインを拡張できます。拡張機能はエクステンダー・プラグインで定義され、ホスト・プラグインの動作を変更します。通常、この動作変更によって、ホスト・プラグインに処理要素が追加され(たとえば、Eclipse workbenchへの新規メニュー項目の追加など)、エクステンダー・プラグインが提供するサービスによって、これらの追加要素の動作がカスタマイズされます。(たとえば、独自のメニュー・イベント・ハンドラーによる新規メニュー項目のカスタマイズなど)

単純なケースでは、拡張機能の1つの動作に対して1つのコールバック・オブジェクト(callback object)が、ホスト・プラグインとエクステンダー・プラグインが通信する環境に追加されます。コールバック・オブジェクトは、ホスト・プラグインやエクステンダー・プラグインのオブジェクトとは異なります。プラグイン・オブジェクトは、Eclipse platformが自動的にインスタンス化し管理するコンポーネントですが、コールバック・オブジェクトは、プロバイダーが提供するコードが独自にインスタンス化し管理するプレーンな従来のJavaオブジェクトです。拡張機能の1つの動作は、実行環境に複数のコールバック・オブジェクトを追加できます。たとえば、1つの拡張機能を通して、Eclipseユーザー・インターフェイスにメニュー・セットを追加できます。

拡張モデルは本質的に非常に汎用的に設計されているので、エクステンダー・プラグインがカスタム・コールバック・オブジェクトを常に用意する必要はないということに注意してください。たとえば、コンパイル時にホスト・プラグインで利用できるクラスのオブジェクトだけを使って、ホスト・プラグインの動作を完全に変更することができます。その結果、拡張機能宣言は組み込みクラスのインスタンスを単にパラメーター化するだけです。

同様に、拡張モデルでは本質的に、ホスト・プラグインはインターフェイス上に拡張機能の各側面を直接表示する必要はありません。たとえば、拡張機能はホスト・プラグインのインターフェイス表示をいっさい変更しないで、エクステンダー・プラグインとは無関係にホスト・プラグインで発生する可能性がある特定のイベントの通知を、単に依頼することができます。

プラグインは、さまざまな種類の拡張機能によってそれ自体を拡張できます。たとえば、workbench UIでは、メニューとエディターの両方を拡張できます。どの場合でも、拡張機能は独自の構成セットと動作の要件を満たす必要があります。そのため、拡張可能なプラグインは、拡張機能を組み込みことができる異なるタイプのスロットを用意します。このようなスロットを拡張ポイント(extension point)と呼びます。この記事の残りの部分で、このようなスロットに相当する複合形式の拡張ポイントを扱います。拡張ポイントには、任意の数の拡張機能を組み込むことができます。

拡張機能拡張ポイントは、標準のEclipseプラグインの用語です。ホスト・プラグインエクステンダー・プラグイン、およびコールバック・オブジェクトは、拡張関係において異なる役割を担うオブジェクトを説明するために、この記事で使用する用語です。

図1は、拡張に関与するオブジェクト間の関係を示しています。ここで取り上げるのは、Eclipseヘルプ・システムのメニュー項目によるEclipse workbenchの拡張です。ここでホスト・プラグインは、Eclipse workbenchのユーザー・インターフェイスを実装するorg.eclipse.uiで、そのメニューがactionSetsと呼ばれる拡張ポイントを通して拡張されます。エクステンダー・プラグインは、Eclipseヘルプ・システムのユーザー・インターフェイスを実装するorg.eclipse.help.uiです。ユーザーがヘルプ機能を利用できるように、ヘルプUIプラグインはactionSets拡張ポイントを使って、独自のヘルプ関連メニュー項目Help->Help ContentsおよびSearch->Helpで、workbench UIプラグインを拡張します。拡張機能は、エクステンダー・プラグインで定義します。この例では、単一の拡張機能が、複数のメニュー項目を使ってworkbench UIを拡張しています。

Figure-1

図1. プラグイン拡張に関与するオブジェクト。workbench UIプラグインは、独自のヘルプ関連メニュー項目を定義しているactionSets拡張機能を通して、workbenchヘルプ・プラグインによって拡張される。

(この記事で使用している拡張ポイントのパワー・ストリップ(power-strip)表記は、Don Estberg [5]によって考案されました)

図では、拡張機能のコールバック・オブジェクト・クラス、すなわちヘルプ・システムのメニュー・ハンドラー・クラスも示しています。後で述べるように、コールバック・クラスは、通常、各拡張機能の仕様で宣言されている名前で識別されます。たとえば、この拡張機能のHelp->Help Contentsメニューの仕様では、カスタム・コールバック・クラス(メニュー・ハンドラー)としてHelpContentsActionを宣言しています。拡張機能のSearch->Helpメニューの仕様では、カスタム・コールバック・クラス(メニュー・ハンドラー)としてOpenHelpSearchPageActionを宣言しています。(詳細は、下記をご覧ください)

乱雑になるのを避けるために、これ以降の図では、パッケージ・プレフィックスを省略しています。ここでworkbenchプラグイン・クラスはorg.eclipse.ui.internalパッケージに属しており、ヘルプ・プラグインとその関連クラスはorg.eclipse.help.ui.internalパッケージに属しています。

本節の残りの部分で、拡張ポイントと拡張機能の定義方法を詳しく説明します。

2.3.1 拡張に関与するオブジェクト

拡張に関与するオブジェクトが担うさまざまな役割について、さらに詳しく調べていきましょう。プラグインには2つの役割があります。1つは、ホストとエクステンダーとしての役割であり、もう1つは、各拡張ポイントで定義されるコールバック・オブジェクトの一般的な役割と独自の役割です。

2.3.1.1. ホスト・プラグインの役割

特定の拡張関係において、ホストの役割を担うプラグインは、拡張ポイントを提供し、その拡張ポイントを通して拡張されます。それ自体でサービスを提供することに加え、このようなプラグインは多数の拡張機能の調整役や管理者としても動作します。

ホスト・プラグインのマニフェスト・ファイル内で、拡張ポイントはextension-point XML要素を使って宣言されます。以下に、このような要素の例を示します。この例は、基本Eclipse workbenchのUIプラグインorg.eclipse.uiから抜粋しました。

<?xml version="1.0" encoding="UTF-8"?>
<plugin
 id="org.eclipse.ui"
    name="Eclipse UI"
    version="2.1.0"
    provider-name="Eclipse.org"
    class="org.eclipse.ui.internal.UIPlugin">

 <extension-point id="actionSets" name="Action Sets"
      schema="schema/actionSets.exsd"/>
    <!-- Other specifications omitted. -->
</plugin>
     リスト2.3. 拡張ポイントの宣言

リファレンス・ページに、この拡張ポイントについてのドキュメントが記載されています(Eclipse platformのオンライン・ヘルプ内のトピックPlatform Plugin Developer Guide/Reference/Extension Points Reference/Workbench/org.eclipse.ui.actionSetsでもご覧になれます)。ドキュメントでは、拡張ポイントが基本Eclipse workbenchに追加されるメニュー・セット、メニュー項目、およびボタンに対してプラグイン・スロットを提供していることも述べられています。

拡張ポイントの仕様() では、拡張ポイントに対してホスト・プラグイン内で一意の識別子を定義します。グローバルなコンテキスト内で拡張ポイントを一意に識別するには、拡張ポイントの識別子の前にホスト・プラグイン固有の識別子 () を付加して、拡張ポイントの完全修飾(fully-qualified)識別子を形成します。たとえば、Eclipse UIプラグインで、actionSets拡張ポイントの完全修飾識別子はorg.eclipse.ui.actionSetsです。拡張ポイントを拡張するプラグインは、完全修飾識別子で拡張ポイントを参照します。

拡張ポイントの仕様()では、この拡張ポイントの拡張機能に対するXMLスキーマも宣言します。スキーマはworkbench UIに追加されるメニュー・セット、メニュー項目、およびボタンを宣言するときのシンタックスを規定します。第2.3.3節でこのようなスキーマの構造と内容について詳しく説明します。

2.3.1.2. エクステンダー・プラグインの役割

特定の拡張関係において、エクステンダーの役割を担うプラグインは、拡張機能を定義し、通常、拡張機能を通して独自の機能をホスト・プラグインに追加します。さらに、ホスト・プラグインが独自の処理要素を実行環境に追加できるようにします。拡張機能は、エクステンダー・プラグインのマニフェスト・ファイル内でextension XML要素を使って宣言されます。以下に、エクステンダー・プラグインの例を示します。この例は、org.eclipse.help.uiプラグインのマニフェスト・ファイルから抜粋しました。このプラグインは、2つのメニュー項目を追加して、リスト2.3のactionSets拡張ポイントを拡張します。

<plugin
        id="org.eclipse.help.ui"
        name="Help System UI"
        version="2.1.0"
        provider-name="Eclipse.org"
        class="org.eclipse.help.ui.internal.WorkbenchHelpPlugin">
    <!-- ... -->
    <!-- Action Sets -->
    <extension
         point="org.eclipse.ui.actionSets">
        <actionSet
                label="Help"
                visible="true"
                id="org.eclipse.help.internal.ui.HelpActionSet">
         <action
                label="&Help Contents"
                icon="icons/view.gif"
                helpContextId="org.eclipse.help.ui.helpContentsMenu"
                tooltip="Open Help Contents"
                class="org.eclipse.help.ui.internal.HelpContentsAction"
                menubarPath="help/helpEnd"
                id="org.eclipse.help.internal.ui.HelpAction">
            </action>
            <!-- ... other actionSet elements -->
         <action
                label="&Help..."
                icon="icons/search_menu.gif"
                helpContextId="org.eclipse.help.ui.helpSearchMenu"
             class="org.eclipse.help.ui.internal.OpenHelpSearchPageAction"
                menubarPath="org.eclipse.search.menu/dialogGroup"
                id="org.eclipse.help.ui.OpenHelpSearchPage">
            </action>
        </actionSet>
    </extension>
<!-- ... -->
</plugin>
    リスト2.4. 拡張機能の宣言

このエクステンダー・プラグインでは、actionSets拡張ポイントを完全修飾識別子で参照しています()。

リスト2.4に示したactionSets拡張機能は、2つのアクション()を定義しています。アクションはそれぞれworkbenchのメニュー項目Help->Help ContentsSearch->Helpを通して利用できます。

2.3.1.3. 拡張機能コールバックの役割

特定の拡張関係において、コールバックの役割を担うオブジェクトは、(プラグインではなく)プレーンな従来のJavaオブジェクトです。このオブジェクトは、拡張ポイント契約で指定された特定のイベントがホスト・プラグインで認識されると、ホスト・プラグインから呼び出されます。コールバック・オブジェクトのインターフェイスはホスト・プラグインで提供され、拡張ポイントのドキュメント内で文書化されます。コールバック・オブジェクトは、通常、独自の拡張機能に固有のカスタム・クラスとして実装され、エクステンダー・プラグインの作成者が用意します。エクステンダー・プラグイン内に実装されるコールバック・オブジェクトは、通常、ホスト・プラグインとともにパッケージ化されるコールバック・インターフェイスを参照するため、エクステンダー・プラグインは、普通、ホスト・プラグインにも依存します

2.3.1.3.1. コールバックの独自の役割

各コールバック・オブジェクトは、拡張機能内で独自の役割を果たします。このような独自の役割を単にコールバックの役割(callback role)と呼ぶことにします。コールバックの役割は、拡張機能のXML定義の中で子要素または子孫要素を使って定義されます。たとえば、actionSets拡張ポイントは、actionと呼ばれる子孫要素を使ってコールバックの役割を定義します。複数のコールバック・オブジェクトが、actionSets拡張機能の中でこの役割を担うことがあり、各コールバック・オブジェクトが独自のworkbenchメニューやボタンを提供します。

コールバック・オブジェクトのカスタム実装が必要なときは、通常、対応する拡張ポイントのXMLスキーマに、カスタム・コールバックを実装するクラスの完全修飾名を指定する属性を追加します。リスト2.4()で例示されているように、ヘルプUIプラグインでは、actionクラスを指定するXML属性の名前は「class」です。

エクステンダー・プラグインは、必要な独自のコールバック・オブジェクトを定義し、そのカスタム実装クラスを宣言しますが、コールバック・オブジェクトが作成されるのは、ホスト・プラグインで特定のアクションが実行されたときだけです(一般に拡張機能のための一部のアクションが実行されて、コールバック・オブジェクトがはじめて必要になるときだけです)。たとえば、リスト2.4()で、workbenchのSearch->Helpメニューに対するactionコールバック・クラスは、エクステンダー・プラグインorg.eclipse.help.uiでOpenHelpSearchPageAction(org.eclipse.help.ui.internalパッケージにあります)として指定され、関連するコールバック・インスタンスは、ホストプラグインorg.eclipse.uiで作成されます。この場合、コールバック・インスタンスは、Search->Helpメニューがはじめて起動されたときに作成されます。

エクステンダー・プラグインの拡張機能の設計者は、拡張機能におけるコールバックの役割について理解し、このような役割を果たす具体的なコールバック・オブジェクトを供給する必要があります。コールバックの役割は、拡張ポイントに関連するXMLスキーマ内で独自の要素として定義され(第2.3.3節を参照)、XMLスキーマのドキュメントに記述されます。ドキュメントは、コールバック・オブジェクトが各役割を果たすのに必要なインターフェイスの記述を含んでいる必要があります。

たとえば、以前に紹介したactionSetsリファレンス・ページには、メニュー項目のアクションに対するコールバック・インターフェイスは、org.eclipse.ui.IWorkbenchWindowActionDelegateであることが記述されています。そのため、ヘルプ・システムのメニュー・アクション・ハンドラーは、このインターフェイスを実装する必要があります。このインターフェイスで実際にメニュー・アクションを実行するメソッドは、以下のように宣言されています。

    public void run(org.eclipse.jface.action.IAction action);

新しいサービスがメインのworkbenchメニューを通してアクセス可能になると、サービスを提供するプラグインはIWorkbenchWindowActionDelegate インターフェイスの実装を与えて、その中のrunメソッドがサービスを実施します。実装クラスの完全修飾名は、メニューのコールバック・クラスとしてサービスを提供するプラグインのマニフェスト・ファイルに追加されます。

2.3.1.4. 独自ではないサービスを提供するオブジェクト

拡張機能定義で使用されるすべてのXML要素が、カスタム・コールバックの役割に対応しているわけではありません。たとえば、一部の要素は、単に説明のためのものであり、また対応するUI要素を形成したり、拡張機能の一部を表現するカスタムでない内部のホスト・オブジェクトを作成するために、ホスト・プラグインに特定のパラメーターを与えることがあります。例にあるactionSet要素は、それ自体では(子要素は別にして)カスタム・コールバック・オブジェクトを定義しませんが、このような要素によって、内部オブジェクトが、アクション・セットを表現するためにorg.eclipse.uiプラグイン内で作成されます。

同様に、actionSets拡張ポイントを使えば、actionSetのmenu要素を通して、新しいトップレベルのworkbenchメニューを宣言できます。トップレベル・メニューに関連するアクションは、「下位レベルのメニュー項目リストを表示する」など汎用的なので、エクステンダー・プラグインが独自のコールバック・オブジェクトをトップレベル・メニューに関連付ける必要はありません。workbenchのトップレベル・メニューは、内部のworkbench UIオブジェクトによって記述されます。

拡張ポイントのユーザーは、ホスト・プラグインが供給するこのような独自でない内部オブジェクトに関心をもつ必要はありません。しかし、ホスト・プラグインの設計者は、このような仕組みは複雑な拡張構造を設計する際に柔軟であることを理解する必要があります。拡張構造の一部は、拡張機能で提供されるコールバックとして公開されます。他の部分は、ホスト・プラグインのコード内に汎用的に組み込まれ、拡張機能の対応するXML属性を通してパラメーター化できます。

2.3.2. プラグイン・オブジェクトと拡張機能オブジェクトの関係

Eclipseでは、拡張機能の動作原理は非常に汎用的な概念で、汎用性の全容を理解するのに、プラグイン・オブジェクト、拡張ポイント、およびコールバック・オブジェクトの間に存在する関係のタイプを要約することは、役に立ちます。

  1. 複数の拡張ポイントがホスト・プラグイン内に存在することがあります。

  2. プラグインは拡張ポイントを公開してホスト・プラグインとして動作し、同時にプラグインを拡張してエクステンダー・プラグインとして動作することがあります。

  3. 複数のプラグインが特定の拡張ポイントを拡張することがあります。

  4. 特定のプラグインが特定の拡張ポイントを複数回、拡張することがあります。

  5. エクステンダー・プラグインが異なるホスト・プラグインの異なる拡張機能を含むことがあります。

  6. 特定のプラグインの特定の拡張機能によって、拡張ポイントに対する拡張機能の単一の動作が複数のコールバック・オブジェクトを作成することがあります。

  7. プラグインは自己の拡張ポイントに対して拡張機能を定義できます。

自分自身を拡張するという考え方は、最初は奇妙に思えるかもしれません。自己拡張プラグインの顕著な例は、workbench UI 自身です。このプラグインは、自己の拡張ポイントを拡張して、自己のUIに、たとえばエディターなどのさまざまな機能を追加します。

2.3.3. 拡張ポイント・スキーマ

独自の拡張機能は、エクステンダー・プラグインでXML構成要素を使って定義します。構成要素によって、拡張機能のコールバック・オブジェクトをインスタンス化し、初期化するのに必要な情報が与えられます。また、ホスト・プラグインのインターフェイスと動作をカスタマイズするのに必要な情報も提供されます。 ホスト・プラグインの設計者が拡張ポイントを作成する際には、プラグイン・マニフェスト・ファイル内で拡張ポイントを宣言することに加え、拡張ポイントへの拡張機能に対して構成上のシンタックスを定義する責任も負っています。このシンタックスはXMLスキーマで定義され、actionSets.exsdのように拡張子が.exsdのファイルに格納されます。 スキーマ定義は、ホスト・プラグインのドキュメントの一部になります。(Eclipseには、XMLスキーマ・エディターとドキュメント作成に利用できるフォーマッターが用意されています)

拡張ポイント・スキーマを利用することで、エクステンダー・プラグインの設計者は、拡張機能をパラメーター化する方法を知ることができます(Eclipseには、拡張しようとしている拡張ポイントのXMLスキーマを利用する拡張機能用の構成エディターも用意されています)。

Eclipse platformプラグインの拡張ポイント・スキーマにざっと目を通せば、拡張ポイント・スキーマの構造と内容が理解できます。これらのスキーマは、各Eclipseをインストールしたフォルダーの下にあるplugins/org.eclipse.platform.source_<ver>/src(<ver>はEclipseのバージョンです)で利用できます。特定のplatformプラグインの拡張ポイント・スキーマは、このフォルダーの下の<plugin>_<plugin_ver>/schemaフォルダーにあります。<plugin>はプラグインの識別子で、<plugin_ver>はプラグインのバージョンです。たとえば、Eclipse 2.1の場合、actionSets拡張ポイントのスキーマactionSets.exsdは、platform.sourceフォルダーの下のorg.eclipse.ui_2.1.0/schemaフォルダーにあります。

actionSets.exsdのかなり省略したバージョンを、以下に示します。

<schema targetNamespace="org.eclipse.ui">
   <element name="extension">
      <complexType>
         <sequence>
         <element ref="actionSet" minOccurs="1" maxOccurs="unbounded"/>
         </sequence>
         <attribute name="point" type="string" use="required"> </attribute>
         <attribute name="id" type="string"> </attribute>
         <attribute name="name" type="string"> </attribute>
      </complexType>
   </element>
   <element name="actionSet">
      <complexType>
         <sequence>
            <element ref="menu" minOccurs="0" maxOccurs="unbounded"/>
            <element ref="action" minOccurs="0" maxOccurs="unbounded"/>
         </sequence>
         <attribute name="id" type="string" use="required"> </attribute>
         <attribute name="label" type="string" use="required"> </attribute>
         <attribute name="visible" type="boolean"> </attribute>
         <attribute name="description" type="string"> </attribute>
      </complexType>
   </element>
   <element name="action">
      <complexType>
         <choice>
            <element ref="selection" minOccurs="0" maxOccurs="unbounded"/>
            <element ref="enablement" minOccurs="0" maxOccurs="1"/>
         </choice>
         <attribute name="id" type="string" use="required"> </attribute>
         <attribute name="label" type="string" use="required"> </attribute>
         <attribute name="toolbarPath" type="string">
         <attribute name="icon" type="string"> </attribute>
         <attribute name="tooltip" type="string"> </attribute>
         <attribute name="class" type="string"> </attribute>
      </complexType>
   </element>
</schema>
     リスト2.5. 拡張ポイント・スキーマ定義(.exsdファイル)

スキーマは、いくつかの属性と連続するactionSetを含む要素として、actionSets拡張機能を定義しています。actionSetは、いくつかの属性と連続するmenuとactionを含む要素として定義されています。

このファイルの完全バージョンでは、人間が読めるdocumentation要素を使って各属性に注釈が施されています。拡張ポイント・スキーマのdocumentation要素は、拡張ポイントに対するHTMLリファレンス・ページ(たとえば、この記事で以前に触れたactionSetsリファレンス・ページ)を生成するのに使用されます。これは、Java APIリファレンス・ページがjavadocによって生成されるのと同じ方法です。このようなリファレンス・ページは、ホスト・プラグインのドキュメントの一部として利用され、workbenchのヘルプ・システムに統合されます。

リファレンス・ページは、新規機能を拡張ポイントに組み込むために必要な2つの作業をサポートします。すなわち、

2.3.3.1. 拡張機能メンバー

拡張ポイント・スキーマ定義に何を加えるかは、ホスト・プラグインの設計者次第です。Eclipse Platform Plug-in Manifest Specificationで、extension XML要素は、任意であることを示すXML ANYとして定義されています。ただし、その定義は一般的すぎます。実際、XML extensionの仕様は、常に、要素の(可能な限り退化する)シーケンスです。このシーケンス内の項目を、拡張機能メンバー(extension member)と呼ぶことにします。たとえば、actionSets拡張機能は、actionSetメンバーのシーケンスです。しばしば、拡張機能メンバーは同じ要素タイプをもちます。つまり、シーケンスは同種のメンバーのリストを表わしています。

XMLでは、シーケンス内の各タイプの要素の数は、要素の定義内でminOccurs属性とmaxOccurs属性を使って制限されます。actionSetsスキーマ(リスト2.5 )でこの用法を示します。ここでは、actionSets拡張機能は、少なくとも1つの、さもなければ無制限の数のactionSet要素を含むことができると指定されています。

拡張ポイントが同種のメンバーのシーケンスを定義し、拡張機能が複数のメンバーをもてるとき、拡張ポイントには複数形の名前が割り当てられます。このような拡張ポイントと拡張機能を、複数形式(plural-form)と呼ぶことにします。通常、複数形式は、複数の単独メンバーからなる拡張機能を表現するための略称として使われます。

拡張機能メンバー、すなわち拡張機能の(トップレベルの)シーケンス内のトップレベルのXML要素の概念は、Eclipseの構成要素(configuration element)の概念と対照をなします。構成要素は、拡張機能のXML仕様の中で任意の要素(トップレベルの要素または下位レベルの子孫要素)として定義されています。両方の用語は、ここではXMLの要素自体と、その構文解析されたプログラム表現に対して使用します。

3. 拡張処理

第2節でEclipse拡張モデルを高レベルで示し、拡張機能とそのコールバック・オブジェクトの宣言的仕様を紹介しました。本節では、拡張ポイントの契約下でホスト・プラグインの責務をサポートするために、このような宣言をプログラムでどのように処理するのかを説明します。つまり、ホスト・プラグインの設計者が定義しようとする各拡張ポイントに対して記述する必要があるコードを考えます。

org.eclipse.uiプラグインとactionSets拡張ポイントを、再び取り上げます。このプラグインがアクティブになると、このプラグインを拡張するすべてのプラグインにある拡張機能宣言を処理する必要があります。そのため、UIはメニューとボタン、および対応するメニューとボタンのイベントが発生したときに呼び出すコールバック・オブジェクトの作成方法を知っています。本節では、このタイプの拡張処理をサポートするために、Eclipse platformで用意されているAPI呼び出しを示します。また、これらのAPI呼び出しを使って拡張機能を処理するために、プラグインの開発者が使用する慣用句を例示します。

3.1. 拡張ポイントに対する拡張機能への参照の取得

特定の拡張ポイントに対して拡張機能宣言をどのように処理するのでしょうか。一般に、拡張機能はまったく自由に構成できます。この構成についての知識をもつシステムの一部がホスト・プラグインであり、その設計時に拡張ポイントのスキーマ定義が作成されます。たとえば、UIヘルプ・プラグインorg.eclipse.help.uiにあるactionSets拡張機能の処理方法についての知識は、actionSets拡張ポイントを定義しているプラグイン、すなわちorg.eclipse.uiプラグインに備わっています。

ホスト・プラグインが拡張ポイントに対する拡張機能を処理するには、Eclipse runtimeから、拡張機能のリストと各拡張機能に対して拡張機能メンバーの構文解析されたバージョンを取得する必要があります。

プラグイン・レジストリーAPIが、すべての利用可能なプラグイン仕様の構文解析された表現へのプログラムからのアクセスを提供することを思い出してください。このAPIは、プラグイン、プラグイン・コンポーネント、およびプラグイン間の関係に関する情報を取得するためのメソッドを提供しています。レジストリーAPIの構成要素(IConfigurationElementインターフェイス)は、エクステンダー・プラグインのマニフェスト・ファイル内にある拡張機能要素の構文解析されたバージョンを表現します。以下のサンプルコードでは、拡張ポイントに対する全拡張機能の全メンバーに繰り返しアクセスするためのプラグイン・レジストリーAPIの使用方法を示します。

package com.bolour.sample.eclipse.demo;

import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;

public interface IProcessMember {
    public Object process(IExtension extension,
      IConfigurationElement member);
}

package com.bolour.sample.eclipse.demo;

import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IPluginRegistry;
import org.eclipse.core.runtime.Platform;

public class ProcessExtensions {
 public static void process(String xpid, IProcessMember processor) {
        IPluginRegistry registry = Platform.getPluginRegistry();
        IExtensionPoint extensionPoint =
            registry.getExtensionPoint(xpid);
        IExtension[] extensions = extensionPoint.getExtensions();
        // For each extension ...
     for (int i = 0; i < extensions.length; i++) {
            IExtension extension = extensions[i];
         IConfigurationElement[] elements =
              extension.getConfigurationElements();
            // For each member of the extension ...
         for (int j = 0; j < elements.length; j++) {
                IConfigurationElement element = elements[j];
             processor.process(extension, element);
            }
        }
    }
}
     リスト3.1. 拡張ポイントに対する拡張機能とメンバーの反復処理

ProcessExtensionsクラスは、拡張処理にネストしたループの慣用句を使用するprocessメソッドを備えています()。このメソッドは、特定の拡張ポイントの全拡張機能を繰り返し処理し()、さらに各拡張機能の全メンバーを繰り返し処理します()。processメソッドは、引数にメンバー処理ビジターを与えることで、汎用的に記述されています。メンバー処理ビジターは、処理中の各メンバーにコールバックされるIProcessMemberインターフェイスのインスタンスです ()。

この拡張処理メソッドが、第1引数にactionSets拡張ポイントの識別子、すなわちorg.eclipse.ui.actionSetsをともなって呼び出されると仮定します。actionSets拡張ポイントに対する拡張機能の1つが、リスト2.4のヘルプ拡張機能であり、1つのactionSetメンバーをもっています。外側のループでヘルプ拡張機能に到達すると、actionSetメンバーが内側のループに与えられ、メンバー処理ビジターによって処理されます。

同種の処理ループが、(actionSets拡張ポイントのオーナーである)Eclipse workbenchのUIプラグインで、このプラグインがアクティブになるときに見られます。UIプラグインは、IConfigurationElementと関連するインターフェイス・メソッドを使って、actionSetメンバーの属性とサブ要素を取り出します。そして、これらを使って指定されたメニューとボタン要素をworkbenchのユーザー・インターフェイスに組み込み、必要になるとコールバック・オブジェクトをインスタンス化します。

もちろん、内側のループ内で呼び出されるメンバー処理関数の中で()、ホスト・プラグインは各メンバーを処理するのに必要なことは何でも自由に行なうことができます。メンバーはXML構成要素として与えられます。

3.1.1. 拡張ポイント・メンバーを取得する簡易メソッド

リスト3.1は、IConfigurationElement[] getConfigurationElements()メソッドを使って、拡張機能の全メンバーの構成要素を取得する方法を例示しています(リスト3.1 )。拡張ポイント全体、すなわちIExtensionPointインターフェイスに対して、同一のシグネチャーをもつメソッドも存在します。このメソッドは、拡張ポイントに対する全拡張機能の全メンバーの構成要素を返します。この簡易(shorthand)メソッドを使用すると、拡張機能とメンバーのネストしたループ(リスト3.1 )は、拡張ポイントの全拡張機能の全メンバーを処理する単一のループに還元されます。拡張ポイントを処理するのに、このより単純な慣用句で十分なことがよくあります。

3.1.2. 拡張処理の動作

汎用的な拡張処理クラスのprocessメソッド(リスト3.1 )に、具体的な拡張処理ビジターを与えることによって、拡張処理の動作を体験できます。たとえば、特定の拡張ポイントに対するすべての拡張機能メンバーについて識別情報を出力するには、以下に示すビジター・インターフェイスIProcessMemberの実装を使用できます。

package com.bolour.sample.eclipse.demo;
// imports ...
public class PrintMemberIdentity implements IProcessMember {
    private String memberLabelAttribute = null;
    public PrintMemberIdentity(String memberLabelAttribute) {
        this.memberLabelAttribute = memberLabelAttribute;
    }
    public Object process(IExtension extension,
      IConfigurationElement member) {
        String label =
          extension.getDeclaringPluginDescriptor().getLabel() + "/"
          + member.getAttribute(memberLabelAttribute);
        System.out.println(label);
        return label;
    }
    public static void test(String extensionPoint,
      String memberLabelAttribute) {
        // Validate input ...
        System.out.println("Extension members of " + extensionPoint + ":");
        IProcessMember processor =
          new PrintMemberIdentity(memberLabelAttribute);
        ProcessExtensions.process(extensionPoint, processor);
    }
}
    リスト3.2. 拡張ポイント・メンバーの識別データの印刷

actionSetメンバーには名前がlabelのラベル付け用の属性があり、workbenchの現在のインスタンス内に存在するすべてのactionSetsについて、その識別情報を印刷するには、次の呼び出しを使用します。

    PrintMemberIdentity.test("org.eclipse.ui.actionSets", "label")

この呼び出しは、以下のような出力行を生成します。

Java Development Tools UI/Java Element Creation
Java Development Tools UI/Java Navigation
Help System UI/Help
CVS Team Provider UI/CVS
Extension Processing Demo/Demo Menu Actions
Eclipse UI/Resource Navigation
...

この拡張処理のデモは、この記事のcompanionプラグインにあります。デモを試すには、記事の最後にあるプラグインのインストール方法を参照してください。

3.2. 拡張処理の標準化

拡張処理の最も重要な機能は、拡張機能コールバック・オブジェクトのインスタンス化です。第3.3節で説明しますが、パフォーマンス上の理由から、通常、この機能は実際の作業を行なうためにコールバック・オブジェクトが必要になると、遅延方式(lazy manner)によって実行されます。ただし、本節では、この重要なパフォーマンスの最適化を無視し、かわりにコールバックのインスタンス化における主要な機能の側面に集中します。

Eclipseはコールバック・オブジェクトをインスタンス化し、初期化するための標準的な方法を定義しています。すでに述べたように、コールバック・オブジェクトは、通常、拡張機能の独自の子供レベルまたは子孫レベルのXML要素内に記述され、完全修飾クラス名がこの要素の属性値として与えられます。また、コールバック・オブジェクトの初期状態は、普通、対応する要素とその子孫要素によって完全にパラメーター化されます。これらの条件を満たすと、Eclipseが備える標準のコールバック・オブジェクトのインスタンス化、および初期化機能を使って、コールバック・オブジェクトをインスタンス化し、初期化することができます。

たとえば、拡張処理コードが、独自のコールバック・オブジェクト(たとえば、actionSets拡張機能内でactionの役割を担うオブジェクト)に対応する要素への拡張機能メンバーの構文解析された表現を取得していると仮定します。また、この要素が変数IConfigurationElement elementに格納され、コールバック・オブジェクトの完全修飾クラス名を指定する要素の属性名がclassであるとします。このとき、以下に示すIConfigurationElementのメソッドcreateExecutableExtension(String classPropertyName)が、コールバック・オブジェクトをインスタンス化するのに使用されます。

ICallback callback = (ICallback) element.createExecutableExtension("class")

ICallbackは、対応するコールバックの役割に応じて与えられるインターフェイスです。

createExecutableExtensionメソッドは、2つのことを行ないます。

  1. 引数なしのパブリックなコンストラクター(必要です)を使って、必要なクラスのメンバーをインスタンス化する。

    必要なクラスの完全修飾名は、メソッドのclassPropertyName引数で与えられる名前をもつ属性の値として、構成要素内で検索されます。

  2. 標準の方法でこのインスタンスを初期化できるときは、初期化する。

    標準の方法で初期化できるようになるために、拡張機能クラスは、Eclipseで用意されている標準の拡張機能初期化インターフェイス IExecutableExtensionを実装します。

    package org.eclipse.core.runtime;
    public interface IExecutableExtension {
       public void setInitializationData(IConfigurationElement config,
         String classPropertyName, Object data) throws CoreException;
    }
    

    インスタンス化されたコールバック・オブジェクトがIExecutableExtensionのインスタンスであれば、createExecutableExtensionメソッドは、自動的にこの拡張機能オブジェクトのsetInitializationDataを呼び出し、コールバック・オブジェクトの構成要素をパラメーターとして引き渡します。(引数にはクラスの属性名とデータ・パラメーターもありますが、これらの詳細はこの記事の範囲を越えています。さしあたっては、無視してもかまいません(Eclipseのドキュメントで詳しく説明されています))

    もちろん、この初期化メソッドで実際に何を行なうかは、コールバック・オブジェクトの設計者次第です。このメソッドにパラメーターとして与えられるコールバック・オブジェクトの構成要素が、初期化プロセスを起動し、初期化メソッドによってコールバック・オブジェクトの状態に反映されます。

第4節で、この仕組みを使ってコールバック・オブジェクトをインスタンス化し、初期化する完全な実例を示します。

以前にも述べたし、次節でも説明するように、この全体の仕組みは一般に遅延方式で、たとえば独自の作業を行なうためにコールバック・オブジェクトが実際に必要になったときにだけ使用されます。同様に、コールバック・オブジェクトの初期化メソッドも、時間のかかる作業を、オブジェクト上の後続のメソッド呼び出しがこの作業を必要とするか、この作業からメリットを得られるまで、可能な限り遅らせる必要があります。たとえば、コールバック・オブジェクトがリソース・セットへのアクセスを管理する場合、コールバック・オブジェクトへの後続のメソッド呼び出しが管理されたリソースを実際に必要とするときにはじめて、そのリソースをオープンすることは、しばしば可能であり、より望ましいことです。

3.3. 遅延拡張処理

ホスト・プラグインがアクティブになると、拡張機能の熱心な(eager)処理は、すべてのエクステンダー・プラグインと、再帰的にプラグイン階層を下って、より正確にはプラグイン拡張ネットワークをたどって、関連するエクステンダー・プラグインをアクティブにします。特定のプラグインから到達可能なすべてのプラグインのアクティブ化は、それらのクラスのロードをともないます。このようなプラグインの全拡張ポイントの処理は、拡張ポイントに対する全拡張機能のカスタム・コールバック・クラスのロードを意味します。その結果、熱心な拡張処理方式では、プラグインをアクティブにするのにかなりの時間がかかり、システムの起動も遅くなります。

そのため、プラグイン開発者は、エクステンダー独自のコールバック・オブジェクトの作成を、このオブジェクトがアクションを実行するために実際に必要になるまで遅らせることにしました。

3.3.1. 遅延拡張処理における仮想プロキシーの使用

遅延アクティブ化方式を実装する方法の1つに、ホスト・プラグインのアクティブ化処理の中で各コールバック・オブジェクトに対する仮想プロキシー(virtual proxy)の使用があります(参考文献[1]にあるプロキシー・パターン仮想的用法を参照)。このようなプロキシーは、実際のコールバック・オブジェクトの役割を代行し、実際のコールバック・オブジェクトはアクションによって必要になったときにだけインスタンス化されます。たいていの場合、プロキシー・クラス中にコールバック・オブジェクトを必要とする汎用関数を用意することができます。このような関数は、特定のカスタム拡張機能クラスを参照しないでホスト・プラグインに組み込むことができる上、特定のエクステンダー・プラグインをアクティブにする必要もありません。

仮想プロキシーを使ってホスト・プラグインの拡張機能を初期化すると、限られた数の拡張機能クラスだけが、すなわち各拡張ポイントの役割に対して1つのクラスだけが、ホスト・プラグインのアクティブ時にロードされます。

例として、内部パッケージorg.eclipse.ui.internalには、UIアクションに対するPluginActionと呼ばれる抽象仮想プロキシー・クラスと多くの具体的なサブクラスが含まれています。actionSets拡張ポイントへの最初の拡張処理では、各UIアクションに対してこのようなプロキシー・クラスだけがインスタンス化されます。

アクション・プロキシーに対する一般的なコンストラクターは抽象クラスPluginActionで定義され、次に示すシグネチャーをもっています。

    public PluginAction(IConfigurationElement actionElement,
      String runAttribute, String definitionId, int style)

actionElementパラメーターは、アクションを表わす拡張機能XML要素の構文解析された表現への参照を提供し、runAttributeパラメーターは、アクションを実行するためにインスタンス化されるカスタム・アクション・クラスの名前を表わすXML属性を特定します。PluginActionプロキシー・クラスは、後でこれらのプロパティを実際のアクション・オブジェクトをインスタンス化し、初期化するのに使用するために、単に記憶しておくだけです。

プロキシー・クラスは、IActionと呼ばれるworkbenchのアクション・インターフェイスを実装することによって、アクション呼び出しを処理します。このインターフェイスのrunメソッドがはじめて呼び出されると、プロキシーはrunAttributeで与えられた名前をもつカスタム・アクションの実装クラスをインスタンス化し、runメソッド呼び出しがこのカスタム・アクションのインスタンスにディスパッチされます。カスタム・アクションのインスタンスは、アクション・デリゲート(action delegate)として知られています。

3.3.1.1. コールバック・プロキシー 対 コールバック・アダプター

正規のプロキシー・パターンでは、プロキシー・クラスとデリゲートを実装するクラスの両方が、同じ機能インターフェイスを実装します。しかし、コールバック・オブジェクトの仮想プロキシーの実装では、プロキシー・パターンの厳密な解釈にならう必要はありません。実際、アダプター(adapter)パターンにならった実装は、コールバックの遅延インスタンス化の目的に対しては、より柔軟なメカニズムを提供します。

たとえば、Eclipse UIアクション・ハンドラーPluginActionは、正確には仮想アダプター(virtual adapter)として記述されています。このハンドラーは、高レベルのIActionインターフェイスを実装し、カスタム・アクション・コールバックの実装で要求されるより低レベルのIActionDelegateインターフェイスに適合させます。IActionとIActionDelegateの2つのインターフェイスは無関係であり、ハンドラー・クラスPluginActionは、独自のカスタム・アクション・ハンドラーを参照することなく、いくつかの基本サービスを提供し、要求されたアクションを実際に実行するために、workbenchのIActionインターフェイスのrunメソッドをアクション・コールバックのIActionDelegateインターフェイスのrunメソッドに適合させます。

第2.3.1.3.1節で述べたカスタムworkbenchメニューとボタン・ハンドラーに要求されるIWorkbenchWindowActionDelegateインターフェイスは、アクション・コールバック・インターフェイスIActionDelegateから派生しています。)

Eclipse開発者が、コールバックの遅延インスタンス化を実現するのに、厳密なプロキシー手法を使う必要はありませんが、Eclipseではプロキシーという用語は、カスタム・コールバック・オブジェクトのインスタンス化を遅らせる目的で、このオブジェクトの前段(front)として使用される内部オブジェクトを指すときに、普通に使用されます。

4. 実例: 拡張可能な演算関数サービス

これまで、すでに存在する拡張ポイントだけを扱ってきましたが、拡張処理の仕組みを使って自己の拡張ポイントを作成する準備が整いました。実例として取り上げる拡張ポイントは、これまで扱ってきたactionSets拡張ポイントに大枠の構造が似ています。

  1. カスタム・サービス・セットによってホスト・プラグインの機能を拡大するためのスロットを提供する。
  2. 複数形式を用意する。そのため、各拡張機能はホスト・プラグインに複数のサービスを自由に追加できる。
  3. 各拡張機能は提供するサービスを実現する具体的なコールバック・オブジェクトを用意する必要がある。
  4. ホストは各拡張機能(実際には、各拡張機能のメンバー)に対して、要素をユーザー・インターフェイスに追加する責務を負う。これによって、ユーザーは拡張機能が提供するサービスと相互作用できる。

さらに、実例では、Eclipseが備えている標準のコールバック初期化機能を使って、コールバック・オブジェクトを独自に初期化する機能も提供します。

目的は、できるだけ単純な拡張ポイントを使って、拡張機能のこれらの顕著な特徴を示すことです。もちろん、主な関心は、アーキテクチャー上の問題点、パターンの用法、および拡張処理を示すことにあります。ここでは、ユーザー・インターフェイス設計の問題は扱いません。実際、実例では、ユーザー・インターフェイス機能と拡張処理機能との間で必要になる相互作用のタイプを示すのに足りる程度の、ごく単純なユーザー・インターフェイスを使用するだけです。

4.1. 実例の概要

ホスト・プラグインは、この実例のためにユーザー・インターフェイスを供給し、UIプラグイン(UI plug-in)と呼ばれます。

単一の拡張ポイントがUIプラグインで定義され、非常に単純なタイプのサービスでプラグイン拡張をサポートします。サービスは、1つの引数をとる整数型演算関数へのアクセスを提供します。関数のシグネチャーは、以下に示すコールバック・インターフェイスで定義されます。

package com.bolour.sample.eclipse.service.ui;
public interface IFunction {
    public long compute(long x) throws ArithmeticException;
}

実例では、各拡張機能メンバーは、IFunctionインターフェイスを実装するコールバック・オブジェクトを通して、このような関数の実装を提供します。

標準の拡張処理を使ってコールバック・オブジェクトを初期化する方法を示すために、拡張機能の宣言内でXML属性として与えられる構成パラメーター(計算で使用される定数)を介して関数を構成するようにします。もちろん、パラメーターの加算、乗算などのさまざまなコールバック・クラスで実装される異なる計算のクラスがあります。一般に、各タイプの計算は別々にパラメーター化されます。

単純にするために、計算機能は高々1つの整数パラメーターでパラメーター化されるものとします。たとえば、引数の値を2倍する関数は、定数因子2で乗算されるとみなします。この場合、値2は、対応するコールバック・オブジェクトの拡張機能宣言内で、XML構成パラメーターとして与えられます。

UIプラグインは、サービス関数の引数を入力する入力フィールド、関数の値を表示する結果フィールド、および計算処理で使用される定数パラメーター(もし、あれば)を表示する定数(constant)フィールドを提供します。各関数には、関数を起動するための独自のボタンと、関数についての情報を表示するための独自のラベルも必要です。そのため、各拡張機能メンバーに対して、UIプラグインはユーザー・インターフェイスに対応するボタンとラベルを追加します。ボタンが選択されると、UIプラグインは関連するサービス関数をコールバックします。

図2は、エクステンダー・プラグインの典型的な構成に対するUIプラグインのユーザー・インターフェイスを示しています。

Figure-2

図2. サービスUIプラグインの関数起動画面。ホスト・プラグインの拡張機能は、この画面を通してアクセスされる演算関数を宣言する。

UIプラグイン・クラスはcom.bolour.sample.eclipse.service.uiで、拡張ポイントはfunctionsと呼ばれます。

4.2. 拡張ポイント

functions拡張ポイントは、UIプラグインのマニフェスト・ファイル内で以下のように宣言されます。

    <extension-point id="functions" name="Functions"
      schema="schema/functions.exsd"/>

この拡張ポイントに対するリファレンス・ページが用意されています(拡張機能のスキーマ・ファイルのソースfunctions.exsdは、companionプラグインzipファイルにあります)。リファレンス・ページに記載されているように、functions拡張機能は、以下の属性をもつfunctionメンバーのシーケンスです。

次節で、これらの属性を使った拡張機能の完全な実例を示します。さしあたって、ここでは定数因子として2をともなう乗算関数 compute(x) = constant * x に対する拡張機能メンバー宣言の例を示します。

<function
     name="DOUBLE"
     constant="2"
     class="com.bolour.sample.eclipse.service.multiplication.Multiplication"/>

この場合、Multiplicationクラスは、標準のEclipseコールバック初期化インターフェイスIExecutableExtensionを実装し、与えられた定数因子でそのインスタンスを初期化します。第4.3.2節で、詳しく説明します。もちろん、計算処理の設計者は、定数によるパラメーター化を行なわないことがあります。エコー関数 compute(x) = x (第4.3.1節を参照)は、このようなパラメーター化を行なわない計算処理の実例です。この場合、対応するコールバック・オブジェクトに対して独自の初期化は必要でないため、関連するコールバック・クラスはIExecutableExtensionを実装しません。

図3は、関数UIプラグインとその拡張機能との間の関係を示しています。

Figure-3

図3. functions拡張ポイントとその拡張機能。演算関数セットが、プラグインのfunctions拡張ポイントの拡張によってUIプラグインに追加される。

これで、functions拡張ポイントに対してサンプルの拡張機能を作成するのに必要なすべての情報が揃いました。UIプラグインの実装の詳細に進む前に、この拡張ポイントに対する拡張機能のサンプルを示します。第4.3節では、このような拡張機能について、2つのサンプルを示します。第4.4節で、UIプラグインにおける拡張処理の詳細を示し、再びこのプラグインの実装に戻ります。

4.3. 拡張機能の実例

4.3.1. エコー: シンプルな拡張機能

エコー・サービスは、入力を出力にそのまま返す関数を提供します。そのサービス・クラスは、以下のように定義されています。

package com.bolour.sample.eclipse.service.echo;
import com.bolour.sample.eclipse.service.ui.IFunction;
public class Echo implements IFunction {
    public long compute(long x) {
        return x;
    }
}
     リスト4.1. エコー・コールバック関数クラス 

functions拡張ポイントを拡張することで、ユーザーはエコー・プラグインを利用できるようになります。エコー拡張機能の仕様を、以下に示します。

<extension
       id="functions.echo"
       name="EchoFunction"
       point="com.bolour.sample.eclipse.service.ui.functions">
   <function
        name="ECHO"
        class="com.bolour.sample.eclipse.service.echo.Echo"/>
</extension>
    リスト4.2. エコー拡張機能の仕様

この仕様では、サービス・コールバック・クラスを宣言し、サービス演算の名前にECHOと指定しています。

この単純なケースでは、Echoオブジェクトは独自の状態をもたないため、初期化の必要はありません。そのため、オプションのconstant属性は使用されず、Echoコールバック・クラスは、IExecutableExtensionインターフェイスを実装しません。

4.3.2. 乗算: カスタムな初期化を行なう拡張機能

入力引数を定数値で乗算するサービス関数を提供するとします。必要な乗算因子をインストール時に構成できると、このようなサービスはより有用になります。function要素のconstant属性は、この目的で使用されます。

サンプルの乗算拡張機能を、以下に示します。

<extension
    id="functions.multiplication"
    name="MultiplicationFunctions"
    point="com.bolour.sample.eclipse.service.ui.functions">
  <function
     name="DOUBLE"
     constant="2"
     class="com.bolour.sample.eclipse.service.multiplication.Multiplication"/>
  <function
     name="TRIPLE"
     constant="3"
     class="com.bolour.sample.eclipse.service.multiplication.Multiplication"/>
</extension>
    リスト4.3. 乗算拡張機能の仕様

この拡張機能が処理されると、constant属性は乗算因子として解釈されます。

乗算コールバック・クラスを、以下に示します。(簡潔にするために、最小限必要なコードだけに編集しています)

package com.bolour.sample.eclipse.service.multiplication;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExecutableExtension;
import com.bolour.sample.eclipse.service.ui.IFunction;

public class Multiplication implements IFunction, IExecutableExtension {

    private static final String FACTOR_ATTRIBUTE = "constant";
    private int factor = 0;

 public long compute(long x) {
        return factor * x;
    }
 public void setInitializationData(IConfigurationElement member,
      String classPropertyName, Object data) throws CoreException {

     String s = member.getAttribute(FACTOR_ATTRIBUTE);
        try {
         factor = Integer.parseInt(s);
        }
        catch (NumberFormatException ex) {
            // throw exception ...
        }
    }
}
     リスト4.3. 乗算関数コールバック・クラス

このクラスのcomputeメソッド()は、サービス・インターフェイスIFunctionの実装を提供し、setInitializationDataメソッド()は、標準の初期化インターフェイスIExecutableExtensionの実装を提供します。

次節では、functions拡張ポイントに対する拡張処理が、標準のコールバック・インスタンス化メソッドcreateExecutableExtensionを使用しています。MultiplicationクラスはIExecutableExtensionインターフェイスを実装しているため、createExecutableExtensionメソッドは、パラメーターにそのインスタンスの構文解析されたXML要素を与えて、コールバック・インスタンスのsetInitializationDataメソッドを自動的に呼び出します。初期化メソッドは、指定された乗算因子を要素のconstant属性から取得して、後の計算処理のためにそれを記憶します()。

4.4. 「functions」拡張ポイントの処理

いくつかのサンプル・テスト・ケースを使ってfunctions拡張ポイントを試し、この拡張ポイントの実装とUIプラグインの定義についてより詳しく調べる準備が整いました。この例では、拡張処理クラスはProcessServiceMembersと呼ばれます。このクラスは、Eclipseの拡張処理で用いられる標準的な慣用句を取り入れ、functions拡張ポイント用に特殊化しています。特にこのクラスでは、functions拡張ポイントに対して標準的な遅延手法を使って拡張処理を実装しています。FunctionsGridと呼ばれる独立したUIクラスは、UIプラグインに対するすべてのUI処理をカプセル化します。

一般に遅延拡張処理では、アプリケーション操作の2つの異なるフェーズでの処理が必要になります。

ホスト・プラグイン起動フェーズでは、全拡張機能メンバーの構成を取得し、汎用的なコールバック・プロキシーを作成して、ホスト・プラグインのユーザー・インターフェイスを構築するために、functions拡張ポイントの全拡張機能メンバーをはじめて走査します。このフェーズでは、実例の拡張処理コードは、各拡張機能関数に対する関数起動要素で、ホスト・プラグインのユーザー・インターフェイスを拡張するために、addFunctionと呼ばれるUIメソッドを繰り返し呼び出します(リスト4.4 を参照)。

アプリケーション相互作用フェーズでは、各構成関数の計算処理を実行するのに必要な独自のコールバック・オブジェクトが遅延手法によって作成され、呼び出しはプロキシー・オブジェクトで受信されます。このフェーズの間、関数に対するUIボタンの選択は、拡張処理クラスが提供するプロキシー・コールバック・オブジェクトを通して、その関数にコールバックされます。呼び出しは、プロキシーによって実際のコールバック・オブジェクトに委譲されます。各プロキシーが最初に呼び出されたときに、実際のコールバック・オブジェクトが標準の拡張処理を介してインスタンス化され、初期化されます。

第4.4.1節では、拡張処理クラスを概説します。第4.4.2節では、拡張処理と相互作用するUIクラスの部分を概説します。UIの詳細を実装する大量のユーザ・インターフェイス・コードは、拡張処理とは直接関係ないため、この記事では扱いません。

4.4.1. 拡張処理クラス

拡張処理関数の最初のバージョンを、以下に示します。コードは読みやすく編集されています。

package com.bolour.sample.eclipse.service.ui;
public class ProcessServiceMembers {
    private static final String EXTENSION_POINT =
        "com.bolour.sample.eclipse.service.ui.functions";
    private static final String FUNCTION_NAME_ATTRIBUTE = "name";
    private static final String CLASS_ATTRIBUTE = "class";
    private static final String CONSTANT_ATTRIBUTE = "constant";
 public static void process(FunctionsGrid grid)
      throws WorkbenchException {
        IPluginRegistry registry = Platform.getPluginRegistry();
        IExtensionPoint extensionPoint =
          registry.getExtensionPoint(EXTENSION_POINT);
        IConfigurationElement[] members =
          extensionPoint.getConfigurationElements();
        // For each service:
     for (int m = 0; m < members.length; m++) {
            IConfigurationElement member = members[m];
            IExtension extension = member.getDeclaringExtension();
            String pluginLabel =
              extension.getDeclaringPluginDescriptor().getLabel();
            String functionName =
              member.getAttribute(FUNCTION_NAME_ATTRIBUTE);
            String label = pluginLabel + "/" + functionName;
            Integer constant = null;
            String s = member.getAttribute(CONSTANT_ATTRIBUTE);
            if (s != null) {
                try {
                    constant = new Integer(s);
                }
             catch (NumberFormatException ex) {
                    // Invalid function. Inform the user ... and ignore.
                    continue;
                }
            }
         IFunction proxy = new FunctionProxy(member);
         grid.addFunction(proxy, functionName, label, constant);
        }
    }
    // ...
}
     リスト4.4. サービス関数に対する拡張機能メンバー処理

拡張処理ループ()は、第3.1.1節で紹介した拡張ポイントの全拡張機能のメンバーを取得するための簡略化イディオムにしたがっています。このループは、functions拡張ポイントの拡張機能メンバーを走査して、各関数に対してプロキシー・コールバック・オブジェクトを作成します()。そして、UI関数グリッドのaddFunctionメソッドを呼び出して()、このプロキシーと関連するUIウィジェットをUIに追加します。(後のリスト4.6で、addFunctionメソッドの本体を示します)

実例にでてくる不正定数などの特定の構成エラーは、一般的な方法で、すなわち(インスタンス化を遅延しているため)独自のコールバック・オブジェクトに頼ることなく検出されます。このようなエラーは、初期の拡張処理の間に処理されるものです。そのため、ホスト・プラグインでは、不正なコールバック・オブジェクトや関連するウィジェットになるべく直面しないようになっています。したがって、functions拡張ポイントの初期の拡張処理で整数でない定数に出会うと、対応する誤った構成の関数は無視されます()。

アプリケーション相互作用フェーズの間に、関数呼び出しがコールバック・プロキシーで受信されると、この呼び出しは関数のカスタム実装に委ねられ、実装は呼び出しがプロキシーで最初に受信されたときに遅延手法を使ってインスタンス化されます。以下のコード断片に、詳細が示されています。

package com.bolour.sample.eclipse.service.ui;
public class ProcessServiceMembers {
    // ...
    private static final String CLASS_ATTRIBUTE = "class";
    // ...
    private static class FunctionProxy implements IFunction {
        private IFunction delegate = null;      // The real callback.
        private IConfigurationElement element;  // Function's configuration.
        private boolean invoked = false;        // Called already.
        public FunctionProxy(IConfigurationElement element) {
            this.element = element;
        }
        public final long compute(long x) throws ArithmeticException {
            try {
                getDelegate();
            }
            catch (Exception ex) {
                throw new ArithmeticException("invalid function");
            }
            if (delegate == null) {
                throw new ArithmeticException("invalid function");
            }
            return delegate.compute(x);
        }
        private final IFunction getDelegate() throws Exception {
            if (invoked) {
                return delegate;
            }
            invoked = true;
            try {
                Object callback =
                  element.createExecutableExtension(CLASS_ATTRIBUTE);
             if (!(callback instanceof IFunction)) {
                    // throw exception ...
                }
                delegate = (IFunction)callback;
            }
         catch (CoreException ex) {
                // process and rethrow ...
            }
            return delegate;
        }
    }
}
    リスト4.5. サービス関数に対する拡張機能メンバー処理

このコードは仮想プロキシー・パターンをそのまま手を加えずに使用しているので、これ以上注釈しません。ただし、エラー処理については次節で解説します。

4.4.1.1. エラー処理

前節で述べたように、特定の構成エラーは、初期の拡張処理の間に一般的な方法で検出することができ、その時点で処理されました。残念ながら、すべての構成エラーがこのように処理できるわけではありません。なぜなら、遅延手法を使ってコールバック・オブジェクトの初期化を遅らせることは、コールバック・オブジェクトのインスタンス化と初期化に関係するエラーの検出も遅れるということになるからです。アプリケーション相互作用フェーズの間に検出される構成エラーを、相互作用時(interaction time)の構成エラーと呼ぶことにします。

相互作用時の構成エラーは、2つのカテゴリーに分類されます。インターフェイスの不一致は、コールバック・クラスが必要なインターフェイスを実装していないときや、作成または初期化エラー、たとえば、存在しないクラスや不正な構成パラメーター(たとえば、除算関数に対する0の分母)などで見られます。リスト4.5では、インターフェイスの不一致はで検出され、作成または初期化エラーはで検出されます。コールバック・オブジェクトの初期化におけるエラーも含め、createExecutableExtensionの実行中に検出されたエラーは、CoreException例外を介して呼び出し側に通知されます。

実例では、相互作用時の構成エラーが発生すると、ArithmeticException例外が投入されます。UIの例外処理では、関数起動画面の結果フィールドに単にエラーを表示しているだけです。実例のような単純なアプリケーションでは、このように誤って構成された関数に関連するUI要素を無効にはしていません。そのため、構成エラーが検出されても、ユーザーは同じ失敗を繰り返すことになります。より洗練されたアプリケーションでは、拡張機能の誤って構成されたメンバーに関連するユーザー・インターフェイス要素を動的に無効にしたり、削除することがあります。

companionプラグインzipファイルには、構成エラーを例示するサンプル・プラグインerrortestが含まれています。デフォルトでは、このようなエラーを含むfunctions拡張機能は、コメントアウトされています。このアプリケーションで、構成エラーの効果を試すには、errortestプラグインのマニフェスト・ファイル内にあるfunctions.error拡張機能のコメントを取り除いてから、Eclipseを再起動してください。

遅延拡張処理による構成エラーの遅い検出は、構成エラーを適切に処理することをより困難にします。一般にアプリケーション起動時のパフォーマンス、適切なエラー処理、および特定の役割を担うコールバック・オブジェクトのカスタマイズ性の間には、矛盾する関係が存在します。

4.4.2. ユーザー・インターフェイス・クラス

本節では、拡張処理クラスと相互作用する範囲内で、UIプラグインのユーザ―・インターフェイス・クラスを概説します。

functions拡張ポイントの拡張機能に含まれる各関数の仕様に対して、初期の拡張処理では、関数起動ボタンと対応するラベルがUIプラグインのユーザー・インターフェイスに追加されます。この目的で使用されるUIコードの簡略バージョンを、次に示します。

package com.bolour.sample.eclipse.service.ui;

public class FunctionsGrid {
        private Composite buttons;      // Function invocation area.
    // ...
 public void addFunction(IFunction function, String functionName,
      String label, Integer constant) {
        GridRow row = new GridRow(function, functionName, label, constant);
    }
    // ...
    private class GridRow {
        private IFunction function;     // Callback object.
        private Button button;          // UI widget.
        private Label functionLabel;    // UI widget.
        // Constant of the computation (for display only).
        private String constantDisplay;

        public GridRow(IFunction function, String functionName,
          String label, Integer constant) {
            // ...
         this.function = function;
            this.constantDisplay =
              constant == null ?  "none" : constant.toString();
            button = new Button(buttons, SWT.NONE);
            button.setText(functionName);
            // ...
            button.addSelectionListener(new SelectionListener() {
                public void widgetSelected(SelectionEvent e) {
                    handleButton(e);
                }
                // ...
            });
            functionLabel = new Label(buttons, SWT.NONE);
            functionLabel.setText(label);
            // ...
        }
     public void handleButton(SelectionEvent e) {
            String t = input.getText();
            parameter.setText("");
            try {
                int x = Integer.parseInt(t);
             result.setText(String.valueOf(function.compute(x)));
                parameter.setText(constantDisplay);
            }
            catch (Exception ex) {
                result.setText("error");
            }
        }
    }
}
    リスト4.6. ユーザー・インターフェイスへの関数の追加

拡張処理は、必要なボタンとラベルを含むグリッド行(grid row)を作成するために、UIのaddFunctionメソッド()を呼び出します。このメソッドは、4つの引数をとります。

addFunctionメソッドの各起動は、同じパラメーターをもつGridRowのコンストラクターに委譲されます。GridRowクラスは、ユーザー・インターフェイス上の関数表現をカプセル化します。各関数に対して、GridRowコンストラクターは、今後の呼び出しのためにコールバック・オブジェクトへの参照を保存します()。また、ボタン・イベント・ハンドラーhandleButton()とボタンを関連付けます。ユーザーが後にボタンを選択すると、イベント・ハンドラーは、UIの入力フィールドから取得したユーザー入力を入力として、ボタンに関連するサービス関数を起動し、関数が返した値を表示します()。また、計算処理で使用された定数パラメーター(もしあれば)も表示します。

これで、演算関数起動処理についての説明を終了します。この記事のcompanionサンプルには、関数起動UIプラグインとサンプルのエクステンダー・プラグインが含まれています。companionプラグインを編成するには、記事の最後にある指示を参照してください。

5. リスナー拡張とオブザーバー・パターン

前節では、サービス拡張パターン(service extension pattern)とも言うべき単純な実例を示しました。サービス拡張機能(service extension)の各メンバーは、固有のユーザー・インターフェイス・ウィジェット・セットと、これらのウィジェット上で固有の拡張機能オブジェクトにコールバックを引き起こすイベントを定義しています。

もちろん、拡張機能の実装に他のパターンを適用することもできます。本節では、参考文献[1]のオブザーバー・デザイン・パターンに類似した、このようなパターンの1つを簡単に説明します。このパターンの適用を通して、Eclipse拡張モデルと非常に単純なオブザーバー・パターンとを比較します。

Java言語APIでは、オブザーバーはリスナーと呼ばれ、本節で論じる拡張パターンはリスナー拡張パターン(listener extension pattern)と呼ばれるものです。 リスナー拡張パターンでは、複数の拡張機能メンバーが、ホスト・プラグインで発生する同じイベントを受信することがあります。イベントが発生すると、ホスト・プラグインはすべての拡張機能メンバー、正確には拡張機能メンバーに関連するリスナー・コールバックに通知します。

Eclipse のJDT JUnitプラグインorg.eclipse.jdt.junitは、このようなパターンを使用しているため、Eclipse workbench内の複数のオブザーバーが、JUnitテストの開始や終了などのテスト・イベント通知を取得できます。BeckとGammaの草稿[2]で、この用法を取り上げています。この草稿で、はじめてリスナー拡張パターンを知りました。

リスナー拡張パターンでは、ホスト・プラグインはオブザーベーションのサブジェクト(subject)として動作し、エクステンダー・プラグインはオブザーバー(observer)またはリスナー(listener)として動作します。そのため、ホスト・プラグインは、listenersと呼ばれる拡張ポイントとIListenerと呼ばれる対応するインターフェイスを提供します。各エクステンダー・プラグインは、IListenerインターフェイスを実装する独自のリスナー、またはリスナーのシーケンスを供給することによって、listeners拡張ポイントを拡張します。

リスナーは、プラグイン拡張メカニズムを通して宣言的に指定され、拡張処理によってイベント通知に自動的に登録されます。最初の通知が必要になると、サブジェクト・プラグインはlistenersメンバーを処理し、各メンバーに対して、独自のリスナー・コールバック・オブジェクトをインスタンス化して、イベント通知にそのリスナーを登録します。

companionサンプル・プラグインには、リスナー拡張パターンの実例が含まれています。図4に、その構造を示します。

Figure-4

図4. リスナー拡張パターンの拡張構造。各リスナー拡張機能のメンバーは、サブジェクトのイベントに対して通知コールバックを提供する。

この例では、サブジェクトが更新されると、listeners拡張ポイントに対する全拡張機能の全メンバーに通知がブロードキャストされます。

listeners拡張ポイント・リファレンス・ページを参照してください。

ボックスの外側に記しているように、この例には、listeners拡張ポイントを拡張する2つのプラグインが含まれています。それぞれ、firstlistener、secondlistenerと呼ばれ、それぞれが2つのlistenerメンバー、ListenerXとListenerYを含んだ拡張機能をもっています。したがって、1つ目のリスナー・プラグインの拡張機能仕様は、以下のようになります。

<extension
    point = "com.bolour.sample.eclipse.listener.subject.listeners">
    <listener
      class="com.bolour.sample.eclipse.listener.firstlistener.ListenerX"/>
    <listener
      class="com.bolour.smaple.eclipse.listener.firstlistener.ListenerY"/>
</extension>
     リスト5.1. リスナー拡張機能の仕様。

ホスト・プラグインが定義しているメニュー項目を選択すると、サブジェクトの状態が更新されます。サブジェクトの状態変化によって、リスナー通知がシステムに登録されている各リスナーにブロードキャストされます。

この例におけるリスナー・コールバックは、取るに足らないささいなものであり、以下のリスナー・クラスで示すように、単に標準出力に情報メッセージを出力するだけです。

package com.bolour.sample.eclipse.listener.firstlistener;
import com.bolour.sample.eclipse.listener.subject.IListener;
public class ListenerX implements IListener {
    public void listen() {
        System.out.println(this.getClass().getName() + " notified");
    }
}
     リスト5.2. リスナー・コールバックの実装。

コンソール・ボックスからこの例を実行すると、以下のコンソール・メッセージが表示されます。

com.bolour.sample.eclipse.listener.secondlistener.ListenerX notified
com.bolour.sample.eclipse.listener.secondlistener.ListenerY notified
com.bolour.sample.eclipse.listener.firstlistener.ListenerX notified
com.bolour.sample.eclipse.listener.firstlistener.ListenerY notified

    5.1. Eclipse拡張モデル 対 オブザーバー・パターン

    今まで見てきたように、リスナー拡張パターンは、インストール時にリスナーが静的に登録されることを除いてオブザーバー・パターンに似ています。オブザーバー登録における動的な性質を無視すれば、オブザーバー・パターンは、Eclipse拡張モデルの特殊化と考えられます。実際、動的登録を差し引いても、Eclipse拡張モデルは多くのレベルでオブザーバー・パターンを強化しています。

    1. コールバック・バンドル 対 単一のコールバック。 単一の拡張機能が、複数のコールバック・オブジェクトを提供できます。単一のオブザーバーは、1つのコールバック・オブジェクトしか提供できません。

    2. エクステンダーの多様性 対 オブザーバーの一様な扱い。 拡張機能パラメーターと拡張ポイント契約の仕様にしたがって、特定の拡張ポイントに対する異なる拡張機能は別々に扱われます。これに反して、オブザーバー・パターンは特定のサブジェクトのすべてのオブザーバーを一様に扱い、1つのオブザーブ・イベントをすべてのオブザーバーに通知します。

    3. ホストにおける任意のセマンティック 対 サブジェクトにおける固定の通知セマンティック。 ホストのセマンティックは、拡張機能のインスタンスと結びついて、任意にパラメーター化されます。拡張機能の構成に基づいて、ホストは拡張機能の契約下で、たとえばユーザー・インターフェイス要素の表示などのさまざまなカスタマイズ可能な責務を果たします。オブザーバー・デザイン・パターンには、このようなパラメーター化やカスタマイズ性はありません。

    Eclipse拡張モデルと、OSのデバイス・ドライバーなどの、いわゆる内部サーバー(internal server)によるマイクロカーネル(microkernel)拡張モデル(実例は、参考文献[4]のマイクロカーネル・パターンを参照)との間には、よりいっそうの類似点が見られます。両方のモデルとも、プロバイダーが提供するサービスの追加によって、基本サービス・セットを拡張できます。しかし、Eclipse拡張モデルは、2つの方法でマイクロカーネル・アーキテクチャーの内部拡張モデルを一般化します。第1に、Eclipseプラグインは、関連する拡張機能セットをパッケージ化するメカニズムを提供しています。第2に、Eclipseでは、すべてのプラグインが拡張ポイントを用意して他のプラグインによってそれ自体を拡張できます。これに反して、マイクロカーネル・アーキテクチャーでは、マイクロカーネルのコア(たとえばOSカーネル)だけがシステムで唯一の拡張可能なコンポーネントとして特別な立場にあります。

    6. まとめと結論

    Eclipseプラグイン拡張モデルは、疎結合された(loosely-coupled)コンポーネントに基づいて拡張可能なシステムを設計するためのパワフルで汎用的なパラダイムを提供します。もちろん、このアーキテクチャーの原理に基づいて作成されているのが、Eclipse workbenchです。しかし、基本となる拡張モデルは、workbenchに特有の具体化とはまったく別の抽象的なアーキテクチャー・パターンです。

    この抽象モデルの原理をなす機能は、

    1. インストール時に組み込み可能なコンポーネント。 プラグインは、インストール時にシステムに組み込まれるコンポーネントで、プラグイン・クラスのインスタンスとして稼動中のシステムに実装されます。各プラグインの特性はマニフェスト・ファイル内で宣言的に指定され、プラグインをインスタンス化し、他のプラグインに関連付けるために実行時に解釈されます。

    2. 拡張ポイント。 プラグイン自体を拡張するという独特の方法は、拡張ポイントを使って実現されます。拡張ポイントは、ホストの役割を担うプラグインによって定義され、エクステンダーの役割を担う複数のプラグインによって拡張されます。各拡張ポイントに付随する契約があり、ホスト・プラグインとエクステンダー・プラグインの両方に責務を課します。

    3. パラメーター化されるコールバック・バンドルとしての拡張機能。 拡張ポイント契約は一般に複数のコールバック・インターフェイスを提供し、エクステンダーは、これらのインターフェイスに対してカスタム実装(コールバック・オブジェクト)を提供する必要があります。ホストは、契約で指定された特定の条件下で、独自の拡張機能構成パラメーターに基づいてこれらのコールバック・オブジェクトをコールバックする責務を負っています。

    4. ホストの責務。 拡張ポイント契約下でのホストの責務には、処理要素の追加によってホストのインターフェイスを拡張するなどの、ホストの動作への追加要件が含まれます。

    5. エクステンダーの責務。 エクステンダーは、マニフェスト・ファイル内で拡張機能の特性を宣言的に記述します。拡張ポイント契約はこの記述に対してXMLスキーマを提供し、エクステンダーのマニフェスト・ファイル内の拡張機能仕様は、このスキーマに適合する必要があります。スキーマには、拡張機能コールバック・オブジェクトの具体的なクラスと、これらのオブジェクトを構成するのに必要なパラメーターへのスロットがあります。具体的なクラスはエクステンダーによって用意され、ホストで定義された所定のインターフェイスに適合する必要があります。実行時に、ホストは構成パラメーターに基づいて構成されたコールバック・オブジェクトをインスタンス化します。

    要約すると、Eclipseプラグインは拡張性において柔軟なモデルを提供し、疎結合されたコンポーネントからシステムを構成するための抽象的なアーキテクチャーは、ソフトウェア・システムで利用できるアーキテクチャー・パターンのレパートリーに重要なものとして加えられるということです([4]を参照)。

    サンプル・コードのインストール

    この記事で取り上げたサンプルを実行し、そのソース・コードを見るには、Eclipseをインストールしたフォルダーにcompanionプラグインzipファイルを解凍してください。

    workbenchを通してサンプルと対話するには、以下に示すように、ユーザー・インターフェイス要素を有効にする必要があります

    簡潔にするために、サンプルではメッセージ表示に標準出力標準エラーを使用しています。デフォルトで、win32プラットフォーム上のEclipseはjavaw仮想マシーンを使用しますが、この仮想マシーンは、標準入出力に連結されたコンソールをもっていません。win32プラットフォーム上で、標準入出力に連結されたコンソールとともにEclipseを起動するには、以下に示すように、-vmオプションを使って、Eclipse実行イメージがjavawではなくjavaを使用することが求められます。

    eclipse -vm C:\jdk1.3.1_02\jre\bin\java.exe
    

    サンプルは、Windows 2000上でEclipse 2.1とJDK 1.3.1_02を使って動作確認されています。

    参考文献

    1. Gamma, Erich, Richard Helm, Ralph Johnson, John Vlissides, Design Patterns, Elements of Reusable Object-Oriented Software, Addition-Wesley, 1995. (本位田 真一、吉田 和樹(訳)、オブジェクト指向における再利用のためのデザインパターン、ソフトバンクパブリッシング、1995)
    2. Beck, Kent, and Erich Gamma, Contributing to Eclipse (pre-publication draft) 2003. (Contributing to Eclipse: Principles, Patterns, and Plug-Ins (Eclipse Series), Addison-Wesley, 2003)
    3. Shavor, S., Jim D'Anjou, et. al. The Java Developer's Guide to Eclipse, Addison-Wesley, 2003. (吉舗 紀子、下宮 弘子(訳)、Java開発者のためのEclipseエキスパートガイド、コンピュータ・エージ社、2004)
    4. Buschmann, Frank, et. al., Pattern-Oriented Software Architecture, A System of Patterns, Volume 1, John Wiley and Sons, 1996. (金沢 典子、桜井 麻里、他(訳)、ソフトウェアアーキテクチャー − ソフトウェア開発のためのパターン体系、近代科学社、2000)
    5. Estberg, Don. How the Minimum Set of Platform Plugins Are Related, Wiki Page 2587, Eclipse Wiki.

    謝辞
    この記事は、Eclipseプラグインに関するSilicon Valley Patterns Group内のディスカッションから生まれました。グループのメンバー、特にTracy Bialik、Phil Goodwin、Jan Looney、Jerry Louis、Chris Lopez、Russ Rufer、Rich Smith、そしてCarol Thistlethwaiteに感謝します。彼らとの創造力に富んだ会話が、この記事を執筆するきっかけになりました。また、非常に多くの提言が題材の内容と表現をよりよいものにしてくれました。グループのメンバーへのEclipseの先駆者として、特にRuss RuferとTracy Bialikに感謝します。Eclipseプラグインの概念と機能を紹介してくれたKent BeckとErich Gammaに負うところは多く、彼らの初期の草稿 Contributing to Eclipse のレビューを中心にグループ・ディスカッションは編成されました。Don Estbergは、この記事で使用したパワー・ストリップ・メタファーに基づく拡張ポイントのダイアグラム表現を考案しました。Donには詳細なコメントをしてくれたことにも感謝します。最後に、この記事の最終原稿のレビューに非常に多くの時間を割いてくれたDennis AllardとDan Condeに、そしてこの記事の質を高めるために詳細レビューと非常に多くの提言をしてくれたJim des Rivieresに感謝します。