「いくらなんでも、ちょっと…」という感じのインジェクション手法ですが、プログラミング構文がスッキリするのがメリットです。ポート間を繋ぐ操作がとても見やすく表現できます。
フィールド・インジェクションに欠点があるのは承知ですが、とにかく単純明解ですからね(実用には、セッター・インジェクションがお勧めです)。
フィールド・インジェクションは、簡単な事例やDI概念の説明には非常に好都合です。特に(僕にとっては)、ポートベース・コンポネントのポートを単純かつ明白に実現できるのがうれしい。しかし、フィールド・インジェクションだけで済ませるのは現実的には難しいでしょう。その問題点を列挙すれば:
- 静的型検査はできるが、注入された値を実行時に検証できない。
- 一度だけしか代入(あるいは取得)されないことを(それが必要なときに)保証する手段がない。
- 注入された値を加工したり、計算した値を保存したいことがある。
- 同一ポートに2つ以上の型(ユニオン型に相当)を持たせたいことがある。
- イベントリスナー登録のように、同一ポートに複数の値をセットしたいことがある。
というわけで、セッター・インジェクションを使う必要性が出てきます。ポートベース・コンポネントでは、メス(プラス)・ポートとオス(マイナス)・ポートは対称/対等ですから、オス・ポートを取得(抽出)するゲッター・メソッドも必要です。
「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
(オス、マイナス)でポートであることを明示・識別しました(たぶん、これがベストの方法)。ゲッター/セッターでは、メソッド名にも情報が含まれるので、方式に選択肢(判断事項)が出てきます。
- アノテーションだけに頼り、名前は任意とする。
- 名前だけに頼り、アノテーションは使わない。
- アノテーションと名前(命名規則)を併用する。
それぞれにメリットがありますが、アノテーション+名前の併用が現実的な気がします。
実行時に明示的なアンワイヤーをするために、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つとして「ポートベース・コンポネント」なんてどうだろう、という提案でした。関連することはまた書くでしょうが。