依赖注入容器
在提取清晰抽象子系统的帮助下,依赖注入容器(DIP)建议我们创建模块化低耦合代码。
例如,如果你想简化一个大类,你可以将它分割成许多块的程序代码,并将每一个块提取成一个新的简单的独立类。
原则上,你的低级块应该实现一个充足清晰的抽象,并且高级代码应该只与这个抽象工作,不能与低级实现工作。
当我们将一个大的多任务类分割成小的专门类,我们会遇到创建依赖对象并将它们注入到对方中的问题。
之前如果我们创建了一个实例:
$service = new MyGiantSuperService();
分割以后我们将会创建或者获取所有的依赖项,并建立我们的服务:
$service = new MyService(
new Repository(new PDO('dsn', 'username', 'password')),
new Session(),
new Mailer(new SmtpMailerTransport('username', 'password', host')),
new Cache(new FileSystem('/tmp/cache')),
);
依赖注入容器是一个工厂,它能让我们不关心创建自己的对象。在Yii2中,我们可以一次性配置一个容器,然后就可以通过如下方式获取我们的服务:
$service = Yii::$container->get('app\services\MyService')
我们也可以使用这个:
$service = Yii::createObject('app\services\MyService')
或者在构造其它服务是,我们让容器作为一个依赖注入它:
use app\services\MyService;
class OtherService
{
public function __construct(MyService $myService) { … }
}
当我们获取OtherService实例时:
$otherService = Yii::createObject('app\services\OtherService')
在所有情况下,容器将会解析所有的依赖,并为每个注入依赖对象。
在本节中,我们创建了带有存储子系统的购物手推车,并将手推车自动注入到控制器中。
准备
按照官方向导http://www.yiiframework.com/doc-2.0/guide-startinstallation.html中的描述,使用Composer包管理器创建一个新应用。
如何做...
执行如下步骤:
- 创建一个购物手推车(shopping cart)类:
<?php
namespace app\cart;
use app\cart\storage\StorageInterface;
class ShoppingCart
{
private $storage;
private $_items = [];
public function __construct(StorageInterface $storage)
{
$this->storage = $storage;
}
public function add($id, $amount)
{
$this->loadItems();
if (array_key_exists($id, $this->_items)) {
$this->_items[$id]['amount'] += $amount;
} else {
$this->_items[$id] = [
'id' => $id,
'amount' => $amount,
];
}
$this->saveItems();
}
public function remove($id)
{
$this->loadItems();
$this->_items = array_diff_key($this->_items, [$id => []]);
$this->saveItems();
}
public function clear()
{
$this->_items = [];
$this->saveItems();
}
public function getItems()
{
$this->loadItems();
return $this->_items;
}
private function loadItems()
{
$this->_items = $this->storage->load();
}
private function saveItems()
{
$this->storage->save($this->_items);
}
}
它将只会和它自己的项工作。并不是内置地将项目存放在session,它将这个任务委派给了任意的外部存储类,这些类需要实现StorageInterface接口。
这个购物车类只是在它自己的构造器中获取了存储对象,将它保存在私有的$storage字段里,并通过load()和save()方法来调用。
使用必需的方法定义一个常用的手推车存储接口:
<?php
namespace app\cart\storage;
interface StorageInterface
{
/**
* @return array of cart items
*/
public function load();
/**
* @param array $items from cart
*/
public function save(array $items);
}
- 创建一个简单的存储实现。它将会在一个服务器session存储选择的项:
<?php
namespace app\cart\storage;
use yii\web\Session;
class SessionStorage implements StorageInterface
{
private $session;
private $key;
public function __construct(Session $session, $key)
{
$this->key = $key;
$this->session = $session;
}
public function load()
{
return $this->session->get($this->key, []);
}
public function save(array $items)
{
$this->session->set($this->key, $items);
}
}
这个存储可以在它的构造器中获取任意框架session实例,然后使用它来获取和存储项目。
在config/web.php文件中配置ShoppingCart类和它的依赖:
<?php
use app\cart\storage\SessionStorage;
Yii::$container->setSingleton('app\cart\ShoppingCart');
Yii::$container->set('app\cart\storage\StorageInterface',
function() {
return new SessionStorage(Yii::$app->session,
'primary-cart');
});
$params = require(__DIR__ . '/params.php');
//…
- 基于一个扩展的构造器创建cart控制器:
<?php
namespace app\controllers;
use app\cart\ShoppingCart;
use app\models\CartAddForm;
use Yii;
use yii\data\ArrayDataProvider;
use yii\filters\VerbFilter;
use yii\web\Controller;
class CartController extends Controller
{
private $cart;
public function __construct($id, $module, ShoppingCart $cart, $config = [])
{
$this->cart = $cart;
parent::__construct($id, $module, $config);
}
public function behaviors()
{
return [
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['post'],
],
],
];
}
public function actionIndex()
{
$dataProvider = new ArrayDataProvider([
'allModels' => $this->cart->getItems(),
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}
public function actionAdd()
{
$form = new CartAddForm();
if ($form->load(Yii::$app->request->post()) && $form->validate()) {
$this->cart->add($form->productId, $form->amount);
return $this->redirect(['index']);
}
return $this->render('add', [
'model' => $form,
]);
}
public function actionDelete($id)
{
$this->cart->remove($id);
return $this->redirect(['index']);
}
}
- 创建一个form:
<?php
namespace app\models;
use yii\base\Model;
class CartAddForm extends Model
{
public $productId;
public $amount;
public function rules()
{
return [
[['productId', 'amount'], 'required'],
[['amount'], 'integer', 'min' => 1],
];
}
}
- 创建视图文件views/cart/index.php:
<?php
use yii\grid\ActionColumn;
use yii\grid\GridView;
use yii\grid\SerialColumn;
use yii\helpers\Html;
/* @var $this yii\web\View */
/* @var $dataProvider yii\data\ArrayDataProvider */
$this->title = 'Cart';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="cart-index">
<h1><?= Html::encode($this->title) ?></h1>
<p><?= Html::a('Add Item', ['add'], ['class' => 'btn btn-success']) ?></p>
<?= GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
['class' => SerialColumn::className()],
'id:text:Product ID',
'amount:text:Amount',
[
'class' => ActionColumn::className(),
'template' => '{delete}',
]
],
]) ?>
</div>
- 创建视图文件views/cart/add.php:
<?php
use yii\helpers\Html;
use yii\bootstrap\ActiveForm;
/* @var $this yii\web\View */
/* @var $form yii\bootstrap\ActiveForm */
/* @var $model app\models\CartAddForm */
$this->title = 'Add item';
$this->params['breadcrumbs'][] = ['label' => 'Cart', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="cart-add">
<h1><?= Html::encode($this->title) ?></h1>
<?php $form = ActiveForm::begin(['id' => 'contact-form']);
?>
<?= $form->field($model, 'productId') ?>
<?= $form->field($model, 'amount') ?>
<div class="form-group">
<?= Html::submitButton('Add', ['class' => 'btn btn-primary']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>
- 添加链接项目到主菜单:
['label' => 'Home', 'url' => ['/site/index']],
['label' => 'Cart', 'url' => ['/cart/index']],
['label' => 'About', 'url' => ['/site/about']],
// …
- 打开cart页并尝试添加几行:
工作原理...
在这个例子中,通过一个抽象接口,我们定义了一个依赖较少的主类ShoppingCart:
class ShoppingCart
{
public function __construct(StorageInterface $storage) { … }
}
interface StorageInterface
{
public function load();
public function save(array $items);
}
然后我们实现了这个抽象类:
class SessionStorage implements StorageInterface
{
public function __construct(Session $session, $key) { … }
}
然后我们可以按如下方式手动创建一个cart的实例:
$storage = new SessionStorage(Yii::$app->session, 'primary-cart');
$cart = new ShoppingCart($storage)
它允许我们创建许多不同的实现,例如SessionStorage、CookieStorage或者DbStorage。并且我们可以在不同的项目和不同的框架中复用不依赖框架的基于StorageInterface的ShoppingCart类。我们只需为需要的框架使用接口的方法实现这个存储类。
并不需要手动创建一个带有所有依赖的实例,我们可以使用一个依赖注入容器。
默认情况下容器解析所有类的构造函数,并递归创建所有需要的实例。例如,如果我们有四个类:
class A {
public function __construct(B $b, C $c) { … }
}
class B {
...
}
class C {
public function __construct(D $d) { … }
}
class D {
...
}
我们可以用两种方法获取A类的实例:
$a = Yii::$container->get('app\services\A')
// or
$a = Yii::createObject('app\services\A')
并且容器自动创建B、D、C和A的实例,并将他们注入到对象中。
在我们的例子中,我们将cart实例标记为一个单件模式(singleton):
Yii::$container->setSingleton('app\cart\ShoppingCart');
这意味着容器将会为每一个重复的请求返回一个单例,而不是一次又一次的创建。
此外,我们的ShoppingCart在它自己的构造器中有StorageInterface类型,并且容器知道需要为这个类型实例化哪些类。我们必须按如下方式为接口手动绑定这个类:
Yii::$container->set('app\cart\storage\StorageInterface', 'app\cart\storage\CustomStorage',);
但是我们的SessionStorage有一个非标准构造器:
class SessionStorage implements StorageInterface
{
public function __construct(Session $session, $key) { … }
}
因此我们使用一个匿名函数来手动创建这个实例:
Yii::$container->set('app\cart\storage\StorageInterface', function()
{
return new SessionStorage(Yii::$app->session, 'primary-cart');
});
毕竟在我们自己的控制器、控件等其它地方,我们可以从容器中手动获取cart对象,
$cart = Yii::createObject('app\cart\ShoppingCart')
但是,在框架内部中,每一个控制器和其它对象将会通过createObject方法创建。并且我们可以通过控制器构造器来注入cart:
class CartController extends Controller
{
private $cart;
public function __construct($id, $module, ShoppingCart $cart,
$config = [])
{
$this->cart = $cart;
parent::__construct($id, $module, $config);
}
// ...
}
使用被注入的cart对象:
public function actionDelete($id)
{
$this->cart->remove($id);
return $this->redirect(['index']);
}
参考
- 更多关于DIP的信息,参见https://en.wikipedia.org/wiki/Dependency_inversion_principle
- 欲了解更多关于依赖注入控制,参见https://en.wikipedia.org/wiki/Dependency_inversion_principle