目次


インテリジェント・データによりSwingをシンプルに保つ

iDataテクニックを使うと複雑なGUI開発がもっと簡単になる

Comments

高度なSwingアーキテクチャーにより、開発者は以前にも増して複雑な表示画面を設計できるようになりました。そのような表示では、しばしば長大なロジックが必要となり、それによってバグが発生する可能性は高くなり、メインテナンスも難しくなります。JTableやJTreeなどの高度なSwingコンポーネントの場合、プログラム・ロジックにおいて、セル・ベースのデータ・ストレージ、編集、および表示の機能を使用し、よりグローバルな知識が求められるときに、しばしば問題が発生します。コンポーネント・モデルには、高度なアプリケーションの開発に必要な知識を提供するセル・データのような、インテリジェント・データ、つまり高度な知識を含むデータを組み込むことができます。この記事で説明するiDataテクニックは、モデル・ビュー・コントローラー・アーキテクチャーを保ちながら、インテリジェント・データとSwingコンポーネントを統合するための汎用アーキテクチャーを確立します。これは、データ・ストレージ、データ検索インディレクション、および表示設定インディレクションのための、インテリジェント・データを使用して密接に統合されたインディレクション(間接的結合)・スキームによって実現されます。その結果得られるインディレクション・オブジェクトは、複雑さを最低限に抑えながら、複雑なビジネス表示ロジックや対話機能を実現するための柔軟で拡張可能な中心的場所となります。

開発者がiDataアーキテクチャーをそれぞれのプロジェクトに組み込む際には、オープン・ソースiDataツールキットを使用できます。このツールキットには、インディレクション・レイヤーを定義する一連のインターフェース、そしてデフォルトのインプリメンテーション、最適化、カスタム・エディターおよびレンダラー、および豊富なサンプルが含まれています。「参考文献」セクションには、このツールキットへのリンクがあります。

iDataテクニックの3つのレイヤー

iDataテクニックは、次の3つのレイヤーで構成されています。

  • データ・ストレージ: iDataテクニックでは、アプリケーションがDataObject の中にデータを保存することが想定されています。大ざっぱに言ってDataObject は、いくつかのフィールドと、それに対応するget[FieldName]() およびset[FieldName]() メソッドを含む、JavaBeanに準拠したオブジェクト、と定義されます。
  • 表示コンポーネントへのデータ値のインディレクション: データ・インディレクション・レイヤーは、DataObject を含むオブジェクトを定義するインターフェースで構成されます。これは、インテリジェント・データ、またはiDataレイヤーと呼ばれます。(iDataレイヤーを、アーキテクチャー全体の名前であるiDataテクニックと混同しないようにしてください。)iDataレイヤーのインターフェースは、DataObject に含まれるフィールドにアクセスし、それを変更するための汎用メソッドを定義します。各具象iDataレイヤー・クラスは、それらの汎用アクセサーおよびmutatorメソッドを、特定の要件に合わせて実装したものです。多くの場合、iDataレイヤーの実装は、単にDataObject の値を取得および設定するだけです。しかし、後の例で示すように、このインディレクションは、編集の検証、仮想データ、およびデータの装飾を含む、複雑なロジックを実装するための中心的な場所となります。iDataレベルは、さらに不変 (読み取り専用) データのための機能と、可変 (読み取り/書き込み) データのための機能に分割されます。そのように区別するのは、編集ロジックを必要としないが複雑で編集不可能なデータのためのインターフェースを単純化するためです。
  • データに基づいて編集およびレンダリング・コンポーネントをカスタマイズするための表示インディレクション: インテリジェント表示 (iDisplay) レイヤーは、iDataレイヤーによく似たインディレクションを使用してインテリジェントな表示を実現します。iDisplayレイヤーは、iDataレイヤー・オブジェクトを編集したりレンダリングしたりするコンポーネントのためのインターフェースを定義します。このiDisplayレイヤー・カスタマイズの例としては、セルの背景色を変更することによってエラー状態を表示したり、iDataレイヤーの実装において、そのデータを編集するために最適なコンポーネントを決定できるようにするための汎用エディターを作成したりする例があります。iDataレイヤーと同じように、iDisplayレイヤーも、不変データのための機能と可変データのための機能に分割されています。

