依赖注入与面向接口编程

一、依赖注入(Dependency Injection)

今天我们讨论的内容核心是面向接口编程,我决定还是要从依赖注入开始讲起,因为DI的思想可以说是面向接口编程思想的特殊表现,也可以说是与面向接口编程相辅相成。

举个例子~
我们有一个公交车类(Bus),每天早上6点钟需要发车(work),为其分配对应的司机(Driver),看代码

<pre>
@implementation Bus

  • (void)work {
    Driver *driver = [[Driver alloc] initWithName:@"张三"];
    //dosomething
    }
    @end
    </pre>

上面这段代码中,Bus对象的运作需要用到Driver对象,因而创建了一个Driver对象,我们称Bus对Driver有一个依赖。这样的强耦合关系会因为日后的变化而给我们带来很多麻烦,不久将来张三师傅辞职了,我们需要修改Bus-work()的代码,也就是说在开发过程中非常不便于单元测试(一是不能方便地更换各种Driver对象,二是如果Driver这个职位创建是耗时操作或者高成本操作,我们并不能使用准备好的Driver实现快速重复测试)。

<pre>
@implementation Bus

@property (strong, nonatomic) Driver *driver;

  • (instancetype)initWithDriver:(Driver *)driver {
    self = [super init];
    if (self) {
    self.dirver = driver;
    }
    return self;
    }
  • (void)work {
    //dosomething
    }
    @end
    </pre>

以上这段代码我们通过init方法,为Bus对象传入了一个Driver对象,像这种非自己主动初始化依赖,而从外部通过注入点注入依赖的方式,我们就称为依赖注入,而例子中的这种注入的方法称之为构造器注入。明显的,这个场景中Bus和Driver的耦合因此轻了一层。说到解耦,并不是说Bus和Driver之间的依赖关系就不存在了,在Bus的范围内看来,只是将依赖建立从编译期间推迟到了运行期间,毕竟Bus无论如何也是需要Driver提供服务的。
“依赖就像是系统中的plugin,主系统并不强依赖于任何一个插件,但一旦插件被加载,主系统就应该可以准确调用适当插件的功能”。

类似这样的注入方式还有

  • 属性注入
  • 方法注入
  • 环境上下文注入
  • 子类重写方法注入等

不同的知识注入的手段,思想还是一样的。

二、开闭原则(Open-Closed Principle)

他的原文解释是Software entities should be open for extension,but closed for modification,对扩展开放,对修改关闭。也就是说我们对模块的设计,应该满足将来在不可修改源代码的情况下对模块的职能扩展,或者改变模块的行为。单单这句话就能表现出OCP可怕的地位,他迫使我们主动考虑了将来,使应用保证了核心代码的稳定性和对新需求的灵活性。

三、依赖获取(Dependency Locate)

上面我们理解了依赖注入的基础思想,让依赖显式化,为依赖提供合适的注入点(针口),提升程序的灵活性。带来的结果就是当我们需要更换依赖的时候不需要对使用服务的类(姑且叫作客户类)作代码修改,将提供服务的类(服务类)由注入点注入到客户类中,耦合的确轻了一层,也符合OCP原则,ok现在我们往外跳一层,在实例化客户类的角色上下文中,需要实例化服务类进而完成对客户类注入,服务类的更变必然导致此处代码的修改,这时OCP又要站出来打差评。
此时有必要讲下依赖获取。既然有注入,当然也应该有获取,但这两者并不是先后执行的两个过程,而是相同目的的同一种操作,换句话说,我们让客户类由被动注入转换成主动获取,继续贯彻的仍然是依赖注入思想。
DL就是在系统中配置一个获取点,客户类依赖于服务类的接口而不直接依赖服务类,客户类根据自身需要从获取点主动获取服务类为其提供服务。理解了DI,对DL的概念肯定是迎刃而解。
我们思考下,客户类只知道获取点,按照道上的规矩交货的对方的身份完全不需要去了解,有没有发现面向接口(POP)的内体又暴露了一点?

四、更高级的依赖注入

认识完DI的另一种方式依赖获取后,做依赖注入的办法就不仅仅局限于上文列举的几种最基本的依赖注入方式。目前比较主流的有配置文件依赖注入,反射依赖注入,例如java中强大的Spring和移植到.NET平台的Spring.NET,.NET中自己的Autofac,他们是结合配置文件和反射工作的,而oc中的objection我看了下是通过key-Value内存容器来做的DI,如果我自己做的话,还可以使用runtime target-action方式(类似于其他语言的反射)。

下面还是用一个简单的例子来增强对通过配置文件做依赖获取的认识:

<pre>
//定义一个主题接口,让所有主题都实现它
@protocol ItfThemeFactory

  • (void)drawing;
    @end

//主题
@implementation SpringFactory

  • (void)drawing {
    //drawing theme...
    }
    @end

