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

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

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

参照用 記事

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

このエントリーは、ウチの工作員ショー君への業務連絡とか、新たな工作員勧誘とかの目的もあるのですけど、ふつうにソフトウェア関連ネタとして読めます。ソフトウェアのコンポネント化とコンポネント群の複合方式の話題です。

随分と長いので、2回(か3回)に分けてポストします。

印刷のときはサイドバーが消えます。
内容:

関連:

DI(依存性注入)

DI(依存性注入)については、雑誌や書籍で随分紹介されているので、そういうのを見てください。Web上の解説記事「Inversion of Control コンテナと Dependency Injection パターン」は、原典といってもいいものでしょう。

[追記]「DI(依存性注入)を白紙から説明してみる」を書きました。[/追記]

とりあえずお約束にしたがって、HelloWorldのDI化をコンストラクタ・インジェクションでやってみる例; まず、Helloが提供するインターフェースとHelloが使用するインターフェース:

public interface Greeter {
  /** あいさつメッセージをどこかに書き出す。*/
  public void greet();
}
public interface Printer {
  /** ストリームに文字列を出力 */
  public void print(String msg);
  /** ストリームに文字列を改行付きで出力 */
  public void println(String msg);
}

先に、Printerの実装:

public class SysoutPrinter implements Printer {
  public void print(String msg) {
    System.out.print(msg);
  }
  public void println(String msg) {
    System.out.println(msg);
  }
}

そして、Greeterの実装であるHello。

public class Hello implements Greeter {
  private Printer printer;

  /* このコンストラクタでPrinterを注入 */
  public Hello(Printer printer) {
    this.printer = printer;
  }

  /* implements Greeter */
  public void greet() {
    printer.println("Hello");
  }
}

Helloを使うときは、こんな感じ。

public class HelloDemo {
  public static void main(String[] args) {
    // 部品の生成と組み立て
    Printer p = new SysoutPrinter();
    Greeter g = new Hello(p); // ここで注入もしている

    // 目的の仕事を実行
    g.greet();
  }
}

こんなことして何がうれしいかって? それは、ファウラー先生とかその他エライ人とかエラクない人とかに聞いてください。

ポートベース・コンポネント

DIはひとつの技法なので、この技法を使って何か面白いことをしましょう。で、題材はポートベース・コンポネント(port-based components) -- ってそれなに? ザッと説明しましょう、僕(檜山)のアレンジがだいぶ入りますが。

コンポネントとはプログラム部品ですが、素子とか装置(デバイス)をイメージしてください。ポート(あるいはピン)は素子/装置を繋ぐときのクチとなる端子です(ソフトウェア的な意味は後でだんだん分かります)。コンポネントは、ポートを通してだけ外部(他のコンポンネント達、実行環境)とやり取りします。

1本のポートは、次の3つの属性で特徴付けられます。

  1. ポートの極性
  2. ポートの名前
  3. ポートの型

極性とはプラスまたはマイナスの符号のことです。電気回路におけるプラス/マイナスとほぼ同じ意味ですが、電流ではなくて、情報/指令/要求などの流れる方向を規定します。

コンポネントは箱として図示し、マイナスのポートをオス端子(形は丸)、プラスのポートをメス端子として描くことにします。



プラス/マイナスの決め方については、さんざん悩んだ結果です(Janus(ヤヌス)の紹介」第4節「極性」を参照)。「逆じゃないの?」とか言われても聞く耳持ちません。

マイクロソフトのボックス・アンド・スプーン図(ロリポップ図)を知っている人は、ほぼ同じ図だと思ってください。ポートを箱のどの場所に取り付けるかは大問題なんですが、今ここでは立ち入らないでエエカゲンな場所に配置することにします。

再びDI:フィールド・インジェクション

さて、ポートベース・コンポネントをDIで実現したいわけですが、インジェクションにはフィールド・インジェクションを使うことにします。フィールド・インジェクションはファウラー先生の原典にはない方法ですが、いたって単純明快。オブジェクトのフィールド(変数)に、外部から値や参照を直接に代入することによりインジェクト(注入)しちゃう、という方法です。

「いくらなんでも、ちょっと…」という感じのインジェクション手法ですが、プログラミング構文がスッキリするのがメリットです。ポート間を繋ぐ操作がとても見やすく表現できます。どのフィールドがインジェクト対象であるかはアノテーションでマークしましょう。次のアノテーションを準備します。

/* インジェクションの必要性 */
public enum Necessity {
  MANDATORY, // 必須(かならずインジェクト)
  OPTIONAL   // 省略可能(デフォルト値あり)
}
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD) 
public @interface Require {
  Necessity value() default Necessity.MANDATORY;
}

詳細はともかくとして、@Requireで修飾されたフィールド(変数)は、メス(符号はプラス)のポートに対応し、外部から値や参照がインジェクト(注入)されます。

public class Hello implements Greeter {
  /* このフィールドにPrinterを注入 */
  @Require
  public Printer printer;

  /* implements Greeter */
  public void greet() {
    printer.println("Hello");
  }
}

次のように使います。

public class HelloDemo {
  public static void main(String[] args) {
    // 部品の生成
    SysoutPrinter p = new SysoutPrinter();
    Hello h = new Hello();

    // 部品の組み立て(注入の実行)
    h.printer = p;

    // 目的の仕事を実行
    h.greet();
  }
}

これで、メス・ポート(符号はプラス)に対応するプログラミング要素が確定しましたが、オス・ポート(符号はマイナス)が存在してません。オス(マイナス)・ポートの実現を考えたいのですが、その前に、ワイヤーの概念を説明します。