これらの3つのレイヤーを組み合わせることによって、データ自体ではなくコンポーネント・モデルに追加される一連のインディレクション・オブジェクトを密接に統合したものが作成されます。このアーキテクチャーにより、Swingのモデル・ビュー・コントローラー・アーキテクチャーを保ちつつ、セル・ベースの知識が可能になります。データの検索、表示、および編集のためのロジックは、各セル内のインテリジェント・データにカプセル化されます。その結果、複雑なユーザー・インターフェースの表示や対話を実現するための、機能的に柔軟で拡張可能なテクニックを実現できます。

図1. iDataテクニックのアーキテクチャー全体を示すクラス図
図1. iDataテクニックのアーキテクチャー全体を示すクラス図
図1. iDataテクニックのアーキテクチャー全体を示すクラス図

以下のセクションではiDataテクニックのアーキテクチャーをレイヤーごとに見ていくことにします。その際、このテクニックの使用方法を示すために、架空の自転車店のアプリケーションを構成するいくつかの部分を作成します。

DataObject

DataObject は、いくつかのフィールドとそれに対応するget[FieldName]() およびset[FieldName]() メソッドを含む、JavaBeanに準拠したオブジェクト、と定義されます。一般に、ビジネス領域ごとにいくつかのデータ・フィールドが組み合わされて、1つのDataObject となります。この記事の例の自転車店アプリケーションには、Bicycle と呼ばれるDataObject があり、それにはたくさんのフィールド (modelNamemanufacturerpricecost など) と、それに対応するgetメソッドおよびsetメソッドが含まれます。それ以外にも、この自転車店のDataObject には、BicycleComponent (Bicycle とよく似たフィールドを含む)、Purchase DataObject (purchasorNamepricedateOfPurchase などのフィールドを含む) などが考えられます。自転車店アプリケーションのBicycle DataObject の一部は、次のとおりです。

リスト1. サンプルDataObject
public class Bicycle
{
     //フィールド
     double price = ...
     String manufacturer = ...
     ...

     //デフォルト・コンストラクター
     public Bicycle(){}

     // アクセサー
     public Double getPrice()
     {
          // プリミティブを、関連したオブジェクトの型でラップする
          // ことが必要な場合がある ...
          return new Double(this.price);
     }
     public String getManufacturer()
     {
          return this.manufacturer;
     }

     ...

     // mutator
     public void setPrice(Double price)
     {
          this.price = price.doubleValue();
     }
     public void setManufacturer(String manufacturer)
     {
          this.manufacturer = manufacturer;
     }
     ...
}

インディレクション: iDataレイヤー

前述のように、iDataレイヤーは不変データの機能と可変データの機能に分けられます。MutableIData インターフェースはImmutableIData インターフェースを拡張したものなので、まずは不変データの機能から調べることにします。

読み取り専用インテリジェント・データのためのデータ・インディレクション・レイヤー (ImmutableIData)

ImmutableIData インターフェースは、iDataレイヤーの一部であり、不変iDataインディレクションを表します。これは、次の2つのメソッドと1つの推奨されるメソッド・オーバーライドで構成されています。

  • getData() は、DataObject からのデータ値を型付きで戻します。
  • getSource() は、DataObject 自体を戻します。
  • toString() メソッドのオーバーライドは、getData() の結果をstring で表記したものです。

例として、Manufacturer (メーカー) フィールドに関するImmutableIData の実装を見てみましょう。

リスト2. 自転車メーカーのImmutableIData実装
public class BicycleManufacturerIData implements ImmutableIData
{
  // DataObject
  Bicycle bicycle = null;
  public BicycleManufacturerIData(Bicycle bicycle)
  {
    this.bicycle = bicycle;  // DataObjectをキャッシュに入れる
  }
  public Object getSource()
  {
    return this.bicycle; // 単にDataObjectを戻す
  }
  public Object getData()
  {
    // DataObjectのManufacturerフィールドを戻す。
    // これはインディレクション・レイヤーの主要なロジックとなるメソッドである。
  return bicycle.getManufacturer(); }
  public String toString()
  {
    // 描画時のヌル・ポインター例外を避けるため、// Stringメソッドに対する安全策を作成 ...
    Object data = this.getData();
    if (data != null)     return data.toString();
    else
      return "";
  }
}

iDataツールキットには、ImmutableIData を実装した抽象クラスDefaultImmutableIData が含まれています。これは、ObjecttoString() メソッドをオーバーライドし、安全にgetData().toString() を戻します。この記事の例の残りの部分は、iDataレイヤー・インターフェースのデフォルトの実装を拡張したものです。それらのデフォルト実装も、ツールキットに含まれています。

JTableとの統合
引き続き自転車店の例を使用して、JTableにiDataテクニックを組み込むことにします。この表に含まれる列は、manufacturermodelNamemodelIDpricecost、およびinventory です。ImmutableIData 実装の残りの部分は、manufacturer iData に倣ってすでに作成されているとします。

JTableDataModel に実際に追加されるデータは、それぞれがDataObject を包含するImmutableIData の実装です。このDataObject を包含するiDataレイヤー実装の追加という考え方が、前に述べたインディレクション・レイヤーを実現しています。

図2. DataObjectを包含するImmutableIData実装を伴うJTableセル
図2. DataObjectを包含するImmutableIData実装を伴うJTableセル
図2. DataObjectを包含するImmutableIData実装を伴うJTableセル

一度に行全体を作成するヘルパー・メソッドがあると便利です。私は、それをcreateRow() という名前で作成しています。AbstractTableModel のサブクラスを使用しているなら、その行の全体をモデルに追加できます。createRow() メソッドはDataObject をパラメーターとし、特定の表に対する該当するImmutableIData 実装のインスタンスを生成します。

リスト3. createRow() メソッド
protected Vector createRow(Bicycle bicycle)
  {
    Vector vec = new Vector();
      vec.add(new BicycleModelNameImmutableIData(bicycle));
      vec.add(new BicycleModelIDImmutableIData(bicycle));
      vec.add(new BicycleManufacturerImmutableIData(bicycle));
      vec.add(new BicyclePriceAndCostImmutableIData(bicycle));
      vec.add(new BicycleProfitImmutableIData(bicycle));
      vec.add(new BicycleInventoryImmutableIData(bicycle));
    return vec;
  }

さらに、createRow() メソッドは、このモデルの中にどのImmutableIData 実装を含めるかを決定するロジックのための中心的な場所となるものです。クラス管理の点からは、ImmutableIData のいろいろな簡単な実装には、createRow() メソッドの中で直接宣言した無名の内部クラスを使用することも役立ちます。

レンダリング・シーケンス
デフォルトのレンダラーは、toString() メソッドを呼び出すことによって、表示するオブジェクトのストリング表記を作成します。これは、ImmutableIData 実装において、便利なtoString() メソッドを用意することが重要である理由の1つです。レンダリング時に、レンダラーはJTableからImmutableIData 実装を受け取ります。iDataをレンダリングするため、toString() メソッドが呼び出されます。レンダリング・シーケンスの全体は、次のようになります。

  • iData実装に対するtoString()
  • iData実装に対するgetData()
  • DataObject に対するget[FieldName]()
図3. レンダリング・シーケンス
図3. レンダリング・シーケンス
図3. レンダリング・シーケンス
図4. 読み取り専用表
図4. 読み取り専用表
図4. 読み取り専用表

動的整合性維持

DataObject をiDataのデータとして使用すると、iDataインディレクションのための柔軟性が提供されるだけでなく、動的整合性維持を提供するデータ・インディレクションの有用なレイヤーが追加されます。外部で値が更新される表を表示する例を考慮してみましょう。通常なら、更新された値を維持するためのモデル内の位置を推論するために、クライアントに複雑なロジックを実装する必要があります。iDataとDataObject インディレクションを使用する場合には、新しい値が自動的に維持されるため、そのようなロジックはまったく不要です。これは、同じDataObject インスタンスを含む複数のiDataオブジェクトによってモデルにデータを入れることによるものです。DataObject の内部値が変更されても、iDataオブジェクトはすべて同じインスタンスを指しているため、DataObject 自体は変更されません。getメソッドおよびsetメソッドを使用してDataObject を再照会すると、手動による維持処理をしなくても、常に最新の結果が戻されます。更新時にクライアントが実行しなければならないのは、再描画だけです。つまり、更新されたセルをレンダリングしなおすようレンダラーに指示し、それによって新しいデータ値を取り出して表示する、ということだけです。

このインディレクションによる1つの成果は、クライアント・データ・キャッシュを統一できるということです。コンポーネントがiDataインディレクションおよびDataObjects を中央のクライアント・キャッシュから使用するとすれば、あらゆるデータ編集操作がアプリケーション全体を通じて動的に維持されることになります。これによって、商取引のシステムなど、動的データの表示を担当するクライアントは大幅に簡略化されます。

例: 仮想列

