概要
DIやDIコンテナについて、自分が調べたことをまとめます。
DIとは何か
依存性の注入(Dependency injection)を略してDIと言います。
依存性とは具体的にはクラスのことです。
以下の例を見てみます。
class ProductController {
public function mainAction() {
$model = new ProductModel();
$product = $model->getById(1);
}
}
class ProductModel {
public function getById($id) {
$db = new MySqlConnect();
$query = $db->query('select * from product where id = ?', $id);
$product = $query->getResult();
// ここに様々な処理があるとする
return $product;
}
}
ProductModelではMySqlConnectをnewして利用しています。
この状態をProductModelはMySqlConnectに依存している、と言い、以下のようなデメリットがあります。
-
ProductModelとMySqlConnectは互いに密結合になるため、修正による影響を受けやすくなる。 -
MySqlConnectをモックで置き換えられないため、ProductModelがテストしづらくなる。 -
ProductModelはMySqlConnectが実装されていないと動かないため、分業がしづらい。
例えばデータベースシステムの移行などで、新たにOracleConnectを実装することを考えてみます。
上記のデメリットがあるため、MySqlConnectのインスタンスをProductControllerから渡します。
class ProductController {
public function mainAction() {
$model = new ProductModel(new MySqlConnect());
$product = $model->getById(1);
}
}
class ProductModel {
private $db;
pubic function __construct(MySqlConnect $db) {
$this->db = $db;
}
public function getById($id) {
$query = $this->db->query('select * from product where id = ?', $id);
$product = $query->getResult();
// ここに様々な処理があるとする
return $product;
}
}
上記の手法、デザインパターンをDIと呼びます。
コンストラクタでセットしているので、コンストラクタ注入と呼びます。
続いてMySqlConnectをOracleConnectに置き換えます。
単純に置き換えると、将来的にPgSqlConnectが登場した際に困りますので、DatabaseConnectインターフェースを実装します。
interface DatabaseConnect {}
class MySqlConnect implements DatabaseConnect {}
class OracleConnect implements DatabaseConnect {}
class ProductController {
public function mainAction() {
// $model = new ProductModel(new MySqlConnect());
$model = new ProductModel(new OracleConnect());
$product = $model->getById(1);
}
}
class ProductModel {
private $db;
pubic function __construct(DatabaseConnect $db) {
$this->db = $db;
}
public function getById($id) {
$query = $this->db->query('select * from product where id = ?', $id);
$product = $query->getResult();
// ここに様々な処理があるとする
return $product;
}
}
これで新たにPgSqlConnectが登場しても、ProductModelを修正する必要はありません。
DIコンテナとは何か
ですが、毎回この作業を手動で実装するのは大変です。
そこでDIコンテナという仕組みを使います。
以下はpimpleを使った例です。
require_once __DIR__.'/vendor/autoload.php';
use Pimple\Container;
interface DatabaseConnect {}
class MySqlConnect implements DatabaseConnect {}
class OracleConnect implements DatabaseConnect {}
class Settings {
private $container;
public function __construct() {
$container = new Container();
$container['db'] = function ($c) {
return new OracleConnect();
}
$this->container = $container;
}
public function get($name) {
return $this->container[$name];
}
}
class ProductController {
public function mainAction() {
$settings = new Settings();
$model = new ProductModel($settings->get('db'));
$product = $model->getById(1);
}
}
class ProductModel {
private $db;
pubic function __construct(DatabaseConnect $db) {
$this->db = $db;
}
public function getById($id) {
$query = $this->db->query('select * from product where id = ?', $id);
$product = $query->getResult();
// ここに様々な処理があるとする
return $product;
}
}
これでSettingsを変更するだけで、すべてのクラスにOracleConnectを適用させることができます。
DIコンテナ、インターフェースを使うことで、以下のメリットが得られます。
- クラス同士の結合度を下げ、互いの修正が影響し合わないように設計できる。
-
DatabaseConnectインターフェースを実装したテスト用のクラスを作成することで、ProductModelを簡単にテストすることができる。 -
OracleConnectが未実装であっても、ProductModelを開発することができるので、分業ができる。
Symfonyではどのようになっているのか
SymfonyはDIコンテナを備えたフレームワークです。
Services.ymlという設定ファイルがDIコンテナの設定ファイルの役割を果たしています。
services:
product_model:
class: AppBundle\SomeDirectory\ProductModel
arguments: ["@db"]
db:
# ここを変えれば別のクラスをProductModelに渡せる
class: AppBundle\SomeDirectory\MySqlConnect
class ProductController {
public function mainAction() {
$model = $this->get('product_model');
$product = $model->getById(1);
}
}
class ProductModel {
private $db;
pubic function __construct(DatabaseConnect $db) {
$this->db = $db;
}
public function getById($id) {
$query = $this->db->query('select * from product where id = ?', $id);
$product = $query->getResult();
// ここに様々な処理があるとする
return $product;
}
}
まとめ
自分自身あまりよく背景を知らずにSymfonyを使っていたのですが、DIコンテナを知ることでSymfony自体の理解も進みました。
ご参考になれば幸いです。