「いくらなんでも、ちょっと…」という感じのインジェクション手法ですが、プログラミング構文がスッキリするのがメリットです。ポート間を繋ぐ操作がとても見やすく表現できます。
フィールド・インジェクションに欠点があるのは承知ですが、とにかく単純明解ですからね(実用には、セッター・インジェクションがお勧めです)。
フィールド・インジェクションは、簡単な事例や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つとして「ポートベース・コンポネント」なんてどうだろう、という提案でした。関連することはまた書くでしょうが。