依赖注入容器

在提取清晰抽象子系统的帮助下,依赖注入容器(DIP)建议我们创建模块化低耦合代码。

例如,如果你想简化一个大类,你可以将它分割成许多块的程序代码,并将每一个块提取成一个新的简单的独立类。

原则上,你的低级块应该实现一个充足清晰的抽象,并且高级代码应该只与这个抽象工作,不能与低级实现工作。

当我们将一个大的多任务类分割成小的专门类,我们会遇到创建依赖对象并将它们注入到对方中的问题。

之前如果我们创建了一个实例:

  1. $service = new MyGiantSuperService();

分割以后我们将会创建或者获取所有的依赖项,并建立我们的服务:

  1. $service = new MyService(
  2. new Repository(new PDO('dsn', 'username', 'password')),
  3. new Session(),
  4. new Mailer(new SmtpMailerTransport('username', 'password', host')),
  5. new Cache(new FileSystem('/tmp/cache')),
  6. );

依赖注入容器是一个工厂,它能让我们不关心创建自己的对象。在Yii2中,我们可以一次性配置一个容器,然后就可以通过如下方式获取我们的服务:

  1. $service = Yii::$container->get('app\services\MyService')

我们也可以使用这个:

  1. $service = Yii::createObject('app\services\MyService')

或者在构造其它服务是,我们让容器作为一个依赖注入它:

  1. use app\services\MyService;
  2. class OtherService
  3. {
  4. public function __construct(MyService $myService) { }
  5. }

当我们获取OtherService实例时:

  1. $otherService = Yii::createObject('app\services\OtherService')

在所有情况下,容器将会解析所有的依赖,并为每个注入依赖对象。

在本节中,我们创建了带有存储子系统的购物手推车,并将手推车自动注入到控制器中。

准备

按照官方向导http://www.yiiframework.com/doc-2.0/guide-startinstallation.html中的描述,使用Composer包管理器创建一个新应用。

如何做…

执行如下步骤:

  1. 创建一个购物手推车(shopping cart)类:
  1. <?php
  2. namespace app\cart;
  3. use app\cart\storage\StorageInterface;
  4. class ShoppingCart
  5. {
  6. private $storage;
  7. private $_items = [];
  8. public function __construct(StorageInterface $storage)
  9. {
  10. $this->storage = $storage;
  11. }
  12. public function add($id, $amount)
  13. {
  14. $this->loadItems();
  15. if (array_key_exists($id, $this->_items)) {
  16. $this->_items[$id]['amount'] += $amount;
  17. } else {
  18. $this->_items[$id] = [
  19. 'id' => $id,
  20. 'amount' => $amount,
  21. ];
  22. }
  23. $this->saveItems();
  24. }
  25. public function remove($id)
  26. {
  27. $this->loadItems();
  28. $this->_items = array_diff_key($this->_items, [$id => []]);
  29. $this->saveItems();
  30. }
  31. public function clear()
  32. {
  33. $this->_items = [];
  34. $this->saveItems();
  35. }
  36. public function getItems()
  37. {
  38. $this->loadItems();
  39. return $this->_items;
  40. }
  41. private function loadItems()
  42. {
  43. $this->_items = $this->storage->load();
  44. }
  45. private function saveItems()
  46. {
  47. $this->storage->save($this->_items);
  48. }
  49. }
  1. 它将只会和它自己的项工作。并不是内置地将项目存放在session,它将这个任务委派给了任意的外部存储类,这些类需要实现StorageInterface接口。

  2. 这个购物车类只是在它自己的构造器中获取了存储对象,将它保存在私有的$storage字段里,并通过load()和save()方法来调用。

  3. 使用必需的方法定义一个常用的手推车存储接口:

  1. <?php
  2. namespace app\cart\storage;
  3. interface StorageInterface
  4. {
  5. /**
  6. * @return array of cart items
  7. */
  8. public function load();
  9. /**
  10. * @param array $items from cart
  11. */
  12. public function save(array $items);
  13. }
  1. 创建一个简单的存储实现。它将会在一个服务器session存储选择的项:
  1. <?php
  2. namespace app\cart\storage;
  3. use yii\web\Session;
  4. class SessionStorage implements StorageInterface
  5. {
  6. private $session;
  7. private $key;
  8. public function __construct(Session $session, $key)
  9. {
  10. $this->key = $key;
  11. $this->session = $session;
  12. }
  13. public function load()
  14. {
  15. return $this->session->get($this->key, []);
  16. }
  17. public function save(array $items)
  18. {
  19. $this->session->set($this->key, $items);
  20. }
  21. }
  1. 这个存储可以在它的构造器中获取任意框架session实例,然后使用它来获取和存储项目。

  2. 在config/web.php文件中配置ShoppingCart类和它的依赖:

  1. <?php
  2. use app\cart\storage\SessionStorage;
  3. Yii::$container->setSingleton('app\cart\ShoppingCart');
  4. Yii::$container->set('app\cart\storage\StorageInterface',
  5. function() {
  6. return new SessionStorage(Yii::$app->session,
  7. 'primary-cart');
  8. });
  9. $params = require(__DIR__ . '/params.php');
  10. //…
  1. 基于一个扩展的构造器创建cart控制器:
  1. <?php
  2. namespace app\controllers;
  3. use app\cart\ShoppingCart;
  4. use app\models\CartAddForm;
  5. use Yii;
  6. use yii\data\ArrayDataProvider;
  7. use yii\filters\VerbFilter;
  8. use yii\web\Controller;
  9. class CartController extends Controller
  10. {
  11. private $cart;
  12. public function __construct($id, $module, ShoppingCart $cart, $config = [])
  13. {
  14. $this->cart = $cart;
  15. parent::__construct($id, $module, $config);
  16. }
  17. public function behaviors()
  18. {
  19. return [
  20. 'verbs' => [
  21. 'class' => VerbFilter::className(),
  22. 'actions' => [
  23. 'delete' => ['post'],
  24. ],
  25. ],
  26. ];
  27. }
  28. public function actionIndex()
  29. {
  30. $dataProvider = new ArrayDataProvider([
  31. 'allModels' => $this->cart->getItems(),
  32. ]);
  33. return $this->render('index', [
  34. 'dataProvider' => $dataProvider,
  35. ]);
  36. }
  37. public function actionAdd()
  38. {
  39. $form = new CartAddForm();
  40. if ($form->load(Yii::$app->request->post()) && $form->validate()) {
  41. $this->cart->add($form->productId, $form->amount);
  42. return $this->redirect(['index']);
  43. }
  44. return $this->render('add', [
  45. 'model' => $form,
  46. ]);
  47. }
  48. public function actionDelete($id)
  49. {
  50. $this->cart->remove($id);
  51. return $this->redirect(['index']);
  52. }
  53. }
  1. 创建一个form:
  1. <?php
  2. namespace app\models;
  3. use yii\base\Model;
  4. class CartAddForm extends Model
  5. {
  6. public $productId;
  7. public $amount;
  8. public function rules()
  9. {
  10. return [
  11. [['productId', 'amount'], 'required'],
  12. [['amount'], 'integer', 'min' => 1],
  13. ];
  14. }
  15. }
  1. 创建视图文件views/cart/index.php:
  1. <?php
  2. use yii\grid\ActionColumn;
  3. use yii\grid\GridView;
  4. use yii\grid\SerialColumn;
  5. use yii\helpers\Html;
  6. /* @var $this yii\web\View */
  7. /* @var $dataProvider yii\data\ArrayDataProvider */
  8. $this->title = 'Cart';
  9. $this->params['breadcrumbs'][] = $this->title;
  10. ?>
  11. <div class="cart-index">
  12. <h1><?= Html::encode($this->title) ?></h1>
  13. <p><?= Html::a('Add Item', ['add'], ['class' => 'btn btn-success']) ?></p>
  14. <?= GridView::widget([
  15. 'dataProvider' => $dataProvider,
  16. 'columns' => [
  17. ['class' => SerialColumn::className()],
  18. 'id:text:Product ID',
  19. 'amount:text:Amount',
  20. [
  21. 'class' => ActionColumn::className(),
  22. 'template' => '{delete}',
  23. ]
  24. ],
  25. ]) ?>
  26. </div>
  1. 创建视图文件views/cart/add.php:
  1. <?php
  2. use yii\helpers\Html;
  3. use yii\bootstrap\ActiveForm;
  4. /* @var $this yii\web\View */
  5. /* @var $form yii\bootstrap\ActiveForm */
  6. /* @var $model app\models\CartAddForm */
  7. $this->title = 'Add item';
  8. $this->params['breadcrumbs'][] = ['label' => 'Cart', 'url' => ['index']];
  9. $this->params['breadcrumbs'][] = $this->title;
  10. ?>
  11. <div class="cart-add">
  12. <h1><?= Html::encode($this->title) ?></h1>
  13. <?php $form = ActiveForm::begin(['id' => 'contact-form']);
  14. ?>
  15. <?= $form->field($model, 'productId') ?>
  16. <?= $form->field($model, 'amount') ?>
  17. <div class="form-group">
  18. <?= Html::submitButton('Add', ['class' => 'btn btn-primary']) ?>
  19. </div>
  20. <?php ActiveForm::end(); ?>
  21. </div>
  1. 添加链接项目到主菜单:
  1. ['label' => 'Home', 'url' => ['/site/index']],
  2. ['label' => 'Cart', 'url' => ['/cart/index']],
  3. ['label' => 'About', 'url' => ['/site/about']],
  4. // …
  1. 打开cart页并尝试添加几行:

依赖注入容器 - 图1

工作原理…

在这个例子中,通过一个抽象接口,我们定义了一个依赖较少的主类ShoppingCart:

  1. class ShoppingCart
  2. {
  3. public function __construct(StorageInterface $storage) { }
  4. }
  5. interface StorageInterface
  6. {
  7. public function load();
  8. public function save(array $items);
  9. }

然后我们实现了这个抽象类:

  1. class SessionStorage implements StorageInterface
  2. {
  3. public function __construct(Session $session, $key) { }
  4. }

然后我们可以按如下方式手动创建一个cart的实例:

  1. $storage = new SessionStorage(Yii::$app->session, 'primary-cart');
  2. $cart = new ShoppingCart($storage)

它允许我们创建许多不同的实现,例如SessionStorage、CookieStorage或者DbStorage。并且我们可以在不同的项目和不同的框架中复用不依赖框架的基于StorageInterface的ShoppingCart类。我们只需为需要的框架使用接口的方法实现这个存储类。

并不需要手动创建一个带有所有依赖的实例,我们可以使用一个依赖注入容器。

默认情况下容器解析所有类的构造函数,并递归创建所有需要的实例。例如,如果我们有四个类:

  1. class A {
  2. public function __construct(B $b, C $c) { }
  3. }
  4. class B {
  5. ...
  6. }
  7. class C {
  8. public function __construct(D $d) { }
  9. }
  10. class D {
  11. ...
  12. }

我们可以用两种方法获取A类的实例:

  1. $a = Yii::$container->get('app\services\A')
  2. // or
  3. $a = Yii::createObject('app\services\A')

并且容器自动创建B、D、C和A的实例,并将他们注入到对象中。

在我们的例子中,我们将cart实例标记为一个单件模式(singleton):

  1. Yii::$container->setSingleton('app\cart\ShoppingCart');

这意味着容器将会为每一个重复的请求返回一个单例,而不是一次又一次的创建。

此外,我们的ShoppingCart在它自己的构造器中有StorageInterface类型,并且容器知道需要为这个类型实例化哪些类。我们必须按如下方式为接口手动绑定这个类:

  1. Yii::$container->set('app\cart\storage\StorageInterface', 'app\cart\storage\CustomStorage',);

但是我们的SessionStorage有一个非标准构造器:

  1. class SessionStorage implements StorageInterface
  2. {
  3. public function __construct(Session $session, $key) { }
  4. }

因此我们使用一个匿名函数来手动创建这个实例:

  1. Yii::$container->set('app\cart\storage\StorageInterface', function()
  2. {
  3. return new SessionStorage(Yii::$app->session, 'primary-cart');
  4. });

毕竟在我们自己的控制器、控件等其它地方,我们可以从容器中手动获取cart对象,

  1. $cart = Yii::createObject('app\cart\ShoppingCart')

但是,在框架内部中,每一个控制器和其它对象将会通过createObject方法创建。并且我们可以通过控制器构造器来注入cart:

  1. class CartController extends Controller
  2. {
  3. private $cart;
  4. public function __construct($id, $module, ShoppingCart $cart,
  5. $config = [])
  6. {
  7. $this->cart = $cart;
  8. parent::__construct($id, $module, $config);
  9. }
  10. // ...
  11. }

使用被注入的cart对象:

  1. public function actionDelete($id)
  2. {
  3. $this->cart->remove($id);
  4. return $this->redirect(['index']);
  5. }

参考