仮想列 は、iDataテクニックの柔軟性を示す1つの例です。仮想列とは、モデル内で明示的に指定されたデータではなく、複数のフィールドの組み合わせであるようなデータを内容とする列のことです。price (価格) とcost (コスト) の差額を表示するためのprofit (利益) という列があるとしましょう。そのような列を作成するには、ImmutableIData 実装を作成し、getData()pricecost の差を戻すようにします。

リスト4. profit仮想列
public class BicycleProfitImmutableIData extends DefaultImmutableIData
{
  ...
  public Object getData()
  {
    // DataObjectのpriceフィールドとcostフィールドの差を戻す
    return new Double(bicycle.getPrice() - bicycle.getCost());
  }
}

標準的なモデルを使用してこのような仮想列を作成する場合には、かなりの量のロジックが必要になります。まずは、モデルに正しい値を入れることでしょう。しかし、price またはcost が編集された場合には、アプリケーションを通じてprofit 値を更新するために、複雑なロジックが必要となり、そのためにバグが入り込む可能性が高くなります。iDataテクニックを使用するなら、pricecost が編集されたとしても、更新操作は不要です。整合性維持が動的に処理されるからです。

iDataオブジェクトを組み立て用ブロックとして使う
インディレクション・レイヤーは、一連のiDataオブジェクトとして実装されます。それには大きなメリットがあります。たとえば、2つのデータ値を連結するPriceAndCost は、合成によって実装することもできます。新しいCompositePriceAndCost の表示内でDataObject から直接2つのデータ値を取り出す代わりに、前に作成したBicyclePriceImmutableIData オブジェクトとBicycleCostImmutableIData オブジェクトを使用できます。getData() メソッドは、2つのiDataレイヤー実装から取り出した値を、この場合はスラッシュを区切りとして連結することによって戻りストリングを作成します。それで、getData() メソッドは次のようになります。

リスト5. 合成によるPriceAndCostImmutableIDataの実装
public Object getData()
{
    // 前に作成したiData実装を使用して、price、スラッシュ、costを
    // およびcostを連結する
    return new String( (String)priceIData.getData() + " / " + (String)costIData.getData() );  
}

異なるiData実装を結合する機能により、コードの再利用度や柔軟性が向上します。新しいiData実装を開発する場合は、それ以前に作成されたiData実装をさまざまに組み合わせることから出発できます。これにより、具象クラスが少なくて済み、さらに実行時にiData実装を動的に合成できるという点で柔軟性が高くなります。ツールキットには、合成を意図したシンプルなiDataオブジェクトを実装するためのヘルパー・クラスがいくつか含まれています。その中には、任意のiDataオブジェクトに対して、そのiDataのストリング表記をプレフィックスやサフィックスで修飾するためのプレフィックス・サフィックス・デコレーターをiDataで実装したものが含まれています。

リフレクション・ベースのImmutableIData実装 (UniversalImmutableIData)
iDataアプローチの大きな欠点の1つは、クラスの肥大化です。大規模なアプリケーションの場合、あっという間にiDataクラスの数が手に負えないほど多くなってしまいます。iDataレイヤー実装の多くは、DataObject の中でgetData() 要求をget[FieldName]() メソッドにリダイレクトするという、同じ処理シーケンスの繰り返しです。これは、一般にリフレクションを使用して実装できます。ツールキットには、リフレクション・ベースのImmutableIData 実装のデフォルト実装 (UniversalImmutableIData) が含まれています。UniversalImmutableIData には、初期化パラメーターとしてDataObject とフィールド名を指定します。その内部では、指定されたフィールド名を使用し、getData()toString() のいずれかのメソッドが呼び出された時点で呼び出されるget[FieldName]() メソッドを取り出します。この方法により開発はさらに簡単になり、クラスの肥大化が軽減されます。ただし、リフレクションの使用のためにパフォーマンスは若干低下します。ほとんどのアプリケーションの場合、これは影響が出るほどのパフォーマンス低下ではありませんが、大規模なアプリケーションやリアルタイムのアプリケーションの開発者は、この点に注意する必要があります。

