微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

244 设计模式 -- 行为型 -- 依赖注入

这是之前我写在公司的jira上的关于设计模式的分享,后面会陆续转过来一些不涉及业务的其他文章


定义

依赖注入(DI),或者控制反转(IOC),可以帮助我们开发出松散耦合的程序,以下stackoverflow高票答案stackoverflow给出的类比。

When you go and get things out of the refrigerator for yourself,you can cause problems. You might leave the door open,you might get something Mommy or Daddy doesn’t want you to have. You might even be looking for something we don’t even have or which has expired.

What you should be doing is stating a need,“I need something to drink with lunch,” and then we will make sure you have something when you sit down to eat.”

依赖注入构建了一个服务容器(父母),于是高层类(小孩)和底层组件(冰箱,凳子)之间的依赖关系都变成了服务容器和底层组件之间的依赖关系,从而分离了高层类和底层组件之间的依赖。

服务容器可以认为是一个大的工厂(工厂模式),只不过这个大工厂里要生成的所有组件可能并非写死,而是通过配置文件来维护。

一个栗子

目前聚合的每个接口都需要使用这几个服务,数据库组件MysqL,模拟请求组件CURL,数据返回类EXPORT。

于是如果我们新开发一个查询手机品牌的 Api 的话,它的代码应该是类似这样的 (虽然目前这些多是通过全局变量引入的 - - ! ):

栗子
Class MobileBrand {
public function __construct(){
$this ->setter();
}
//注入
protected setter(){
->curl = new Curl();
->db = MysqL();
->export = Export();
}
//业务逻辑
run( $params ){
//查询本地缓存
if ( $data = ->db->query( )){
return ->export( );
} else {
//转发到数据源
->curl->get( $url , ));
}
}
}

随着时间的推移,后来我们又开发了彩票查询、汽车品牌查询、二手车价格查询一百多万个接口,开发过程中发现上面的 setter方法每次都要新写一遍,而且后来由于Curl 组件有问题,我们决定改为使用 Request 组件来发请求,这时候发现这一百多万个接口的代码都要改,于是我们决定用工厂模式改进一下上面的代码

栗子2
class ApiFactory {
setter(){
Request();
MysqL();
Export();
}
}
Class MobileBrand {
__construct(ApiFactory $services ->service = ;
}
可以发现上面的 Curl 已经被我偷偷换成了 Resquest ,而接口内部的代码不需要改变,而且以后替换组件不用再手动改一百多万个类了。


有一天

新来的小李同学发现新的接口需要使用 Postgresql,于是为 ApiFactory 新增了一个Postgresql 组件,并通过了本地的测试脚本,组件中有这一行:

栗子3

$post_config = ( ->config)[ 'post' ];

我们可以看到 $this-> config 应该是个数组,小李想取出这个数组的 post 域并存入 $post_config 这个中间变量。

因为小李本地的PHP版本是 7.0 ,上面的语法是支持的,但是生产环境的版本是 5.4,不支持以上的语法,于是报了Fatal error,然后一百多万个依赖 ApiFactory 的类都中止了服务。

经过几个小时紧张的排查,小李决定 git revert ,还好这次服务终于恢复了...

小李想了想,导致这次问题的根源应该是ApiFactory 所有服务都依赖于组件的具体实现,而且没有延迟加载(Lazy load),所以当一个组件挂了,整个服务工厂可能就都挂了。


将具体变为抽象

梳理一下目前的依赖关系: 业务类(话费充值接口) -> 服务工厂(ApiFactory) ->组件(CURL) ,可见最高层和最底层还是有传递的依赖关系。


如何解耦呢?

首先我们可以尽量隔离最高层(业务层)和最底层(组件)的关联,即最高层依赖于组件的抽象(比如接口),而非具体(实现)。

于是我们重新构建了新的 ApiFactoryNew,它只有两个方法,bind 方法将组件注入容器,make 方法从容器中取出指定的组件:

栗子4

<?PHP
ApiFactoryNew {
protected = [];
make( $abstract ){
(isset( ->services[ ])){
];
}
return null;
}
bind( $concrete ){
( is_object )) {
] = ;
}
}
Curl{
post(){
// post方式发送请求
get( ){
//get 方式发送请求
}
}
$ApiFactoryNew = ApiFactoryNew();
->bind( 'http' PHP plain" style="font-family:Consolas, Curl());
$result ->make( )->get( 'www.baidu.com' );

ApiFactoryNew 和 ApiFactory 并无本质区别,只是把之前保存在多个类属性中的组件挪了个地方( $this-> services 数组),但是如果我们改一下 bind ,再给 Curl 类加个接口(interface),你就会发现其中的不同:

栗子5

//判断 $concrete 是否实现了 $abstract 接口
$concrete instanceof ){
;
{
throw Exception( ->toString() . "并未实现接口" . );
}
}
}
}
interface HttpInterface {
post();
);
}
Curl implements HttpInterface{
post(){
// post方式发送请求
}
){
//get 方式发送请求
}
}
ApiFactoryNew();
'HttpInterface' Curl());
至此,开发者就不需要知道 http 服务的具体实现类到底是谁( curl_foo,curl_bar,curl_alibaba..),只需要知道它实现了 HttpInterface 接口 ,并且有 get 和 post 两个方法