ポート(あるいは男と女)を繋ぐワイヤー(あるいは赤い糸)

ワイヤーとは、2つのポートを繋ぐ線で、それに沿ってメソッド呼び出しや変数参照が“流れ”ます(変数を0引数メソッドと同一視して、変数参照もメソッド呼び出しの一種とすのるのは良い考えです)。流れの方向は電気と同じ「プラスからマイナス」。

コンポネント(箱)、ポート(ジャック、クチ、ピン)、ワイヤー(線、ケーブル)について、実体/物体としての比喩的イメージを持つことは大事です。ワイヤーでポートを繋ぐときは、次のように考えます。

ワイヤーは、掃除機の電源コードのように、オス(マイナス)・ポートの根本に巻き込まれて収納されています。オス・ポートをつかんで引っ張り出す(プルアウトする)と、ワイヤーもズルズルと伸びるので、メス・ポートまで引き回して差し込み(プットインし)ます。



この接続作業では「オス → メス」という感じですが、接続が完了後に呼び出しが流れる方向は「メス → オス」の方向です。このことから連想するに、「結婚前に積極的アプローチをするのは男だが、結婚後に命令を発するのは女だ」とでも申しましょうか。もっと卑猥な比喩が好きな人は自分で考えて(妄想して)ください。なんでもいいけど、ワイヤーの方向性をキチンと押さえてくださいね。

要求ポートと提供ポート

メス(プラス)・ポートの実現は、@Require(要求)というアノテーションで識別したフィールドでした。同様に、オス(マイナス)・ポートもアノテートされたフィールドにします。要求と対<つい>になるアノテーションとして@Provide(提供)を導入しましょう。

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD) 
publid @interface Provide {
  // 特になし
}

@Provideを使って、printerという名のオス(マイナス)・ポートを実現した例:

public class SysoutPrinter implements Printer {
  /* このフィールドを経由してPrinterを提供 */
  @Provide 
  public final Printer printer;

  public SysoutPrinter() {
    printer = this; // printerポートに自分をセット
  }

  /* implements Printer */
  public void print(String msg) {
    System.out.print(msg);
  }
  public void println(String msg) {
    System.out.println(msg);
  }
}

なんでこんな面倒なことをするんだ?と思うでしょうが:

  • printerにセットするオブジェクトがいつも自分とは限りません。
  • 1つのオブジェクトが複数のオス・ポートを持つことがあります。
  • メス・ポートと扱い方が同じになれば対称性が生まれます。

pがprinterオス・ポートを持つコンポネントのとき、p.printerがPrinter実装を指し、p.printer.println("Hello")でその機能を利用できます。

次の表でポートに関して整理しておきます。

極性(符号) 形状 役割 アノテーション 接続操作
プラス メス 要求 @Require 差し込み
マイナス オス 提供 @Provide 引っ張り出し

これがポートベースHelloWorld

では、HelloWoldを完全にポートベースで書いてみましょう。使うインターフェースは次のとおり:

  1. Printer -- ストリームへの出力
  2. Bell -- ベルを鳴らす
  3. Greeter -- あいさつをする

ポートを、「名前 セミコロン 型」の形で列挙しておきます。これに符号が付いたものがコンポネント(箱、素子)に取り付けられます。

  1. printer:Printer
  2. bell:Bell
  3. greeter:Greeter
  4. message:String

最後のmessageのように、ポートの型はインターフェースでなくてもかまいません。クラス型や基本(プリミティブ)型でもOK。

以下に変更・追加があったインターフェースとクラスのソースを列挙:

public interface Bell {
  /** ベルを鳴らす */
  public void ring();
}
import java.awt.*;

public class Beeper implements Bell {
  @Provide 
  public final Bell bell;
  
  public Beeper() {
    bell = this;
  }

  /* implements Bell */
  public void ring() {
    Toolkit.getDefaultToolkit().beep();
  }
}
public class Hello implements Greeter {
  @Require(Necessity.OPTIONAL)
  public String message = "Hello";
  @Require
  public Printer printer = null;
  @Require
  public Bell bell = null;

  @Provide
  public final Greeter greeter;

  public Hello() {
    greeter = this;
  }

  /* implements Greeter */
  public void greet() {
    printer.println(message);
    bell.ring();
  }
}

Helloが出しているメス・ポートであるmessageは、文字列リテラル(例えば"こんにちは")で埋めてしまっていいのですが、ここではわざとらしく、オスのmessageポートを持つコンポネント(つっても単なるデータ)を作っておきます。

public class JapaneseMessage {
  @Provide
  public final String message = "こんにちは";
}

これら、“ポートを出したコンポネント”を使うには、「部品を生成して、部品を繋げて組み立て、実際に利用する」手順となります。次のデモ・サンプルと、下の絵を眺めると、この手順がわかるでしょう。特に、「繋げる」ところが「左右が同一名フィールドである代入文」になっていて分かりやすいですね、フィールド・インジェクションのいいところです。

public class HelloDemo {
  public static void main(String[] args) {
    // 部品の生成
    SysoutPrinter p = new SysoutPrinter();
    Beeper b = new Beeper();
    Hello h = new Hello();
    JapaneseMessage m = new JapaneseMessage();

    // 部品の組み立て
    h.printer = p.printer;
    h.bell = b.bell;
    h.message = m.message;

    // 目的の仕事を実行
    h.greeter.greet(); // ベルになんだか時間がかかるかもよ
  }
}



それから

今回紹介した方法の問題点とか背景とか、そういうことを次回述べようと思います。

関連: