Dependency Injection とは?

この章では、どんなアプリケーションを書くときにも従うべき基本的なプログラミングの作法を紹介します。きれいで分かりやすく、保守しやすいコードを書くために欠かせない土台です。

これらの規則を身につけて守れば、Nette はあらゆる段階であなたを支えます。決まりきった作業を代わりにこなし、最大限の快適さを提供するので、あなたはロジックそのものに集中できます。

ここで示す原則はとても単純です。恐れることは何もありません。

最初に書いたプログラムを覚えていますか

どの言語で書いたかは分かりませんが、もし PHP だったなら、おそらくこんな形だったでしょう。

function addition(float $a, float $b): float
{
	return $a + $b;
}

echo addition(23, 1); // 24 を出力

ほんの数行のありふれたコードですが、そこには多くの重要な考え方が隠れています。変数が存在すること。コードが関数のような小さな単位に分けられること。そこに入力の引数を渡すと、結果が返ってくること。足りないのは条件とループくらいです。

関数にデータを渡すと結果が返ってくるというのは、数学など他の分野でも使われる、まったく理解しやすい考え方です。

関数にはシグネチャがあり、名前、パラメータとその型の並び、そして戻り値の型から成ります。利用者として関心があるのはシグネチャで、内部の実装について知る必要はふつうありません。

では、関数のシグネチャが次のようだったらどうでしょうか。

function addition(float $x): float

パラメータがひとつの足し算? 妙ですね……。ではこれは?

function addition(): float

これは本当に妙ですね。この関数はどう使うのでしょうか。

echo addition(); // 何を出力する?

こんなコードを見たら困惑するでしょう。初心者どころか、熟練したプログラマーでも理解できません。

こうした関数は中身がどうなっているのか気になりますか。足す数はどこから手に入れるのでしょうか。おそらくどうにかして自分で調達しているはずです。たとえば次のように。

function addition(): float
{
	$a = Input::get('a');
	$b = Input::get('b');
	return $a + $b;
}

関数の本体で、ほかのグローバル関数や静的メソッドへの隠れた依存関係が見つかりました。数が実際にどこから来るのかを知るには、さらに調べる必要があります。

こうしてはいけない

いま見せた設計は、多くの悪い性質の元になっています。

  • 関数のシグネチャは、足す数が要らないかのように装っていて、私たちを混乱させた。
  • この関数に別の 2 つの数を足させる方法が分からない。
  • 数がどこから来るのかを知るためにコードを覗く必要があった。
  • 隠れた依存関係が見つかった。
  • 完全に理解するには、その依存関係も調べなければならない。

そもそも入力を手に入れることは、足し算の関数の仕事でしょうか。もちろん違います。その責務は足し算そのものだけです。

こんなコードには出会いたくありませんし、まして書きたくもありません。直し方は簡単です。基本に戻って、素直にパラメータを使うのです。

function addition(float $a, float $b): float
{
	return $a + $b;
}

原則 1: 渡してもらう

最も大切な原則はこれです。関数やクラスが必要とするデータは、すべてそこへ渡さなければならない

データを手に入れる隠れた道を考え出す代わりに、素直にパラメータで渡しましょう。コードを何ひとつ良くしない隠し通路を考える時間を節約できます。

この原則をいつでもどこでも守れば、隠れた依存関係のないコードへの道を歩むことになります。作者だけでなく、あとから読む誰にとっても分かりやすいコードへ。すべてが関数やクラスのシグネチャから読み取れ、実装の中に隠れた詳細を探す必要のないコードへ。

この手法は専門的には Dependency Injection(依存性注入)と呼ばれます。そしてそのデータを依存関係と呼びます。単なるパラメータの受け渡しであって、それ以上のものではありません。

デザインパターンである Dependency Injection と、道具である「Dependency Injection コンテナ」を混同しないでください。両者は根本的に異なるものです。コンテナについてはあとで扱います。

関数からクラスへ

これはクラスにはどう当てはまるのでしょうか。クラスは単純な関数より複雑な存在ですが、原則 1 はここでもそのまま当てはまります。ただ引数の渡し方が増えるだけです。たとえば、関数の場合とよく似た形にできます。

class Math
{
	public function sum(float $a, float $b): float
	{
		return $a + $b;
	}
}

$math = new Math;
echo $math->sum(23, 1); // 24

あるいはほかのメソッドや、コンストラクタを直接使う形でも。

class Sum
{
	public function __construct(
		private float $a,
		private float $b,
	) {
	}

	public function calculate(): float
	{
		return $this->a + $this->b;
	}
}

$sum = new Sum(23, 1);
echo $sum->calculate(); // 24

どちらの例も Dependency Injection に完全に沿っています。

現実の例

現実の世界では、数を足すクラスを書くことはないでしょう。実践的な例に移りましょう。

ブログの記事を表す Article クラスがあるとします。

class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		// 記事をデータベースに保存します
	}
}

使い方は次のようになります。

$article = new Article;
$article->title = 'ダイエットについて知っておくべき 10 のこと';
$article->content = '毎年、何百万人もの人々が ...';
$article->save();

save() メソッドは記事をデータベースのテーブルに保存します。Nette Databaseで実装するのは簡単なはずですが、ひとつ引っかかることがあります。Article はデータベース接続、つまり Nette\Database\Connection クラスのオブジェクトをどこから手に入れるのでしょうか。

選択肢はたくさんありそうです。静的変数から取ってくることもできます。データベース接続を提供するクラスを継承することも。シングルトンを使うことも。あるいは Laravel で使われているような、いわゆるファサードも。

use Illuminate\Support\Facades\DB;

class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		DB::insert(
			'INSERT INTO articles (title, content) VALUES (?, ?)',
			[$this->title, $this->content],
		);
	}
}

素晴らしい、問題が解けました。

本当にそうでしょうか。

原則 1: 渡してもらうを思い出しましょう。クラスが必要とするすべての依存関係は、そこへ渡さなければなりません。この原則を破れば、隠れた依存関係だらけで見通しの悪いコードへの道に踏み出すことになり、その結果は保守と発展が困難なアプリケーションです。

Article クラスの利用者には、save() メソッドが記事をどこに保存するのか分かりません。データベースのテーブル? どのデータベース、本番用かテスト用か? そしてそれはどう変えられるのでしょうか。

利用者は save() メソッドの実装を見て、DB::insert() メソッドが使われていることに気づきます。そこで、このメソッドがデータベース接続をどう手に入れるのかをさらに調べなければなりません。隠れた依存関係はかなり長い連鎖を作り得ます。

きれいでよく設計されたコードには、隠れた依存関係も、Laravel のファサードも、静的変数もありません。きれいでよく設計されたコードでは、引数が渡されます。

