理解 Dependency Injection 实现原理
来源:互联网 发布:多重网络会影响网速吗 编辑:程序博客网 时间:2024/05/17 08:08
现代较新的 Web Framework 都强调自己有 Dependency Injection (以下简称 DI ) 的特色,只是很多人对它的运作原理还是一知半解。
所以接下来我将用一个简单的范例,来为各位介绍在 PHP 中如何实现简易的 DI 。
基本范例
这是一个应用程式的范例,它只包含了登入处理程序。在这个范例中, App 类别的建构式参考了新的 Auth 与 Session 的物件实体,并在 App::login() 中使用。
注:请特别注意,为了呈现重点,我忽略掉很多程式码,同时也没有进行良好的架构设计;所以请不要把这个范例用在你的程式中,或是对为什么我没有进行错误处理,以及为什么要采用奇怪的设计提出质疑。
class App{ protected $auth = null; protected $session = null; public function __construct($dsn, $username, $password) { $this->auth = new Auth($dsn, $username, $password); $this->session = new Session(); } public function login($username, $password) { if ($this->auth->check($username, $password)) { $this->session->set('username', $username); return true; } return false; }}
而 Auth 类别是从资料库验证使用者身份,这里我仅用简单的描述来呈现效果。
class Auth{ public function __construct($dsn, $user, $pass) { echo "Connecting to '$dsn' with '$user'/'$pass'...\n"; } public function check($username, $password) { echo "Checking username, password from database...\n"; return true; }}
Session 类别也是概念性的实作:
class Session{ public function set($name, $value) { echo "Set session variable '$name' to '$value'."; }}
最后我们让程式动起来, client 程式如下:
$app = new App('mysql://localhost', 'username', 'password');$username = 'jaceju';if ($app->login($username, 'password')) { echo "$username just signed in.\n";}
注:这里的 client 程式指的是实际操作这些物件实体的程式。
各位可以先试着想想这个程式在可扩充性上有什么问题?例如我想把身份认证方式换成第三方服务的机制,或是改用其他媒介来存放 session 内容等。
还有如果想在没有资料库连线、或是没有 HTTP session 的环境下对 App::login() 方法的逻辑进行隔离测试,各位会怎么做呢?
解除依赖关系
上面的范例因为 App 类别已经依赖了 Auth 类别和 Session 类别,而这两个类别都有实作跟系统环境有关的程式逻辑,这么一来就会让 App 类别难以进行底层机制的切换或是隔离测试。所以接下来我们要做的,就是把它们的依赖关系解除。
修改后的 App 类别如下:
class App{ protected $auth = null; protected $session = null; public function __construct(Auth $auth, Session $session) { $this->auth = $auth; $this->session = $session; }}$auth = new Auth('mysql://localhost', 'username', 'password');$session = new Session();$app = new App($auth, $session);
首先我们在 App 类别的建构式 __construct 原本的资料库设定参数移除,并将原来直接以 new 关键字所产生的物件实体,改用方法参数的方式来注入。而使用 new 关键字产生物件实体的程式码,就移到 App 类别外。
这种「将依赖的类别改用方法参数来注入」的作法,就是我们说的「依赖注入 (Dependency Injection) 」。
常见依赖注入的方式有两种: Constructor Injection 及 Setter Injection 。它们的实作形式并没有什么不同,差别只在于是不是类别建构式而已。
不过 Constructor Injection 必须在建立物件实体时就进行注入,而 Setter Injection 则是可以在物件实体建立后才透过 setter 函式来进行注入。而这里为了方便解说,我采用的是 Constructor Injection 。
依赖抽象介面
好了,现在的问题是 Auth 类别的实作还是依赖在资料库上,所以我们也要让 Auth 类别跟资料库之间解除依赖关系,让它成为一个抽象介面。
这里的抽象介面是指观念上的意义,而非语言层级上的抽象类别 (Abstract Class) 或介面 (Interface) 。至于在实作上该用抽象类别还是介面,在这个范例里并没有差别,大家可以自行判断;这里我用介面 (Interface) ,因为我仅需要 Auth::check() 这个介面方法的定义而已。
这一步首先我把原来的 Auth 类别重新命名为 DbAuth 类别:
class DbAuth{ public function __construct($dsn, $user, $pass) { echo "Connecting to '$dsn' with '$user'/'$pass'...\n"; } public function check($username, $password) { echo "Checking username, password from database...\n"; return true; }}
接着建立一个 Auth 介面,它包含了 Auth::check() 方法的定义:
interface Auth{ public function check($username, $password);}
然后让 DbAuth 类别实作 Auth 介面:
class DbAuth implements Auth{ // ...}
最后把原来初始化 Auth 类别的物件实体的程式码,改为初始化 DbAuth 的物件实体。
$auth = new DbAuth('mysql://localhost', 'username', 'password');$session = new Session();$app = new App($auth, $session);
透过 Auth 介面的帮助,我们已经让 App 类别与实际的资料库操作类别分离开来了。现在只要是实作 Auth 介面的类别,都可以被 App 类别所接受,例如我们可能会改用 HTTP 认证来取代资料库认证:
class HttpAuth implements Auth{ public function check($username, $password) { echo "Checking username, password from HTTP Authentication...\n"; return true; }}$auth = new HttpAuth();$session = new Session();$app = new App($auth, $session);
当然其他类型的认证方式也可以透过建立新的类别来使用,而不会影响到 App 类别的内部实作。
DI 容器
现在又有个问题, client 程式还是依赖于 DbAuth 类别或是 HttpAuth 类别;通常这种状况在需要编译型的语言 (例如 Java ) 中,程式一旦编译完成布署出去后,就很难再进行修改。
如果我们可以改用设定的方式来告诉程式,在不同的状况下对应不同的类别,然后让程式自行判断环境来产生需要的物件实体,这样就可以解开 client 程式对实作类别的依赖关系。
这里要引入一个技术,称为 DI 容器 (Dependency Injection Container) 。 DI 容器主要的作用在于帮我们解决产生物件实体时,应该参考哪一个类别。我们先来看看用法:
Container::register('Auth', 'DbAuth', ['mysql://localhost', 'username', 'password']);$auth = Container::get('Auth');$session = new Session();$app = new App($auth, $session);
首先我们在 DI 容器中先以 Container::register() 方法来注册 Auth 这个别名实际上要对应哪个类别,以及建立物件实体时会用到的初始化参数。要注意,这里的别名并不是指真正的类别或介面,但我们可以用相同的名称以避免认知上的问题。
然后我们用 Container::get() 方法取得别名所对应类别的物件实体,上面例子里的 $auth 就是 DbAuth 类别的物件实体。
这么一来,我们就可以把注册的程式码移出 client 程式之外,并将注册参数改用设定档引入,顺利解开 client 程式对实作类别的依赖。
DI 容器原理
那么 DI 容器的原理是怎么运作的呢?首先在 Container::register() 方法注册的部份,它其实只是把参数记到 $map 这个类别静态属性里。
class Container{ protected static $map = []; public static function register($name, $class, $args = null) { static::$map[$name] = [$class, $args]; } // ...}
重点在 Container::get() 方法,它透过
class Container{ // ... public static function get($name) { list($class, $args) = isset(static::$map[$name]) ? static::$map[$name] : [$name, null]; if (class_exists($class, true)) { $reflectionClass = new ReflectionClass($class); return !empty($args) ? $reflectionClass->newInstanceArgs($args) : new $class(); } return null; }}
比较特别的是,如果初始化参数不是空值 (null) 时,则必须透过 ReflectionClass::newInstanceArgs() 方法来建立物件实体。 ReflectionClass 类别可以映射出指定类别的内部结构,并提供方法来操作这个结构; Reflection 是现代语言常见的机制, PHP 在这方面也提供了完整的 API 供开发者使用,请参考: PHP: Reflection 。
Container::get() 方法也可以在没有注册的状况下,直接把别名当成类别名称,然后协助我们初始化对应的物件实体;例如:
$session = Container::get('Session');
手动注入
现在我们的 client 程式已经修改成以下的样子:
$auth = Container::get('Auth');$session = Container::get('Session');$app = new App($auth, $session);
不过当初始化参数较多的状况下,重复写好几次 Container::get() 看起来也是挺囉嗦的。
接下来我们实作一个 Container::inject() 方法,提供开发者可以一次注入所有依赖物件实体:
$app = Container::inject('Auth', 'Session', function ($auth, $session) { return new App($auth, $session);});
这里我们让 Container::inject() 接受不定个数的参数,除了最后一个参数必须是 callback 型态外,其他都是要传递给 Container::get() 的参数。 Container::inject() 的实作方式如下:
class Container{ // ... public static function inject() { $args = func_get_args(); $callback = array_pop($args); $injectArgs = []; foreach ($args as $name) { $injectArgs[] = Container::get($name); } return call_user_func_array($callback, $injectArgs); }
在参数个数不定的状况下,可以用 func_get_args() 函式来取得所有参数;而 array_pop() 可以取出最后一个参数值做为 callback 。剩下的参数就透过 Container::get() 来取得物件实体,最后再透过 call_user_func_array() 函式将处理好的参数传递给 callback 执行。
自动解决所有依赖注入
在我们的范例里, Container 类别如果可以提供一个方法,自动为我们解决所有 App 类别依赖问题,那么程式就可以更干净些。
要做到这点,我们就必须知道要注入的方法所需要参数的类型;而在 PHP 中的 Type Hinting ,就可以告诉我们参数所对应的变数类型或类别。
回到 App::__construct() 建构子上,我们看到
class App{ public function __construct(Auth $auth, Session $session) { }}
接着我们为 Container 类别提供一个 resolve() 方法,它可以接受一个类别名称用来建立物件实体,而不需要再使用 new 关键字。
$app = Container::resolve('App');
我们希望 Container::resolve() 方法会自动产生参数所对应的物件,解决这个类别建构子所需要的依赖关系。它的实作如下:
class Container{ // ... public static function resolve($name) { if (!class_exists($name, true)) { return null; } $reflectionClass = new ReflectionClass($name); $reflectionConstructor = $reflectionClass->getConstructor(); $reflectionParams = $reflectionConstructor->getParameters(); $args = []; foreach ($reflectionParams as $param) { $class = $param->getClass()->getName(); $args[] = static::get($class); } return !empty($args) ? $reflectionClass->newInstanceArgs($args) : new $class(); }}
Container::resolve() 方法与 Container::get() 方法的原理类似,但较特别的是它使用了 ReflectionClass::getConstructor() 方法来取得类别建构子的 ReflectionMethod 实体;接着再用 ReflectionMethod::getParameters() 取出参数的 ReflectionParameter 物件集合 (阵列) 。
而后我们就可以在回圈中一一透过 ReflectionParameter::getClass() 方法与 ReflectionClass::getName() 方法来取得 type hint 所指向的类别或介面名称。当有了参数所对应的类别或介面名称后,就可以用 Container::get() 方法来取得参数的物件实体。
最后把这些物件带回建构子的参数里,并初始化我们所需要的物件实体,就完成了 App 类别的自动依赖注入。
深入思考
再强调一次,这里的范例只是为了介绍 DI 容器的原理,并不能真正用在实务上。因为一个完整的 DI 容器还要考虑以下的问题:
类别不存在时的处理。与其他非类别的参数整合。如何建立设定档机制以便切换依赖关系。递回地自动注入物件实体。取得 Singleton 物件实体。可以透过原始码上的 DocBlock 注解来注明依赖关系。
目前已经有很多 DI Framework 帮我们处理好这些事情了,建议大家如果真的需要在专案中使用 DI 时,应该采用这些 Framework 。
总结
如果专案并不会有太多变化性,那么依赖注入对我们来说就不是那么重要。但是如果希望程式对特定类别的依赖性降低,只针对抽象介面实作,那么依赖注入就有其必要性。
在 PHP 上的 DI 容器的基本实作原理也不复杂,透过 Reflection 机制就可以看到类别内部的结构,让我们对它的建构子注入我们想要的参数值。
DI 容器要考量的部份也不少,但这些功能都已经有 Framework 实作,我们应该在专案中使用它们而尽可能不要自行开发。
希望透过以上的介绍,可以让大家对 Framework 的依赖注入机制有基本的认知。
注:上述程式码都可以在 php-di-container-examples 找到。
转载自網站製作學習誌
原文链接:http://jaceju.net/2014-07-27-php-di-container/
- 理解 Dependency Injection 实现原理
- 依赖注入 理解Dependency Injection
- PHP依赖注入(Dependency Injection)代码实例(Laravel container实现原理)
- Dependency Injection
- Dependency Injection
- Dependency Injection
- Dependency Injection
- PHP程序员如何理解依赖注入容器(dependency injection container)
- IOC和Dependency Injection
- IOC和Dependency Injection
- IOC vs Dependency Injection
- Dependency Injection pattern
- Dependency injection with Flex
- DI, Dependency Injection
- Spring 3 Dependency injection
- Setter-based dependency injection
- Dependency Injection with Autofac
- Inside CAB Dependency Injection
- stm32_011_stm32位绑定操作
- 使物体一直面向摄像机
- MySQL简单语法(5)
- 笔记一: 增删改查
- 转载--Android--调用系统照相机拍照与摄像
- 理解 Dependency Injection 实现原理
- laravel service使用
- Hadoop之HA
- java ftp
- 金银岛
- 【安全牛学习笔记】WPS及其他工具
- day25-序列化,common.io工具类,properties,打印流
- 【Scikit-Learn 中文文档】模型选择:选择估计量及其参数
- Ubuntu Caffe IDE工具单步调试