IOC 模式解耦与 Laravel 服务容器

发布于2020-08-05 | 1090 阅读 | 1 喜欢 | phplaravelloc

前言

服务容器是 Laravel 框架实现模块化解耦的核心。模块化即是将系统拆成多个子模块,子模块间的耦合程度尽可能的低,代码中尽可能的避免直接调用。这样才能提高系统的代码重用性、可维护性、扩展性。

下边出行例子有火车、飞机两种出行方式,对应给出了 3 种耦合度越来越低的实现:高度耦合实现、工厂模式解耦、IOC 模式解耦。

高度耦合实现

代码实现

定义 TrafficTool 接口并用 Train、Plane 实现,最后在 Traveler 中实例化出行工具后说走就走。代码十分简洁:

<?php// 定义交通工具接口interface TrafficTool{ public function go();}class Train implements TrafficTool{ public function go() { echo '[Travel By]: train', PHP_EOL; }}class Plane implements TrafficTool{ public function go() { echo '[Travel By]: plane', PHP_EOL; }}// 旅游者类,使用火车出行class Traveler{ protected $trafficTool; public function __construct() { // 直接 new 对象,在 Traveler 与 Train 两个类之间产生了依赖 // 如果程序内部要修改出行方式,必须修改 Traveler 的 __construct() // 代码高度耦合,可维护性低 $this->travelTool = new Train(); } public function travel() { $this->travelTool->go(); }}$me = new Traveler();$me->travel();

运行:

$ php normal.php[Travel By]: train

优点

代码十分简洁:一个接口两个类最后直接调用。

缺点

在第 32 行,Traveler 与 Train 两个组件发生了耦合。以后想坐飞机出行,必须修改 __construct() 的内部实现:$this->travelTool = new Plane();

重用性和可维护性都很差:在实际的软件开发中,代码会根据业务需求的变化而不断修改。如果组件之间直接相互调用,那组件的代码就不能轻易修改,以免调用它的地方出现错误。

工厂模式解耦

工厂模式

分离代码中不变和变的部分,使得在不同条件下创建不同的对象。

代码实现

...class TrafficToolFactory{ public function create($name) { switch ($name) { case 'train': return new Train(); case 'plane': return new Plane(); default: exit('[No Traffic Tool] :' . $name); } }}// 旅游者类,使用火车出行class Traveler{ protected $trafficTool; public function __construct($toolName) { // 使用工厂类实例化需要的交通工具 $factory = new TrafficToolFactory(); $this->travelTool = $factory->create($toolName); } public function travel() { $this->travelTool->go(); }}// 传入指定的方式$me = new Traveler('train');$me->travel();

运行:

$ php factory.php[Travel By]: train

优点

提取了代码中变化的部分:更换交通工具,坐飞机出行直接修改 $me = new Traveler('plane') 即可。适用于需求简单的情况。

缺点

依旧没有彻底解决依赖:现在 Traveler 与 TrafficToolFactory 发生了依赖。当需求增多后,工厂的 switch...case 等代码也不易维护。

IOC 模式解耦

IOC 是 Inversion Of Controll 的缩写,即控制反转。这里的“反转”可理解为将组件间依赖关系提到外部管理。

简单的依赖注入

依赖注入是 IOC 的一种实现方式,是指组件间的依赖通过外部参数(interface)形式直接注入。比如对上边的工厂模式进一步解耦:

<?phpinterface TrafficTool{ public function go();}class Train implements TrafficTool{ public function go() { echo '[Travel By]: train', PHP_EOL; }}class Plane implements TrafficTool{ public function go() { echo '[Travel By]: plane', PHP_EOL; }}class Traveler{ protected $trafficTool; // 参数 $tool 就是控制反转要反转部分,将依赖的对象直接传入即可 // 以后再有 Car, GetWay ... 等新增工具也是实例化后传参直接调用 public function __construct(TrafficTool $tool) { $this->trafficTool = $tool; } public function travel() { $this->trafficTool->go(); }}$train = new Train();$me = new Traveler($train); // 将依赖直接以参数的形式注入$me->travel();

运行:

$ php simple_ioc.php[Travel By]: train

高级依赖注入

简单注入的问题

如果三个人分别自驾游、坐飞机、高铁出去玩,那你的代码可能是这样的:

$train = new Train();$plane = new Plane();$car = new Car();$a = new Traveler($car);$b = new Traveler($plane);$c = new Traveler($train);$a->travel();$b->travel();$c->travel();

看起来就两个字:蓝瘦。上边简单的依赖注入相比工厂模式已经解耦挺多了,参考 Laravel 中服务容器的概念,还能继续解耦。

IOC 容器

高级依赖注入 = 简单依赖注入 + IOC 容器

<?php# advanced_ioc.php ... class Container{ protected $binds = []; protected $instances = []; /** * 绑定:将回调函数绑定到字符指令上 * * @param $abstract 字符指令,如 'train' * @param $concrete 用于实例化组件的回调函数,如 function() { return new Train(); } */ public function bind($abstract, $concrete) { if ($concrete instanceof Closure) { // 向容器中添加可以执行的回调函数 $this->binds[$abstract] = $concrete; } else { $this->instances[$abstract] = $concrete; } } /** * 生产:执行回调函数 * * @param $abstract 字符指令 * @param array $params 回调函数所需参数 * @return mixed 回调函数的返回值 */ public function make($abstract, $params = []) { if (isset($this->instances[$abstract])) { return $this->instances[$abstract]; } // 此时 $this 是有 2 个元素的数组 // Array ( // [0] => Container Object ( // [binds] => Array ( ... ) // [instances] => Array() // ) // [1] => "train" // ) array_unshift($params, $this); // 将参数传递给回调函数 return call_user_func_array($this->binds[$abstract], $params); }}$container = new Container();$container->bind('traveler', function ($container, $trafficTool) { return new Traveler($container->make($trafficTool));});$container->bind('train', function ($container) { return new Train();});$container->bind('plane', function ($container) { return new Plane();});$me = $container->make('traveler', ['train']);$me->travel();

运行:

$ php advanced_ioc.php[Travel By]: train

简化并解耦后的代码

那三个人再出去玩,代码将简化为:

$a = $container->make('traveler', ['car']);$b = $container->make('traveler', ['train']);$c = $container->make('traveler', ['plane']);$a->travel();$b->travel();$c->travel();

Laravel 的服务容器

Laravel 自己的服务容器是一个更加高级的 IOC 容器,它的简化代码如下:

<?php# laravel_ioc.php ... class Container{ // 绑定回调函数 public $binds = []; // 绑定接口 $abstract 与回调函数 public function bind($abstract, $concrete = null, $shared = false) { if (!$concrete instanceof Closure) { $concrete = $this->getClosure($abstract, $concrete); } $this->binds[$abstract] = compact('concrete', 'shared'); } // 获取回调函数 public function getClosure($abstract, $concrete) { return function ($container) use ($abstract, $concrete) { $method = ($abstract == $concrete) ? 'build' : 'make'; return $container->$method($concrete); }; } protected function getConcrete($abstract) { if (!isset($this->binds[$abstract])) { return $abstract; } return $this->binds[$abstract]['concrete']; } // 生成实例对象 public function make($abstract) { $concrete = $this->getConcrete($abstract); if ($this->isBuildable($abstract, $concrete)) { $obj = $this->build($concrete); } else { $obj = $this->make($concrete); } return $obj; } // 判断是否要用反射来实例化 protected function isBuildable($abstract, $concrete) { return $concrete == $abstract || $concrete instanceof Closure; } // 通过反射来实例化 $concrete 的对象 public function build($concrete) { if ($concrete instanceof Closure) { return $concrete($this); } $reflector = new ReflectionClass($concrete); if (!$reflector->isInstantiable()) { echo "[can't instantiable]: " . $concrete; } $constructor = $reflector->getConstructor(); // 使用默认的构造函数 if (is_null($constructor)) { return new $concrete; } $refParams = $constructor->getParameters(); $instances = $this->getDependencies($refParams); return $reflector->newInstanceArgs($instances); } // 获取实例化对象时所需的参数 public function getDependencies($refParams) { $deps = []; foreach ($refParams as $refParam) { $dep = $refParam->getClass(); if (is_null($dep)) { $deps[] = null; } else { $deps[] = $this->resolveClass($refParam); } } return (array)$deps; } // 获取参数的类型类名字 public function resolveClass(ReflectionParameter $refParam) { return $this->make($refParam->getClass()->name); }}$container = new Container();// 将 traveller 对接到 Train $container->bind('TrafficTool', 'Train');$container->bind('traveller', 'Traveller');// 创建 traveller 实例$me = $container->make('traveller');$me->travel();

运行:

$ php laravel_ioc.php [Travel By]: train

Train 类要能被实例化,需要先注册到容器,这就涉及到 Laravel 中服务提供者(Service Provider)的概念了。至于服务提供者是怎么注册类、注册之后如何实例化、实例化后如何调用的... 下节详细分析。

总结

本文用一个旅游出行的 demo,引出了高度耦合的直接实现、工厂模式解耦和 IOC 模式解耦共计三种实现方式,越往后代码量越多还有些绕,但类(模块)之间的耦合度越来越低,最后实现了简化版的 Laravel 服务容器。

Laravel 的优美得益于开发的组件式解耦,这与服务容器和服务提供者的理念是离不开的,下篇将用 Laravel 框架 laravel/framework/src/Illuminate/Container.php 中 Container 类来梳理 Laravel 服务容器的工作流程。r