以下は依存性の注入の基本的な実装パターンです。
依存性の注入にはコンストラクタインジェクションとメソッドインジェクションの2形態が存在します。
コンストラクタインジェクションはコンストラクタで依存性を注入します。
各メソッド間で依存性を共有する場合に適した実装パターンです。
<?php
class Logger
{
public function log($message): void
{
echo "Logging: $message\n";
}
}
<?php
class UserManager
{
private Logger $logger;
public function __construct(Logger $logger)
{
// コンストラクタの引数を通して依存するオブジェクト(Logger)を注入
$this->logger = $logger;
}
public function createUser($username): void
{
// ユーザーの作成ロジック
// ログを記録
$this->logger->log("User created: $username");
}
}
メソッドインジェクションはコンストラクタで依存性を注入します。
特定のメソッドのみで依存性を利用する場合に適した実装パターンです。
<?php
class Logger
{
public function log(string $message): void
{
echo "Logging: $message\n";
}
}
<?php
class UserManager
{
public function createUser(string $username, Logger $logger): void
{
// ユーザーの作成ロジック
// ログを記録
// メソッドの引数を通して依存するオブジェクト(Logger)を注入
$logger->log("User created: $username");
}
}
特性上、コンストラクタインジェクションよりメソッドインジェクションのほうが結合度は低くなります。
ただし、どちらを選択するかは具体的な状況に依存します。
コンストラクタインジェクションは、クラス全体で共有される依存性を設定するのに適しており、クラスのインスタンス化時に一度設定されるため、インスタンスの寿命と同じ依存性が必要な場合に適しています。
メソッドインジェクションは、異なるメソッドが異なる依存性を必要とする場合や、依存性を動的に変更する必要がある場合に適しています。
DIにインターフェイスを活用することで保守性や拡張性が飛躍的に上昇します。
以下は、インターフェイスを活用した実装例です。
DIの対象になっているのは、クラスではなくRandomResultProcessorインターフェイスです。
インターフェイスを型指定している場合は、そのインターフェイスを実装しているクラスを引数に指定することが可能です。
つまり、インターフェイスを実装しているクラスであればどのようなクラスでも引数に指定(DI)することが可能です。
ランダムというテストが非常に困難な機能も、インターフェイスとDIの組み合わせでテスト結果を固定することができテストを容易化できます。
<?php
/**
* インターフェース
*/
interface RandomBooleanGenerator
{
public function getRandomBoolean(): bool;
}
<?php
/**
* インターフェースを実装した実際のクラス
*/
class RandomBooleanGeneratorImpl implements RandomBooleanGenerator
{
public function getRandomBoolean(): bool
{
// ランダムに true または false を返す
return (bool) rand(0, 1);
}
}
<?php
/**
* 必ずtrueを返すクラス
*/
class MockTrueGenerator implements RandomBooleanGenerator
{
public function getRandomBoolean(): bool
{
return true;
}
}
<?php
/**
* 必ずfalseを返すクラス
*/
class MockFalseGenerator implements RandomBooleanGenerator
{
public function getRandomBoolean(): bool
{
return false;
}
}
<?php
/**
* 依存性の注入を使用するクラス
*/
class RandomResultProcessor
{
private $generator;
public function __construct(RandomBooleanGenerator $generator)
{
$this->generator = $generator;
}
public function process()
{
$result = $this->generator->getRandomBoolean();
if ($result) {
echo "True";
} else {
echo "False";
}
}
}
<?php
// インターフェースを実装した実際のクラスではテストが困難
$randomBoolGenerator = RandomBooleanGeneratorImpl()
$processor = new RandomResultProcessor($randomBoolGenerator);
// ランダムで処理を制御しているので、テスト実行毎に処理内容が変化し正確なテストを実施できない。
assertEquals($processor->process(), 'True');
// テスト時にtrueの結果を固定
$fixedTrueGenerator = new MockTrueGenerator();
$processor = new RandomResultProcessor($fixedTrueGenerator);
assertEquals($processor->process(), 'True');
// テスト時にfalseの結果を固定
$fixedFalseGenerator = new MockFalseGenerator();
$processor = new RandomResultProcessor($fixedFalseGenerator);
assertEquals($processor->process(), 'False')
逆制御はコード内の制御フローを逆転させるプログラミングパターンで、通常はDIなどを通じて外部から依存関係を提供し、コンポーネント間の結合度を緩和し、柔軟性を高めるアプローチです。
逆制御により、プログラムの実行フローと制御が外部に委譲され、より柔軟でテスト可能なコードを実現できます。
まずは逆制御を行っていない場合のサンプルコードです。
このコードでは、DBMSの種類に応じて条件分岐が行われ、それによって異なるデータベース接続とタスクの実行が行われます。
このようなアプローチはコードの拡張性を損ない、保守性を低下させます。
例えば、DBMSの種類が増える度に既存のソースコードを改修する必要が発生します。
また、既存コードに手を加えることになるので不具合発生リスクやテスト工数の増加を招きます。
<?php
class DatabaseClient
{
private $dbConnection;
private $dbType; // DBMSの種類を示す変数
public function __construct($dbType)
{
$this->dbType = $dbType;
$this->initializeDbConnection();
}
private function initializeDbConnection(): void
{
if ($this->dbType === 'MySQL') {
$this->dbConnection = new MySQLConnection();
} elseif ($this->dbType === 'PgSQL') {
$this->dbConnection = new PgSQLConnection();
} else {
// 他のDBMSに接続する処理
}
}
public function performDatabaseTask(): void
{
if ($this->dbType === 'MySQL') {
$this->dbConnection->connectToMySQL();
// MySQLデータベースタスクを実行
} elseif ($this->dbType === 'PgSQL') {
$this->dbConnection->connectToPgSQL();
// PostgreSQLデータベースタスクを実行
} else {
// 他のDBMSのデータベースタスクを実行
}
}
}
<?php
// MySQLデータベースのタスクを実行
$dbClient = new DatabaseClient('MySQL');
$dbClient->performDatabaseTask();
// PostgreSQLデータベースのタスクを実行
$dbClient = new DatabaseClient('PgSQL');
$dbClient->performDatabaseTask();
次に、逆制御を行った場合のサンプルコードです。
このサンプルでは、DatabaseClientクラスがDatabaseConnectionインターフェイスを受け入れています。
これにより、異なるデータベース接続を注入することができます。
データベースクライアントのコードは、具体的なデータベース接続の詳細を知る必要がなく、逆制御によってデータベース接続が切り替えられます。
このように、逆制御(依存性の注入)を使用することで、コードの柔軟性とテスタビリティが向上し、コンポーネント間の依存関係が疎結合になります。
DBMSを追加する場合は既存のDatabaseClientクラスに触れずに、新規にDatabaseConnectionインターフェイスを実装したクラスを実装するだけで済みます。
<?php
/**
* インターフェイス
*/
interface DatabaseConnection
{
public function connect();
}
<?php
/**
* MySQLデータベース接続クラス
*/
class MySqlConnection implements DatabaseConnection
{
public function connect(): void
{
echo "MySQLに接続しました\n";
}
}
<?php
/**
* PostgreSQLデータベース接続クラス
*/
class PgSqlConnection implements DatabaseConnection
{
public function connect(): void
{
echo "PostgreSQLに接続しました\n";
}
}
<?php
/**
* データベースを利用するクラス
*/
class DatabaseClient
{
private $dbConnection;
public function __construct(DatabaseConnection $dbConnection)
{
$this->dbConnection = $dbConnection;
}
public function performDatabaseTask(): void
{
$this->dbConnection->connect();
// データベースタスクを実行
echo "データベースタスクを実行中\n";
}
}
<?php
// MySQLデータベースを使用
$mysqlConnection = new MySqlConnection();
$client1 = new DatabaseClient($mysqlConnection);
$client1->performDatabaseTask();
// PostgreSQLデータベースを使用
$pgsqlConnection = new PgSqlConnection();
$client2 = new DatabaseClient($pgsqlConnection);
$client2->performDatabaseTask();