リスト6. UniversalImmutableIDataを使用したcreateRow() メソッド
protected Vector createRow(Bicycle bicycle)
{
  Vector vec = new Vector();
    vec.add(new UniversalImmutableIData(bicycle, "modelName"));
    vec.add(new UniversalImmutableIData(bicycle, "modelID"));
    vec.add(new UniversalImmutableIData(bicycle, "manufacturer"));
    vec.add(new UniversalImmutableIData(bicycle, "priceAndCost"));
    vec.add(new UniversalImmutableIData(bicycle, "inventory"));
  return vec;
}
リスト7. リフレクション・ベースのUniversalImmutableIDataによるサンプル
  protected String field = ...  // フィールド名
  protected Method accessorMethod = ... // アクセサー・メソッド
  protected Object source = ... // DataObject
  ...
  protected void setMethods()
  {
    if (field == null || field.equals(""))
      return;
    // フィールドの最初の文字を大文字にすることにより、
    // getnameではなくgetNameの形式にする。
    String firstChar = field.substring(0,1).toUpperCase();
    // 最初の文字を削除
    String restOfField = field.substring(1);
    // ストリング "get"、大文字にした最初の文字、// そして残りの文字列を連結する
    String fieldAccessor = "get" + firstChar + restOfField;
    // 将来使用するためにメソッド・オブジェクトをキャッシュに入れる
    this.setAccessorMethod(fieldAccessor);
  }

  ...

  protected void setAccessorMethod(String methodName)
  {
    try
    {
      accessorMethod = source.getClass().getMethod(methodName, null);
    }
    catch ( ... )
    {
      ...
    }
  }
  ...

  public Object getData()
  {
    try
    {
      return accessorMethod.invoke(source, null);    }
    catch ( ... )
    {
      ...
    }
  }

編集可能インテリジェント・データのためのデータ・インディレクション・レイヤー (MutableIData)

MutableIData は、ImmutableIDatasetData() メソッドを追加して変更可能にしたものです。setData() メソッドは、新しいデータ値をパラメーターとし、編集操作が成功したかどうかを示すブール値を戻します。多くの場合、DataObject の中でそのフィールドに対してキャッシュに入れられたデータに適合するように、setData() メソッドの中で新しい値をキャストすることが必要です。標準的な実装では、オブジェクトの型を調べ、クラスの型が一致していないならfalseを戻します。

リスト8. 自転車メーカーのMutableIData
  public boolean setData(Object data)
  {
     if (!data instanceof String) return false;
     ((Bicycle)this.getSource()).setManufacturer((String)data);
         return true;
  }

新しいMutableIData オブジェクトがすべて書き込まれたなら、getRow() メソッドを更新して、Immutable インスタンスではなくMutableIData のインスタンスを生成します。私の場合は、あるフィールドを特に不変にしたい場合にのみ、ImmutableIData オブジェクトを作成するようにしています。それ以外の場合には、setData() メソッドが呼び出されることがないとわかった上で、MutableIData を作成し、読み取り専用表の中で単純にそれを使用します。

カスタム・エディター

データを可変にした後、大きな変更が1つあります。データの編集には、カスタム・エディターが必要です。デフォルト・エディターを使用した場合、JTableはObject 型のデータのためのエディターを取り出すので、それは実際にはString エディターになります。編集が停止した場合、エディターはモデルに対して維持されるString 値を戻し、iDataレイヤー実装をString 値で置き換えます。次の図は、iDataインディレクションの完全性を保つために編集操作が従うべきシーケンスを示したものです。

図5. 編集シーケンス
図5. 編集シーケンス
図5. 編集シーケンス

既存のエディターをこのシーケンスに従うように拡張することも可能ですが、あまり実際的ではありません。それはクラスの肥大化を招き、不必要に複雑になってしまいます。状況によってはカスタム・エディターが実際的ですが、ほとんどのエディターは同じシーケンスに従うため、それをカプセル化して別個のクラスにすることができます。iDataツールキットには、そのようなクラスの実装が含まれており、その名前はUniversalTableCellEditor です。

UniversalTableCellEditor では、TableCellEditor の拡張ではなく包含を使用しています。編集時にUniversalTableCellEditor は、iDataレイヤー実装からデータ値を取り去り、その値を使用して、包含されているTableCellEditor を初期化します。編集が停止すると、UniversalTableCellEditorTableCellEditor から値を取り出し、それに従ってiData実装のデータを設定します。最初にエディターが指定されなかった場合、UniversalTableCellEditor は、JTableに含まれるデフォルト・エディターでiData実装のデータ型に適合するものを取り出します。

UniversalTableCellEditor には、上記の編集シーケンスの全体がカプセル化されています。これは、サード・パーティーのものも含む任意のエディターを、iDataロジック実装のために拡張することなく、そのまま使用できるということを意味しています。

