依存性の注入とは?
この章では、すべてのアプリケーションを作成する際に従うべき基本的なプログラミング手法を紹介します。これらは、クリーンで理解しやすく、保守可能なコードを書くために必要な基礎です。
これらのルールを習得し、遵守すれば、Netteはあらゆるステップであなたをサポートします。ルーチンタスクを処理し、最大限の快適さを提供するため、ロジック自体に集中できます。
ここで示す原則は、実際には非常にシンプルです。何も心配する必要はありません。
最初のプログラムを覚えていますか?
どの言語で書いたかはわかりませんが、もしPHPだったら、おそらくこのようになっていたでしょう:
function soucet(float $a, float $b): float
{
return $a + $b;
}
echo soucet(23, 1); // 24 を出力します
いくつかの簡単なコード行ですが、そこには非常に多くの重要な概念が隠されています。変数があること。コードがより小さな単位、例えば関数に分割されること。それらに入力引数を渡し、それらが結果を返すこと。そこには条件とループだけが欠けています。
関数に入力データを渡し、それが結果を返すというのは、数学のような他の分野でも使用される、完全に理解できる概念です。
関数には、その名前、パラメータとその型のリスト、そして最後に返り値の型からなるシグネチャがあります。ユーザーとしては、シグネチャに興味があり、通常、内部実装について何も知る必要はありません。
さて、関数のシグネチャがこのようになっていると想像してみてください:
function soucet(float $x): float
1つのパラメータでの合計?それは奇妙です… では、これはどうでしょう?
function soucet(): float
これは本当に非常に奇妙ですね?関数はどのように使用されるのでしょうか?
echo soucet(); // 何が出力されるでしょうか?
このようなコードを見ると、私たちは混乱するでしょう。初心者だけでなく、熟練したプログラマーでさえ、そのようなコードを理解できません。
そのような関数が内部でどのように見えるか考えていますか?加算される数はどこから取得するのでしょうか?おそらく、何らかの方法でそれらを自分で取得するでしょう、例えばこのように:
function soucet(): float
{
$a = Input::get('a');
$b = Input::get('b');
return $a + $b;
}
関数の本体で、他のグローバル関数や静的メソッドへの隠れた依存関係を発見しました。加算される数が実際にどこから来るのかを知るためには、さらに調査する必要があります。
これはダメ!
私たちが示した設計は、多くの否定的な特徴の本質です:
- 関数のシグネチャは、加算される数を必要としないように見せかけ、私たちを混乱させました
- 他の2つの数を合計するように関数をどうやって強制するのか、まったくわかりません
- 加算される数をどこから取得するかを知るために、コードを見る必要がありました
- 隠れた依存関係を発見しました
- 完全に理解するためには、これらの依存関係も調査する必要があります
そして、入力データを取得することは、加算関数のタスクなのでしょうか?もちろん、そうではありません。その責任は、加算自体だけです。
このようなコードには出会いたくないし、絶対に書きたくありません。修正は簡単です:基本に戻り、単にパラメータを使用します:
function soucet(float $a, float $b): float
{
return $a + $b;
}
ルール1:渡してもらう
最も重要なルールは次のとおりです:関数やクラスが必要とするすべてのデータは、それらに渡されなければなりません。
それらが何らかの方法で自分でアクセスできる隠れた方法を考案する代わりに、単にパラメータを渡してください。隠れたパスを考案するのに必要な時間を節約できます。それは間違いなくあなたのコードを改善しません。
このルールを常にどこでも守れば、隠れた依存関係のないコードへの道を歩んでいます。作者だけでなく、後でそれを読むすべての人にとって理解しやすいコードへ。関数のシグネチャとクラスからすべてが理解でき、実装内の隠れた秘密を探す必要がないコードへ。
この技術は専門的には依存性の注入 (dependency injection) と呼ばれます。そして、それらのデータは依存関係 (dependencies) と呼ばれます。実際には、それは単なるパラメータ渡しであり、それ以上のものではありません。
デザインパターンである依存性の注入と、「依存性注入コンテナ」、つまり全く異なるツールを混同しないでください。コンテナについては後で説明します。
関数からクラスへ
そして、クラスはこれとどのように関連していますか?クラスは単純な関数よりも複雑な全体ですが、ルール1はここでも完全に適用されます。ただし、引数を渡すためのより多くのオプションがあります。例えば、関数の場合と非常によく似ています:
class Matematika
{
public function soucet(float $a, float $b): float
{
return $a + $b;
}
}
$math = new Matematika;
echo $math->soucet(23, 1); // 24
または、他のメソッドやコンストラクタを使用して:
class Soucet
{
public function __construct(
private float $a,
private float $b,
) {
}
public function spocti(): float
{
return $this->a + $this->b;
}
}
$soucet = new Soucet(23, 1);
echo $soucet->spocti(); // 24
両方の例は、依存性の注入と完全に一致しています。
実際の例
現実の世界では、数を合計するためのクラスを書くことはありません。実際の例に移りましょう。
ブログ記事を表すクラス Article
があるとします:
class Article
{
public int $id;
public string $title;
public string $content;
public function save(): void
{
// 記事をデータベースに保存します
}
}
そして、使用法は次のようになります:
$article = new Article;
$article->title = '10 Things You Need to Know About Losing Weight';
$article->content = 'Every year millions of people in ...';
$article->save();
save()
メソッドは記事をデータベーステーブルに保存します。Nette Database
を使用して実装するのは簡単ですが、1つの問題があります:Article
はデータベース接続、つまり Nette\Database\Connection
クラスのオブジェクトをどこから取得するのでしょうか?
多くの選択肢があるようです。静的変数から取得できます。または、データベース接続を提供するクラスから継承することもできます。または、いわゆる シングルトン を使用することもできます。または、Laravelで使用されるいわゆるfacades:
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のfacades、または静的変数は決して存在しません。クリーンで適切に設計されたコードでは、引数が渡されます:
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,
]);
}
}
経験豊富なプログラマーであれば、Article
は save()
メソッドを持つべきではなく、純粋なデータコンポーネントとして表現し、保存は別のリポジトリが担当すべきだと考えるかもしれません。それは理にかなっています。しかし、それでは依存性の注入というトピックや、簡単な例を示すという試みの範囲を大きく超えてしまいます。
例えばデータベースを必要とするクラスを作成する場合、それをどこから取得するかを考え出すのではなく、渡してもらうようにしてください。例えば、コンストラクタや他のメソッドのパラメータとして。依存関係を認めてください。クラスのAPIでそれらを認めてください。理解しやすく予測可能なコードが得られます。
そして、エラーメッセージをログに記録するこのクラスはどうでしょうか:
class Logger
{
public function log(string $message)
{
$file = LOG_DIR . '/log.txt';
file_put_contents($file, $message . "\n", FILE_APPEND);
}
}
ルール1 渡してもらう を守ったと思いますか?
守っていません。
クラスは、重要な情報、つまりログファイルのあるディレクトリを、定数から自分で取得しています。
使用例を見てください:
$logger = new Logger;
$logger->log('温度は23℃です');
$logger->log('温度は10℃です');
実装を知らずに、メッセージがどこに書き込まれるかという質問に答えられますか?機能するためには定数
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℃です');
でも、それは気にしない!
「Article オブジェクトを作成して save() を呼び出すとき、データベースについて考えたくない。設定で設定したデータベースに保存してほしいだけだ。」
「Logger を使用するとき、メッセージが書き込まれるだけで、どこに書き込まれるかは気にしない。グローバル設定が使用されるようにしてほしい。」
これらは正しいコメントです。
例として、ニュースレターを送信し、結果をログに記録するクラスを示します:
class NewsletterDistributor
{
public function distribute(): void
{
$logger = new Logger(/* ... */);
try {
$this->sendEmails();
$logger->log('メールは送信されました');
} catch (Exception $e) {
$logger->log('送信中にエラーが発生しました');
throw $e;
}
}
}
改善された Logger
は、もはや定数 LOG_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
クラスを作成します。したがって、Logger
と DatabaseLogger
の2つのクラスがあり、1つはファイルに書き込み、もう1つはデータベースに書き込みます…
このネーミングに何か奇妙な点はありませんか?Logger
を FileLogger
に名前変更する方が良いのではないでしょうか?間違いなくそうです。
しかし、賢く行います。元の名前の下にインターフェースを作成します:
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:ファクトリに任せる
隠れた依存関係を排除し、すべての依存関係を引数として渡すことで、より構成可能で柔軟なクラスが得られました。したがって、より柔軟なクラスを作成および構成する何か他のものが必要です。それをファクトリと呼びます。
ルールは次のとおりです:クラスに依存関係がある場合、そのインスタンスの作成をファクトリに任せます。
ファクトリは、依存性の注入の世界における new
演算子のより賢い代替品です。
デザインパターンであるファクトリメソッドと混同しないでください。これはファクトリの特定の利用方法を説明するものであり、このトピックとは関係ありません。
ファクトリ
ファクトリは、オブジェクトを作成および構成するメソッドまたはクラスです。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
ファクトリ自体だけです。Article
オブジェクトを操作する他のすべてのコード、たとえば EditController
は、まったく影響を受けません。
今、あなたは私たちが本当に助けになったのか疑問に思って、額を叩いているかもしれません。コードの量が増え、全体が疑わしく複雑に見え始めています。
心配しないでください、すぐにNette
DIコンテナに到達します。そして、それは依存性の注入を使用するアプリケーションの構築を非常に簡素化する多くのトリックを持っています。たとえば、ArticleFactory
クラスの代わりに、単なるインターフェースを書くだけで十分です:
interface ArticleFactory
{
function create(): Article;
}
しかし、それは先走りです、まだ待ってください :-)
まとめ
この章の冒頭で、クリーンなコードを設計する方法を示すことを約束しました。クラスには単純に
一見そうは見えないかもしれませんが、これらの3つのルールには広範囲にわたる影響があります。それらはコード設計に対する根本的に異なる見方につながります。それは価値がありますか?古い習慣を捨て、一貫して依存性の注入を使用し始めたプログラマーは、このステップをプロとしての人生における決定的な瞬間と見なしています。明確で保守可能なアプリケーションの世界が彼らに開かれました。
しかし、コードが一貫して依存性の注入を使用していない場合はどうなりますか?静的メソッドまたはシングルトンに基づいて構築されている場合はどうなりますか?それは何らかの問題を引き起こしますか?非常に重大な問題を引き起こします。