class Article
{
	public function save(Nette\Database\Connection $db): void
	{
		$db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

あとで見るように、コンストラクタを使うほうがさらに実用的です。

class Article
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function save(): void
	{
		$this->db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

経験を積んだプログラマーなら、そもそも Articlesave() メソッドがあるべきではなく、純粋にデータ構造を表し、保存は別のリポジトリが担うべきだと考えるかもしれません。それはもっともです。しかしそれは、この記事の主題である Dependency Injection と、単純な例を示すという目的をはるかに超えてしまいます。

たとえばデータベースを必要とするクラスを書くなら、それをどこから手に入れるかを考え出すのではなく、渡してもらってください。コンストラクタやほかのメソッドのパラメータとして渡すのがよいでしょう。依存関係を認めてください。クラスの API でそれを認めてください。分かりやすく予測できるコードが手に入ります。

では、エラーメッセージを記録する次のクラスはどうでしょうか。

class Logger
{
	public function log(string $message): void
	{
		$file = LOG_DIR . '/log.txt';
		file_put_contents($file, $message . "\n", FILE_APPEND);
	}
}

どう思いますか。原則 1: 渡してもらうを守っているでしょうか。

守っていません。

肝心の情報、つまりログファイルのあるディレクトリを、クラス自身が定数から取ってきています。

使い方の例を見てください。

$logger = new Logger;
$logger->log('気温は 23 °C です');
$logger->log('気温は 10 °C です');

実装を知らずに、メッセージがどこに書かれるか答えられるでしょうか。動作に LOG_DIR 定数の存在が必要だと思い当たるでしょうか。そして別の場所に書き込む 2 つめのインスタンスを作れるでしょうか。まず無理です。

クラスを直しましょう。

class Logger
{
	public function __construct(
		private string $file,
	) {
	}

	public function log(string $message): void
	{
		file_put_contents($this->file, $message . "\n", FILE_APPEND);
	}
}

これでクラスはずっと分かりやすく、設定しやすく、そして役に立つものになりました。

$logger = new Logger('/path/to/log.txt');
$logger->log('気温は 15 °C です');

でも、そんなの気にしたくない

「Article オブジェクトを作って save() を呼ぶとき、データベースのことなんて考えたくない。設定したところに保存されてほしいだけだ。」

「Logger を使うとき、メッセージが書かれてほしいだけで、どこに書かれるかは考えたくない。グローバルな設定を使ってくれればいい。」

もっともな言い分です。

例として、ニュースレターを配信して結果を記録するクラスを見てみましょう。

class NewsletterDistributor
{
	public function distribute(): void
	{
		$logger = new Logger(/* ... */);
		try {
			$this->sendEmails();
			$logger->log('メールを送信しました');

		} catch (Exception $e) {
			$logger->log('送信中にエラーが発生しました');
			throw $e;
		}
	}
}

改良された LoggerLOG_DIR 定数を使わなくなり、コンストラクタでファイルのパスを求めます。これはどう解けばよいでしょうか。NewsletterDistributor クラスはメッセージがどこに書かれるかに関心がなく、ただ記録したいだけです。

答えはまたも 原則 1: 渡してもらうです。クラスが必要とするデータをすべて渡します。

では、ログのパスをコンストラクタで渡して、それを Logger オブジェクトを作るときに使う、ということでしょうか。

class NewsletterDistributor
{
	public function __construct(
		private string $file, // ⛔ こうではありません!
	) {
	}

	public function distribute(): void
	{
		$logger = new Logger($this->file);

こうではありません。そのパスは NewsletterDistributor クラスが必要とするデータではなくLogger が必要とするデータだからです。違いが分かりますか。NewsletterDistributor クラスが必要とするのはロガーそのものです。ですからロガーそのものを渡します。

class NewsletterDistributor
{
	public function __construct(
		private Logger $logger, // ✅
	) {
	}

	public function distribute(): void
	{
		try {
			$this->sendEmails();
			$this->logger->log('メールを送信しました');

		} catch (Exception $e) {
			$this->logger->log('送信中にエラーが発生しました');
			throw $e;
		}
	}
}

これで NewsletterDistributor クラスのシグネチャから、ログの記録がその機能の一部だと分かるようになりました。しかもテストなどのためにロガーを別のものに差し替えるのは、まったく簡単です。さらに Logger クラスのコンストラクタが変わっても、私たちのクラスには何の影響もありません。

原則 2: 自分のものだけ受け取る

惑わされず、自分の依存関係の依存関係を受け取らないでください。受け取るのは自分自身の依存関係だけです。

おかげで、ほかのオブジェクトを使うコードは、そのコンストラクタの変更からまったく独立でいられます。API はより正確になります。そして何より、その依存関係を別のものに差し替えるのが簡単になります。

家族に新しい仲間

開発チームは、データベースに書き込む 2 つめのロガーを作ることにしました。そこで DatabaseLogger クラスを作ります。これで LoggerDatabaseLogger の 2 つのクラスができ、一方はファイルに、もう一方はデータベースに書きます……名前づけが少し変ではないでしょうか。LoggerFileLogger に改名したほうがよくないでしょうか。もちろんそうです。

ただし賢くやりましょう。もとの名前でインターフェースを作ります。

interface Logger
{
	function log(string $message): void;
}

……そして両方のロガーがそれを実装します。

class FileLogger implements Logger
// ...

class DatabaseLogger implements Logger
// ...

おかげで、ロガーを使っているコードのほかの部分を変える必要はまったくありません。たとえば NewsletterDistributor クラスのコンストラクタは、引き続き Logger をパラメータとして求めるだけで済みます。どのインスタンスを渡すかは私たち次第です。

だからこそ、インターフェース名に Interface の接尾辞や I の接頭辞を付けないのです。 さもないと、コードをこれほど優雅に拡張できなくなります。

ヒューストン、問題が発生した

ファイル用でもデータベース用でも、アプリケーション全体でロガーのインスタンスはひとつあれば足り、記録が必要なところに渡せばよいのに対し、Article クラスの事情はかなり違います。そのインスタンスは必要に応じて、何度でも作られます。コンストラクタのデータベース依存はどう扱えばよいでしょうか。

例として、フォームの送信後に記事をデータベースに保存すべきコントローラを考えてみましょう。

class EditController extends Controller
{
	public function formSubmitted($data)
	{
		$article = new Article(/* ... */);
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

答えは明らかに見えます。データベースのオブジェクトをコンストラクタ経由で EditController に渡して、$article = new Article($this->db) とすればよさそうです。

しかし Logger とファイルのパスの先ほどの場合と同じく、これは正しいやり方ではありません。データベースは EditController の依存関係ではなく、Article の依存関係です。ですからデータベースを渡すことは原則 2: 自分のものだけ受け取るに反します。Article クラスのコンストラクタが変われば(新しいパラメータが加われば)、インスタンスを作っているすべての場所のコードを直す必要があります。まいりましたね。

ヒューストン、どうすればいい?

原則 3: ファクトリに任せる

隠れた依存関係をなくし、すべての依存関係を引数として渡すことで、より設定しやすく柔軟なクラスが手に入りました。ですから、その柔軟なクラスを作って設定してくれる何かがさらに必要になります。それをファクトリと呼びます。

原則はこうです。クラスが依存関係を持つなら、そのインスタンスの生成をファクトリに任せる。

ファクトリは、Dependency Injection の世界における new 演算子の賢い代替です。

factory method デザインパターンと混同しないでください。あちらはファクトリの特定の使い方を述べたもので、この話題とは関係がありません。

ファクトリ

ファクトリとは、オブジェクトを作って設定するメソッドやクラスです。Article を作るクラスを ArticleFactory と呼ぶことにすると、次のような形になります。

class ArticleFactory
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function create(): Article
	{
		return new Article($this->db);
	}
}

コントローラでの使い方は次のようになります。

class EditController extends Controller
{
	public function __construct(
		private ArticleFactory $articleFactory,
	) {
	}

	public function formSubmitted($data)
	{
		// ファクトリにオブジェクトを作らせます
		$article = $this->articleFactory->create();
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

これで Article クラスのコンストラクタのシグネチャが変わっても、対応が必要なコードは ArticleFactory だけです。EditController など Article オブジェクトを扱うほかのコードは、まったく影響を受けません。

本当に事態は良くなったのか、と首をかしげているかもしれません。コードの量は増え、全体がやけに複雑に見え始めました。

心配は要りません。もうすぐ Nette の DI コンテナに行き着きます。そこには、Dependency Injection を使ったアプリケーションの構築を大きく単純にしてくれる仕掛けがいくつもあります。たとえば ArticleFactory クラスの代わりに、インターフェースを書くだけで済むようになります。

interface ArticleFactory
{
	function create(): Article;
}

とはいえ先を急ぎすぎました。どうぞこのまま :-)

まとめ

この章のはじめに、きれいなコードを設計する道筋を示すと約束しました。クラスについて次のことを守るだけです。

  1. 必要とする依存関係を渡してもらう
  2. 逆に、直接必要としないものは渡されない
  3. そして依存関係を持つオブジェクトはファクトリで作るのが最善

一見そうは見えないかもしれませんが、この 3 つの原則は遠くまで届く結果をもたらします。コードの設計に対するまったく違う見方につながるのです。その価値はあるでしょうか。古い習慣を捨てて Dependency Injection を一貫して使い始めたプログラマーは、この一歩を職業人生の節目と見なします。彼らには、明快で保守しやすいアプリケーションの世界が開けたのです。

では、コードが Dependency Injection を一貫して使っていなかったら? 静的メソッドやシングルトンの上に築かれていたら? それは問題につながるでしょうか。はい、それも非常に大きな問題に

バージョン: 3.x