私は、各TableColumn のデフォルト・エディターをUniversalTableCellEditor に設定することによって、JTableのエディターを設定することを提案します。iDataツールキットには、いくつかの静的ヘルパー・メソッドを含むユーティリティー・クラスが用意されています。このユーティリティー・クラスに含まれるconfigureTable() メソッドは、TableColumns 内の要素を反復処理し、それぞれの現行エディターを、各列のそれまでのセル・エディターを包含するUniversalTableCellEditor の新しいインスタンスに設定します。

ツールキットには、レンダラーに関連した同様の機能を含むUniversalTableCellRenderer が含まれています。ツールキットには、JTreeおよびJComboBox/JListのためのユニバーサルなエディターとレンダラーの組み合わせも含まれています。

例: 価格 (price) がコスト (cost) よりも大きいことを保証するためのセル内検証
編集でしばしば経験する問題はセル内検証 です。つまり、セル編集が停止する前にデータの妥当性を検証するということです。setData() メソッドは、セル内検証の中心的な場所となります。pricecost のいずれかの値が編集された場合に、前者が後者より小さいならユーザーに通知する、という例を考えてみましょう。それが該当する場合には、ユーザーに次の選択肢を提供することにします。

  • 2つの値をそのまま使用する。
  • price を変更してcost と同じ値にする。
  • 未編集の値を変更して、2つの値の差額が編集前の差額と同じになるようにする。

これは、setData() メソッドの中で比較的簡単に実現できます。ユーザーに対してJOptionPaneが表示され、そこでユーザーはオプションを選択できます。オプションが選択されたなら、該当する値を設定するための計算が実行されます。iDataテクニックにおいては、すべてのデータ値と、このビジネス・ロジックを実装するこの中心的な場所とを知っていることが柔軟性を高めるかぎとなります。

リスト9. セル内検証
String doNotEdit = "編集しない";
String priceEqualsCost = "価格 = コスト";
String keepProfitDifference = "差益を前と同じに保つ";
String keepProfitPercentage = "差益率を前と同じに保つ";
...
public boolean setData(Object data)
{
    double newCost = new Double(data.toString()).doubleValue();
    double oldCost = this.bicycle.getCost();
    double price = bicycle.getPrice();
    ((Bicycle)this.getSource()).setCost(newCost);
    if (price < newCost)
    {
      Object result = JOptionPane.showInputDialog
      (
        null,
        "入力されたコストが、この自転車の価格より高くなっています。"
        + "\n次のうちいずれかを選択してください。",
        "",
        JOptionPane.QUESTION_MESSAGE,
        null,
        new Object[]{doNotEdit, priceEqualsCost, keepProfitDifference, keepProfitPercentage},
        priceEqualsCost
      );
      if (result != null)
      {
        // データの維持
        if (result.equals(priceEqualsCost))
          this.bicycle.setPrice(bicycle.getCost());  // priceとcostの差を保つ
        else if (result.equals(keepProfitDifference))
          this.bicycle.setPrice( newCost + (oldPrice - oldCost) ); // 差益の比率を保つ
        else if (result.equals(keepProfitPercentage))
          this.bicycle.setPrice( newCost * (oldPrice / oldCost) ); }
    }
    return true;
  }

非JTableコンポーネントの使用
この記事では、これまで一貫して同じJTableを使用してきましたが、別のSwingコンポーネントを使用した簡単な例を見てみましょう。自転車の名前を内容とするJListを作成することにします。Bicycle オブジェクトのコレクションの要素ごとに、それらをBicycleModelNameImmutableIData オブジェクトでラップし、JListに追加するという処理を反復実行します。このJListにおいても、JTableの場合に使用したのと同じiDataインスタンスが使用されます。同じようにして、他のコンポーネントでもそれらのインスタンスを使用できます。

リスト10. JListの初期化
protected void initList()
{
    ...
    while ( ... )
    {
       // 自転車をiDataオブジェクトでラップし、それをリスト・モデルに追加する
      Bicycle bike =  ...
      model.addElement(new BicycleModelNameMutableIData(bicycle)); }
    // リスト・レンダラーをiDataツールキットの
    // JList用ユニバーサル・レンダラーでラップする
    list.setCellRenderer(new UniversalListCellRenderer(list.getCellRenderer())); 
}
図6. JListの例
図6. JListの例

インテリジェント表示インディレクション (iDisplay)