只需要向服务容器提出要求( 我需要 'HttpInterface' 服务 )即可。

实现延迟加载

就算组件并未使用,每次 bind 还是会 new 一个组件的实例,因此而来带来的性能开销是无意义的。

我们希望实现延迟加载,即只有在 make 的时候,才会去 new 一个组件实例。

今时今日,PHP匿名函数已经很成熟,我们可以使用匿名函数的特性来实现延迟加载,保持 HttpInterface 和 Curl 不变,我们只需要再修改一下 ApiFactoryNew:

栗子6

= call_user_func(]);
//判断 $concrete 是否实现了 $abstract 接口
){
;
. "未实现" );
}
Exception( "无效的组件" );
}
){
//判断 $concrete 是否是匿名函数
instanceof Closure) {
;
{
//将 $concrete 封装成返回 (new $concrete) 的匿名函数
$closure ->getClosure( );
//instanceof 运算优先级较高
(! $closure Closure) {
"匿名函数包装错误" );
{
;
}
}
getClosure( ){
return () use ){
new ();
};
}
}
HttpInterface {
post();
);
}
HttpInterface{
post(){
// post方式发送请求
}
){
echo ;
//get 方式发送请求
}
}
ApiFactoryNew();
PHP variable" style="font-family:Consolas, 'Curl' );

实现单例的绑定

实现延迟绑定后带来了新的问题,每次make的时候都会重新执行一次当初绑定的匿名函数,重新new 一个实例.

为了实现一开始的单例绑定,我们需要拆分一下 bind()

为 ApiFactoryNew 添加 instance() 方法,每次 make() 的时候优先取 $this->instance 里面的服务:

7
/**
* 服务
* @var [type]
*/
= [];
/**
* 实例
* @var array
*/
$instances = [];
instance( -> [ }
){
->instances[ ])){
];
elseif ])){
]);
//判断 $concrete 是否实现了 $abstract 接口
){
;
}
);
}
);
//判断 $concrete 是否是匿名函数
Closure) {
;
{
//将 $concrete 封装成返回 (new $concrete) 的匿名函数
);
//instanceof 运算优先级较高
Closure) {
);
{
;
}
}
){
();
};
}

自动替代手动 ---- 反射

随着时间的推移,一百多年过去了,我们上线了 API 28.0 ,编写了很多有用的工具和服务,并且引入了大量的第三方的组件,如日志组件 monolog,测试组件 webmozant,配置文件管理组件 Vlucas ,还有各种队列和定时任务等等... ApiFactory 类变得越来越大..

终于有一天小李厌倦了到处 new 的生活,他想能不能通过配置文件来管理日益庞大的 ApiFactory?最好能顺便解决组件之间的互相依赖关系...

解决组件间的依赖关系

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