概要
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自体の理解も進みました。
ご参考になれば幸いです。