iDisplay構造体は、iDataに基づくカスタム表示を作成するためのレイヤーです。たとえば、望ましいManufacturer についてユーザーに通知するために、それを赤色テキストで表示するというインターフェースを考えてみましょう。普通なら、レンダラーによって渡されないデータ値を取り出すために複雑なロジックが必要になり、そのコードの複雑さのために、データ主体のカスタム表示があまり実際的ではなくなってしまいます。iDisplayはiDataと密接に統合されており、それによってシナリオはもっとシンプルになります。さらに、拡張の中心となる場所が作成されます。

読み取り専用インテリジェント・データのための表示インディレクション・レイヤー (ImmutableIDisplay)

ImmutableIDisplay は、表示固有のロジックをカプセル化したものです。これは、3つの主要なレンダラー・タイプ (TableCellRendererTreeCellRenderer、およびListCellRenderer) のそれぞれについてget[Component]CellRendererComponent() メソッドが1つずつ用意されているImmutableIDisplay によって実現されます。ImmutableIDisplayIData は、ImmutableIDisplay を包含することによってImmutableIDisplayImmutableIData を統合したものです。

JTableがUniversalTableCellRenderergetCellRendererComponent() を呼び出し、ImmutableIDisplayIData のオブジェクトを渡すと、UniversalTableCellRenderer はそのgetCellRendererComponent 要求を、ImmutableIDisplayIData に包含されているImmutableIDisplay のうち、それに対応するget[Component]CellRendererComponent に転送します。

ここで、自転車店の別の例を見てみましょう。これは、ユーザーが自転車の価格 (price) としてcost より低い値を入力すると、pricecost のセルの背景色が赤になる、というものです。ImmutableIDisplaygetTableCellRenderer() メソッドはDataObject を取り出し、pricecost より小さいかどうかを調べます。小さい場合には、背景色が赤に設定されます。それ以外の場合、背景色は白に設定されます。特殊ケースに該当しない場合に、背景色を明示的にデフォルト色に設定することは重要です。Swingはレンダリングのためにflyweightパターンを使用し、同じコンポーネントを反復描画します。特殊ケースに対して標準の設定を変更する一方、標準的ケースでそれをリセットしない場合には、予測不能の結果になります。