@implementation SummerFactory

  • (void)drawing {
    //drawing theme...
    }
    @end

//主题工厂Animator
@interface ThemeFactoryAnimator : NSObject
@property (strong, nonatomic) id themeFactory;
@end
@implementation ThemeFactoryAnimator

  • (id)themeFactory {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"theme" ofType:@"plist"];
    NSDictionary *dict = [[NSDictionary alloc] initWithContentsOfFile:path];
    NSString *theme = [dict objectForKey:@"theme"];
    if ([theme isEqualToString:@"spring"]) {
    _themeFactory = [[SpringFactory alloc] init];
    } else if ([theme isEqualToString:@"summer"]) {
    _themeFactory = [[SummerFactory alloc] init];
    } else {
    //assert
    }
    }
    @end

//在执行方法里我们要做什么?

  • (void)work {
    ThemeFactoryAnimator *tfAnimator = [ThemeFactoryAnimator alloc] init];
    id themeFactory = tfAnimator.themeFactory;
    [themeFactory drawing];
    }
    </pre>

以上,我们只需要在执行方法(-work())中拿到themeFactory,对界面进行渲染即可,而原本有可能出现依赖的地方——ThemeFactoryAnimator已经不依赖于外部注入,而仅仅依赖于我的theme.plist配置文件,也可以说我们将多态封装到了这个“获取点”内,因此主题的改变映射到了配置文件中对应内容的改变,但是这个更换主题系统目前就利用DI变得符合OCP原则了吗?不是的,虽然依赖的改变已经映射到了客户类封装的外部——配置文件中,可是我们还是无法避免if-else结构的存在,我们可以不修改代码自由更换主题,可是如果又开发出了一套新的主题呢?这个系统对于未来还是无能为力,这一part的重点是依赖获取,至于怎么消除这种缺陷?看完这篇文章也许你就自然明白了。

五、面向接口编程(Protocol-oriented programming)

接口泛指实体把自己提供给外界的一种抽象化物,用以由内部操作分离出外部沟通方法,使其能被修改内部而不影响外界其他实体与其交互的方式

举个例子说明下:

<pre>
//首先我们定义一个交通工具接口
@protocol Transportation

  • (void)drinking;
  • (void)freight;
    @end
    //还有一个发光体接口
    @protocol Irradiative
  • (void)shine;
    @end
    //当然drinking就代表补需,汽车飞机的内部实现就是加油,马牛的内部实现就是吃草喝水什么的。freight就是装载

//当上帝创造马的时候,让马遵守并实现这个接口:
@implementation Horse

  • (void)drinking {
    //吃草,喝果汁
    }

  • (void)freight {
    //停住脚步,或者半蹲,让友好的人类骑上去
    }
    @end
    //当人类创造飞机的时候,惨了,不知道去哪里找上帝沟通,又怕疏漏了什么影响了这个世界的运行规律?没事上帝给我们留下了Transportation接口,而且飞机同时还要遵守发光体接口Irradiative,于是:
    @implementation Aircraft

  • (void)drinking {
    //加油, 92的
    }

  • (void)freight {
    //降落,熄火,开舱门
    }

  • (void)shine {
    //燃烧汽油,生物质能转化成电能,照你
    }
    @end
    </pre>

六、面向接口编程(架构)
无论是哪种架构方式,层次关系肯定是撇不开的,并且层次关系也代表着一种架构的主心骨,无论业务分层,功能分层,还是角色分层,存在于各个位置的依赖关系都需要我们去正视,而POP的目的正是为了化解这些强依赖,打破上层实例化下层去为其提供服务的强耦合,在大型项目中,一层的变化可能会联动1+N层,这样的变化是致命的,正如上文我们提到过的,让一个实体由依赖另一个实体,转变成依赖一个接口,将被依赖实体的变化隔绝于接口之外。

七、iOS面向协议的编程思想
其实讲了这么多,大家或许已经发现,在我们日常OC编码中似乎随处可见接口编程的痕迹?——侵蚀了我们项目各个模块的代理模式,代理模式的工作原理就是,一方使用protocol(接口)划定一个或一组规则,成为其代理的角色必须遵守这一系列规则,最后根据规则去办事,好处依然是那么明显,主体并不需要与代理沟通,代理也不需要做多余的培训,直接上岗,从这里又强化了一遍接口即一种由内部操作分离出外部沟通方法,而核心就是一系列规则,通过接口工作,比直接访问属性或者方法稳健得多。

举个例子说明~

<pre>

pragma mark - 面向对象传统的方式:

//服务实现者 甲方 ,编写一个服务类
@interface MusicLoadingProtocolObj()
@end
@implementation MusicLoadingProtocolObj

  • (void)requestWithUrl:(NSURL *)url Param:(NSDictionary *)param {
    //do something
    }
    @end

//服务使用者 乙方 ,通过接口获取服务类

import "MusicLoadingProtocolObj.h"

