このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

DI(依存性注入)からどこへ行こうか その2

「DI(依存性注入)からどこへ行こうか その1」から引用:

「いくらなんでも、ちょっと…」という感じのインジェクション手法ですが、プログラミング構文がスッキリするのがメリットです。ポート間を繋ぐ操作がとても見やすく表現できます。

「ポートベース・コンポネント:ケーススタディ」から引用:

フィールド・インジェクションに欠点があるのは承知ですが、とにかく単純明解ですからね(実用には、セッター・インジェクションがお勧めです)。

フィールド・インジェクションは、簡単な事例やDI概念の説明には非常に好都合です。特に(僕にとっては)、ポートベース・コンポネントのポートを単純かつ明白に実現できるのがうれしい。しかし、フィールド・インジェクションだけで済ませるのは現実的には難しいでしょう。その問題点を列挙すれば:

  1. 静的型検査はできるが、注入された値を実行時に検証できない。
  2. 一度だけしか代入(あるいは取得)されないことを(それが必要なときに)保証する手段がない。
  3. 注入された値を加工したり、計算した値を保存したいことがある。
  4. 同一ポートに2つ以上の型(ユニオン型に相当)を持たせたいことがある。
  5. イベントリスナー登録のように、同一ポートに複数の値をセットしたいことがある。

というわけで、セッター・インジェクションを使う必要性が出てきます。ポートベース・コンポネントでは、メス(プラス)・ポートとオス(マイナス)・ポートは対称/対等ですから、オス・ポートを取得(抽出)するゲッター・メソッドも必要です。

「DI(依存性注入)からどこへ行こうか その1」の事例Helloに出現したポートをゲッター/セッターに直すならば:

  • メス・ポート void setMessage(String message);
  • メス・ポート void setPrinter(Printer printer);
  • メス・ポート void setBell(Bell bell);
  • オス・ポート Greeter getGreeter();

以下に、フィールド・インジェクションの問題点をセッター・インジェクションにより対処する例を並べます。

実行時の検証/一度だけ代入の保証

  private Printer printer = null;
  public void setPrinter(Printer printer) throws InjectException {
    // 1回しかセットしない
    if (this.printer != null) throw new InjectException("already set.");
    // nullかどうかのチェック
    if (printer == null) throw new InjectException("cannot accept null");
    // 実際にセット
    this.printer = printer;
  }

エラー情報があまり必要ないなら、例外の代わりに戻り値(成功/失敗)でもいいかもしれません。

  private Printer printer = null;
  public boolean setPrinter(Printer printer) {
    if (this.printer != null || printer == null) {
      return false;
    }
    // 実際にセット
    this.printer = printer;
    return true;
  }

注入された値の加工や計算/同一ポートに2つ以上の型

inputという名前のポートには、InputStream型またはReader型をセットでき、内部ではPushbackReaderを使うという状況を考えましょう。

  private PushbackReader input = null;
  public void setInput(Reader reader) {
    this.input = new PushbackReader(reader);
  }
  public void setInput(InputStream stream) {
    setInput(new InputStreamReader(stream));
  }

同一ポートに複数の値

イベントリスナー登録と同じです。例えばobserverというポートに対して、addObserver(Observer observer), removeObserver(observer) を用意すればいいでしょう。

ワイヤリング

フィールド・インジェクションでは、h.printer = p.printer;のような代入文でワイヤリング(ポートの接続)ができました。セッター/ゲッターを使う場合は、h.setPrinter(p.getPrinter());となります。必要に応じて、例外の捕捉や戻り値チェックもします。

アノテーションとネーミング

フィールドをポートに使う場合は、アノテーション@Require(メス、プラス)、@Provide(オス、マイナス)でポートであることを明示・識別しました(たぶん、これがベストの方法)。ゲッター/セッターでは、メソッド名にも情報が含まれるので、方式に選択肢(判断事項)が出てきます。

  1. アノテーションだけに頼り、名前は任意とする。
  2. 名前だけに頼り、アノテーションは使わない。
  3. アノテーションと名前(命名規則)を併用する。

それぞれにメリットがありますが、アノテーション+名前の併用が現実的な気がします。

実行時に明示的なアンワイヤーをするために、unsetXxx, ungetXxx, removeXxxのようなメソッドも許すとして、アノテーション+名前の併用で、きつめの縛りを課す(自由度を減らす)例を挙げましょう(ポート名がxxx、ポート型をT):

アノテーション 使ってよいメソッド名
@Require setXxx(T), unsetXxx(T), addXxx(T), removeXxx(T)
@Provide getXxx(), ungetXxx()

この面倒さをなんとかしなきゃ

フィールドによりポートを実現する方法は、単純でたいした負担にならない点が素晴らしいのですが、いかんせん貧弱です。セッター/ゲッター方式は柔軟で強力ですが、実際に書いてみると(下を参照)、ポート実現コードが膨らんでしまいます。

フィールド方式とセッター/ゲッター方式のどちらも許可して、ポートの使用回数の制限が付いたりすると、コンポネント利用側のワイヤリング(ポートどおしを接続する)コードも複雑化します。

だんだん、人間の注意力や忍耐力に頼るのは難しくなってきます。で、次なる課題は、「ポートとワイヤリングを実現するコーディングの支援/自動化」となるわけね。

public class Hello implements Greeter {

  /* ポートの実現、退屈な繰り返し */

  // +port message
  private String message  = "Hello"; // デフォルト値
  @Require(Occurence.OPTIONAL)
  public void setMessage(String message) {
    if (message == null) {
      this.message = "";
    } else {
      this.message = message;
    }
  }

  // +port printer
  private Printer printer = null;
  @Require
  public void setPrinter(Printer printer) throws PortSetException {
    if (this.printer != null) throw new PortSetException("already set.");
    if (printer == null) throw new PortSetException("cannot accept null");
    this.printer = printer;
  }

  // +port bell
  private Bell bell = null;
  @Require
  public void setBell(Bell bell) throws PortSetException {
    if (this.bell != null) throw new PortSetException("already set.");
    if (bell == null) throw new PortSetException("cannot accept null");
    this.bell = bell;
  }
  
  // -port greeter
  private boolean greeterUsed = false;
  @Provide
  public Greeter getGreeter() throws PortGetException {
    if (greeterUsed) throw new PortGetException("already used");
    greeterUsed = true;
    return this;
  }
  /* ここまで、、、なげーよ! */

  public Hello() {
  }

  public void greet() {
    printer.println(message);
    bell.ring();
  }
}

支援/自動化がない状況でできるだけ楽したいなら、コレが手抜きの手引き。

「DI(依存性注入)からどこへ行こうか」という見出しの記事は、一応この「その2」で終わりです。DIから出発して向かうべき地点の1つとして「ポートベース・コンポネント」なんてどうだろう、という提案でした。関連することはまた書くでしょうが。