リスト11. 自転車のコストに関連したデータに応じてセルの色を変えるgetTableCellRenderer() メソッド
public TableCellRenderer getTableCellRenderer
(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
{
      // 変更比較のために今までの背景色をキャッシュに入れる
      Color oldColor = renderer.getBackground(); // 変更比較のために今までの背景色をキャッシュに入れる
      Color newColor = null;
      // ObjectがMutableIDataかどうかを調べる
      if (value instanceof MutableIData)
      {
        MutableIData arg = (MutableIData)value;  // キャスト
        Bicycle bike = (Bicycle)arg.getSource();
        if (arg.getData() instanceof Number)  // データ型を調べる
        {
        // DataObjectから価格とコストを取り出す
          double cost = ((Number)arg.getData()).doubleValue();
          double price = bike.getPrice();
          // 比較
          if (price > cost)
            newColor = Color.cyan;
          else
            newColor = Color.red;
        }
      }
      // 色が変更されたかどうかを調べる
      if (!newColor.equals(oldColor))
          this.setBackground(newColor);
}
図7. priceとcostの妥当性を色で示した表
図7. priceとcostの妥当性を色で示した表
図7. priceとcostの妥当性を色で示した表

編集可能インテリジェント・データのための表示インディレクション・レイヤー (MutableIDisplay)

不変/可変の区別は、iDisplayの実装でも同じです。MutableIDisplay はエディターを担当し、ImmutableIDisplay はレンダラーを担当します。ImmutableIDisplayIData の場合と同じように、MutableIData を拡張してMutableIDisplay を含むようにしたMutableIDisplayIData があります。その用法はImmutableIDisplay と同じです。唯一違うのは、それがget[Component]CellRenderer() メソッドではなく、get[Component]CellEditor() メソッドを実装したものであるということだけです。ツールキットには、JTable、JTree、およびJComboBoxのためのカスタム・エディターが含まれています。

get[Component]CellRenderer() メソッドとget[Component]CellEditor() メソッドをiDisplayに送ることは、インディレクションの有用なレイヤーとなります。その主要な結果は、表示の設定と機能をカスタマイズするための中心となる、カプセル化された場所が提供されるということです。iDataにおいてiDisplayのために拡張ではなく包含を使用することにより、クラスの肥大化が抑えられ、柔軟性と拡張性が高くなります。最も重要な点として、表示ロジックを非常に込み入ったものにすることの多いカスタム・エディターやカスタム・レンダラーの必要性がほとんどなくなります。完全なカスタム・エディターやカスタム・レンダラーが必要とされる場合でも、表示のほとんどは、iDisplayによって提供されるインディレクション・レイヤーを使用して実装できます。

欠点

iDataテクニックを実装する場合、それにはいくつかの欠点があることに注意してください。

  • パフォーマンス: ほとんどのアプリケーションにとって、iDataテクニックによるパフォーマンス・オーバーヘッドは問題にならない程度のものです。このテクニックでは、かなりの量のインディレクションを指定しますが、ロジックや処理が大量になるわけではありません。しかし、getData()/setData() メソッドまたはget[Component]CellRenderer()/Editor() メソッドの実装に含まれるロジックが多過ぎる場合には、コンポーネントが描画されるたびに、コンポーネント内のあらゆるセルに対してそれらのメソッドのロジックが呼び出されることになります。それらのメソッドは、可能な限りコンパクトなものにしてください。
  • コードベースに追加されるクラス: iDataテクニックの使用には、かなりの数のクラスが必要になることは明らかです。これは、オブジェクト指向のどんなテクニックについても予期されることであり、それにはそれなりのメリットがあります。実際、アプリケーション特有のビジネス・ロジックの多くは、そのような追加クラスに含まれており、それ以外の方法では実現できないようなレベルのカプセル化が実現されています。もしクラスの数をどうしても一定の最低限度内に保たなければならないのであれば、これは最善とは言えないかもしれません。サイズの制限の厳しいそのようなアプリケーションのためには、いくつかの最適化機能が用意されてはいますが、多くの場合、パフォーマンスの面で犠牲を伴います。コードの複雑さ、クラスの数、およびパフォーマンスのコストに関して決定を下す場合には、アプリケーションの要件を十分に考慮してください。
  • 学習の必要性: これは、最も大きな欠点です。iDataテクニックは、柔軟性と拡張性を主眼として設計されたものです。そのため、ある程度の抽象概念を理解する必要があります。これは、圧倒されるほどではないにしても、最初のうちは少しとまどうかもしれません。このアーキテクチャーは、ある程度調査を進めていけば十分理解できるとは思いますが、それなりの努力が求められます。

結論

コンポーネント・モデルに、iDataインディレクション・レイヤーと組み合わせたインテリジェント・データを入れると、それは高度なUI機能を実装するための中心となる、柔軟で拡張可能な中心的場所になります。さらに、この機能は、比較的シンプルなロジックを完全にカプセル化したクラスの形で実装可能であり、それにより柔軟性と再利用可能性が高くなります。このオープン・ソース・ツールキットには、作成されテストされた多くのコードが含まれているので、iDataテクニックの統合の移行は簡単です。それぞれのアプリケーションで必要なことは、iDataインディレクション・レイヤーを実装して、ここで説明したテクニックをうまく利用することだけです。Swingコンポーネントを大々的にカスタマイズすることはなく、カスタム・モデルを用意したり、標準のSwingの機能に変更を加えたりすることもありません。単に注意深くインディレクションを配置するだけです。その結果、複雑な表示機能やカスタマイズを、わかりやすく柔軟で拡張可能な方法で簡単に実装できるシステムになります。

オープン・ソースに関する注

iDataアプローチをうまく実装するには、ツールキットが不可欠でしょう。ツールキットには、ユーザーによるテストを経たコードや、時間をかけて開発された最適化機能およびヘルパー・クラスが用意されており、それにより統合のための時間はごく少なくてすみます。その設計と実装は、これまで私が携わってきたプロジェクトを反映したものになっています。当然ながら、プロジェクトの実装を容易にするために、このツールキットに手を加えたほうがよいという場合があるかもしれません。いろいろな開発者が設計をさらに洗練し、使いやすさと信頼性を増し加えていくなら、このツールキットの有用性はさらに高くなることでしょう。

このツールキットは、Artistic LicenseのもとでSourceForge.netにより配布されています。プロジェクトのホーム・ページ (参考文献を参照) には、ソース、ドキュメンテーション、およびバイナリーの配布物すべてが含まれており、listservなどの情報へのリンクもあります。将来のリリースに含めるとよいコード拡張がありましたら、遠慮なくご連絡ください。オープン・ソース合意事項を明記すれば、どんなアプリケーションにもフリーで使用できます。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=218903
ArticleTitle=インテリジェント・データによりSwingをシンプルに保つ
publish-date=01012002