@interface Client()
@end
@implementation Client

  • (void)work {
    MusicLoadingProtocolObj *musicLoadingProtocolObj = [MusicLoadingProtocolObj alloc] init];
    [musicLoadingProtocolObj requestWithUrl:url Param:param];
    }
    @end
    //当然,在这里我们已经应用了构造器注入的DI思想。或者我们如果使用属性注入?那么当然就没那么直观,没有贯彻接口编程的思想。

pragma mark - 接下来就是面向接口(POP)的做法:

//首先,定义一个ServiceProtocol
@protocol MusicLoadingProtocol

  • (void)requestWithUrl:(NSURL *)url Param:(NSDictionary *)param;
    @end

//甲方
@interface MusicLoadingProtocolObj()
@end
@implementation MusicLoadingProtocolObj

  • (void)requestWithUrl:(NSURL *)url Param:(NSDictionary *)param {
    //do something
    }
    @end

//乙方
@interface Client()
@end
@implementation Client

  • (void)work {
    id service = [[JSObjection defaultInjector] getObject:@protocol(MusicLoadingProtocol)];
    [service requestWithUrl:url Param:param];
    }
    @end
    </pre>

上例中笔者借助了OC的一个轻量级的DI框架objection,服务实现者甲方独立编写服务实现,而后将服务通过objection绑定到protocol之上,去看看服务使用者,乙方利用objection通过protocol拿到服务类实例,根据protocol中定义的规则,马上就实现了服务。不需要import,不需要实例化,高度解耦,并且符合OCP原则。objection的原理就是上文提到的key-value内存映射表,对于大型项目,多小组分项目开发再合并的生产线,POP是必不可少的。

如果说我们在轻型开发中不想使用框架,我们也可以谈谈自己实现POP+DI,利用起OC的利器——runtime。其实在上例已经埋下伏笔,这次我们的乙方可以这样做:

<pre>
//乙方
@interface Client()
@end
@implementation Client

  • (void)work {
    NSString *clazzName = @"MusicLoadingProtocol";
    [clazzName stringByAppendingString:@"Obj"];
    Class serviceClazz = NSClassFromString(clazzName);
    id service = [serviceClazz alloc] init];
    [service requestWithUrl:url Param:param];
    }
    @end
    </pre>

就是这样,甲乙双发约定了以接口名+Obj字符串的规则去定义服务类,乙方做DL时只需要配合runtime,也是轻而易举。

那么如果服务类实例化需要参数呢?

配置文件能解决这个问题,上文有提到Spring框架做DI的原理就是反射+xml,一般来说大部分支持反射机制语言的DI框架原理都是相似的,这里说下笔者了解的两种主流注入原理,构造器注入和属性注入,记得上文也提到过着两种注入方式,笔者强调过那只是一种思想,不是定性的一种方法,ok来看下那些DI框架是怎样做的。

  • 构造器注入
    在进行依赖获取的时候,DI框架通过反射机制得到待创建类的构造方法,然后根据构造器所需参数的类型或者顺序,在DI容器节点中寻找,然后提供参数,创建实例。

  • 属性注入
    同样的,在进行DL时,通过反射得到待创建类型的所有属性,然后根据属性在DI容器节点中进行匹配,有则创建提供,无则跳过。

八、最后

  • 依赖注入+接口编程
  • 调用者无须关心对象任何实现,只需按照接口规则调用服务
  • 在系统分析和架构中,分清层次和依赖关系,每个层次不是直接向其上层提供服务(即不是直接实例化在上层中),而是通过定义一组接口,仅向上层暴露其接口功能,上层对于下层仅仅是接口依赖,而不依赖具体类。
  • 服务使用端由对对象的依赖转变成对接口的依赖,这样甚至可以在服务提供对象还未存在之前编码(分子项目开发)

依赖注入只是一种思想,其实也就是一个过程,依赖注入用到了面向接口的编程思想,面向接口的架构实现用到了依赖注入的执行方式。而面向接口编程和面向对象编程并不是平级的,它并不是比面向对象编程更先进的一种独立的编程思想,而是附属于面向对象思想体系,属于其中一部分。或者说,它是面向对象编程体系中的思想精髓之一。

九、OC中面向协议编程相关文章

《面向协议编程与 Cocoa 的邂逅》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • 依赖注入(Dependency Injection) 今天我们讨论的内容核心是面向接口编程,我决定还是要从依赖注入...
    zhiyi阅读 14,033评论 8 79
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,577评论 18 399
  • 微软成功抢了苹果的风头,是的,巨额收购领英可以说是目前来说业内最大的一次收购了。 在Msn马上退出市场之际,这场收...
    耿彪阅读 224评论 0 1
  • 默默喜欢你很累很累了,不想坚持了,想说再见了,不管别人眼里你多不好,可是在我眼里你就是你,每次决定放弃你的时候,你...
    情意暖暖阅读 83评论 0 0