如下代码展示了一个更复杂的示例。UserLister 类依赖一个实现了 UserFinderInterface 接口的对象;UserFinder 类实现了这个接口,并依赖于一个 Connection 对象。所有这些依赖关系都是通过类构造器参数的类型提示定义的。通过属性依赖关系的注册,DI 容器可以自动解决这些依赖关系并能通过一个简单的 get('userLister') 调用创建一个新的 UserLister 实例。
namespace app\models; use yii\base\Object; use yii\db\Connection; use yii\di\Container; interface UserFinderInterface { function findUser(); } class UserFinder extends Object implements UserFinderInterface { public $db; public function __construct(Connection $db, $config = []) { $this->db = $db; parent::__construct($config); } public function findUser() { } } class UserLister extends Object { public $finder; public function __construct(UserFinderInterface $finder, $config = []) { $this->finder = $finder; parent::__construct($config); } } $container = new Container; $container->set('yii\db\Connection', [ 'dsn' => '...', ]); $container->set('app\models\UserFinderInterface', [ 'class' => 'app\models\UserFinder', ]); $container->set('userLister', 'app\models\UserLister'); $lister = $container->get('userLister'); // 等价于: $db = new \yii\db\Connection(['dsn' => '...']); $finder = new UserFinder($db); $lister = new UserLister($finder);
实践中的运用
当在应用程序的入口脚本中引入 Yii.php 文件时,Yii 就创建了一个 DI 容器。这个 DI 容器可以通过 Yii::$container 访问。当调用 Yii::createObject() 时,此方法实际上会调用这个容器的 yii\di\Container::get() 方法创建新对象。如上所述,DI 容器会自动解决依赖关系(如果有)并将其注入新创建的对象中。因为 Yii 在其多数核心代码中都使用了 Yii::createObject() 创建新对象,所以你可以通过 Yii::$container 全局性地自定义这些对象。
例如,你可以全局性自定义 yii\widgets\LinkPager 中分页按钮的默认数量:
\Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]);
这样如果你通过如下代码在一个视图里使用这个挂件,它的 maxButtonCount 属性就会被初始化为 5 而不是类中定义的默认值 10。
echo \yii\widgets\LinkPager::widget();
然而你依然可以覆盖通过 DI 容器设置的值:
echo \yii\widgets\LinkPager::widget(['maxButtonCount' => 20]);
另一个例子是借用 DI 容器中自动构造方法注入带来的好处。假设你的控制器类依赖一些其他对象,例如一个旅馆预订服务。你可以通过一个构造器参数声明依赖关系,然后让 DI 容器帮你自动解决这个依赖关系。
namespace app\controllers; use yii\web\Controller; use app\components\BookingInterface; class HotelController extends Controller { protected $bookingService; public function __construct($id, $module, BookingInterface $bookingService, $config = []) { $this->bookingService = $bookingService; parent::__construct($id, $module, $config); } }
如果你从浏览器中访问这个控制器,你将看到一个报错信息,提醒你 BookingInterface 无法被实例化。这是因为你需要告诉 DI 容器怎样处理这个依赖关系。
\Yii::$container->set('app\components\BookingInterface', 'app\components\BookingService');
现在如果你再次访问这个控制器,一个 app\components\BookingService 的实例就会被创建并被作为第三个参数注入到控制器的构造器中。
什么时候注册依赖关系
由于依赖关系在创建新对象时需要解决,因此它们的注册应该尽早完成。如下是推荐的实践:
如果你是一个应用程序的开发者,你可以在应用程序的入口脚本或者被入口脚本引入的脚本中注册依赖关系。
如果你是一个可再分发扩展的开发者,你可以将依赖关系注册到扩展的引导类中。
总结
依赖注入和服务定位器都是流行的设计模式,它们使你可以用充分解耦且更利于测试的风格构建软件。强烈推荐你阅读 Martin 的文章,对依赖注入和服务定位器有个更深入的理解。
Yii 在依赖住入(DI)容器之上实现了它的服务定位器。当一个服务定位器尝试创建一个新的对象实例时,它会把调用转发到 DI 容器。后者将会像前文所述那样自动解决依赖关系。
您可能感兴趣的文章: