接口编程那些事

By | 2015年12月8日

转载请注明出处:http://www.olinone.com/

接口是一系列可调用方法的集合。何为接口编程?接口编程是指当写一个函数或一个方法时,我们应该更加关注具体的接口,而不是实现类。具体理解可以参考这篇文章

在OC中,接口又可以理解为Protocol,面向接口编程又可以理解为面向Protocol编程,或者面向协议编程。在Swift中,苹果大幅强化了 Protocol 在这门语言中的地位,整个 Swift 标准库也是基于 Protocol 来设计的,有兴趣的童鞋可以看看这篇文章。面向接口编程正逐步成为程序开发的主流思想

在实际开发中,大多数朋友都比较熟悉对象编程,比如,使用ASIHttpRequest执行网络请求

request是请求对象,当发起请求时,调用者需要知道给对象赋哪些属性或者调用对象哪些方法。然而,使用AFNetworking请求方式却不尽相同

同是请求对象,使用AFNetworking发起请求时,调用者可以不需要关心它有哪些属性,只有接口无法满足需求时才需要了解相关属性的定义。两种设计思路完全不同,当然,此处并不是想表明孰优孰劣,只是想通过两种截然不同的请求方式引出本文的思想——面向接口编程(或者面向协议编程)

接口比属性直观

在对象编程中,定义一个对象时,往往需要为其定义各种属性。比如,ReactiveCocoa中RACSubscriber对象定义如下

参考AFNetworking的思想,以接口的形式提供访问入口

通过接口的定义,调用者可以忽略对象的属性,聚焦于其提供的接口和功能上。程序猿在首次接触陌生的某个对象时,接口往往比属性更加直观明了,抽象接口往往比定义属性更能描述想做的事情

接口依赖

设计一个APIService对象

正常发起Service请求时,调用者需要直接依赖该对象,起不到解耦的目的。当业务变动需要重构该对象时,所有引用该对象的地方都需要改动。如何做到既能满足业务又能兼容变化?抽象接口也许是一个不错的选择,以接口依赖的方式取代对象依赖,改造代码如下

通过接口的定义,调用者可以不再关心ApiService对象,也无需了解其有哪些属性。即使需要重构替换新的对象,调用逻辑也不受任何影响。调用接口往往比访问对象属性更加稳定可靠

抽象对象

定义ApiServiceProtocol可以隐藏ApiService对象,但是受限于ApiService对象的存在,业务需求发生变化时,仍然需要修改ApiService逻辑代码。如何实现在不修改已有ApiService业务代码的条件下满足新的业务需求?

参考Swift抽象协议的设计理念,可以使用Protocol抽象对象,毕竟调用者也不关心具体实现类。Protocol可以定义方法,可是属性的问题怎么解决?此时,装饰器模式也许正好可以解决该问题,让我们试着继续改造ApiService

经过Protocol的改造,ApiService对象化身为ApiService接口,其不再依赖于任何对象,做到了真正的接口依赖取代对象依赖,具有更强的业务兼容性

定义一个Get请求对象

请求代码也随之改变

对象可以继承对象,Protocol也可以继承Protocol,并且可以继承多个Protocol,Protocol具有更强的灵活性。某一天,业务需求变更需要用到新的Post请求时,可以不用修改 GetApiService一行代码,定义一个新的 PostApiService实现Post请求即可,避免了对象里面出现过多的if-else代码,也保证了代码的整洁性

依赖注入

文章写到这里,细心的童鞋可能已经发现问题——GetApiService依然是以对象依赖的形式存在。如何解决这个问题?没错,那就是依赖注入!

依赖注入是什么?借用博客里面的一句话,其最大的特点就是:帮助我们开发出松散耦合、可维护、可测试的代码和程序。这条原则的做法是大家熟知的面向接口,或者说是面向抽象编程。 objc上这篇文章介绍的不错, 有兴趣的童鞋也可以看看,在此就不再累述

基于依赖注入objection开源库的基础上继续改造ApiService

调用者关心请求接口,实现者关心需要实现的接口,各司其职,互不干涉

接口和实现分离的设计适用于团队协作开发,实现了系统的松散耦合,便于以后升级扩展。当然,接口编程对开发人员的要求也比较高,需要提前定义好接口,接口一变,全部乱套,这就是所谓的设计比实现难,但是设计接口的人工资都高啊!!!

2015/12/20,许多朋友希望出个demo,特此补上。另阿里音乐正在招聘iOS高级开发工程师,有兴趣的朋友可以微博或者网页下面跟我留言内推哦!


后记:你可以在github找到我,也可以通过微博联系我,感谢你的来访!

 

42 thoughts on “接口编程那些事

    1. 庞海礁 Post author

      谢谢你的建议,其实是想写个的,时间仓促,以后补上

      Reply
  1. 匿名

    你好,能否提供一个demo来参考下,由于设计模拟不是太熟悉,在改造为post的时候需要具体怎么处理呢。

    Reply
  2. 杨兵

    hi,如果业务升级的话,就算接口不变,扩展性我觉得挺好的,
    原有的业务逻辑我觉得还是要改啊,
    目前还没遇到过面向接口的设计方案,确实没有感受到好处与不足,
    文章还好,简单,易入门.
    谢谢

    Reply
    1. 庞海礁 Post author

      业务升级,只要接口不变,定义一个新的业务对象实现协议即可,就好比定义一个新的PostApiService对象一样,原GetApiService可以不用修改一行代码,也没有所谓的父类继承或者各种判断属性的定义

      Reply
      1. 沈源

        楼主你好,看了你的博客 还是有个疑问 就是传入url和param参数后 怎么获得网络请求的回调内容 在vc里调用 是用代理返回吗

        Reply
    1. 庞海礁 Post author

      简单理解为接口替换对象,将具体对象隐藏起来,以接口的方式对外提供访问入口,不直接依赖具体对象

      Reply
  3. lok.mm

    最后一个的- (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param方法可以改为类方法 名字加上GET嘛,以后的网络获取只用这一个吗

    Reply
    1. 庞海礁 Post author

      改成类方法的想法不错,可以考虑下。名字加不加上get不重要,这只是一个demo,不过为每一个接口取一个好名字的想法值得倡导,加油哦!

      Reply
  4. 刘力硕

    你好,请问最后用注入的意义在哪里? 我觉得并没有实现解耦吧,我们还是要知道具体的类,导入头文件才行,[[JSObjection createInjector] getObject:[GetApiService class]]; 这个是不是要换成用协议来取对象才真正解耦了?初次接触这方面的东西,请指点一下哈。

    Reply
    1. 庞海礁 Post author

      其实你能提出这个问题说明你已经懂得了依赖注入,当时写demo时为了图简单,就直接通过[GetApiService class]获取该对象,从而误导了大家。正常情况下,此处我们只需要获取到遵从该协议的对象就行了,所以可以使用Objection库的protocol的方式获取GetApiService对象,你可以尝试下

      Reply
  5. liulishuo

    你好,请问最后为了解耦的注入有什么作用?我觉得并没有实现解耦,因为还是要导入具体类的头文件,然后用[[JSObjection createInjector] getObject:[GetApiService class]] 取出来,这样和自己直接new没有区别吧?是不是应该用协议来取,而不是用class来取会好一点?初次接触这方面的知识,请多指教哈~

    Reply
    1. 庞海礁 Post author

      对,这个最好用协议来取,赞一个,当初偷懒把大家给误导了

      Reply
  6. Joe

    您好,您的文章写的非常棒,但是我有个疑问,就是在最后调用的地方
    @implementation NSObject (ApiServiceProtocol)
    – (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param {
    id apiSrevice = [GetApiService new];
    ApiServicePassthrough *apiServicePassthrough = [[ApiServicePassthrough alloc] initWithApiService:apiSrevice];
    apiServicePassthrough.url = url;
    apiServicePassthrough.param = param;
    [apiServicePassthrough execNetRequest];
    }
    @end
    这里为什么还需要使用ApiServicePassthrough将apiService包一层,而不是直接这样做:
    – (void)requestNetWithUrl:(NSURL *)url Param:(NSDictionary *)param {
    id apiSrevice = [GetApiService new];
    // ApiServicePassthrough *apiServicePassthrough = [[ApiServicePassthrough alloc] initWithApiService:apiSrevice];
    // apiServicePassthrough.url = url;
    // apiServicePassthrough.param = param;
    // [apiServicePassthrough execNetRequest];
    [apiService requestNetWithUrl:self.url Param:self.param];
    }

    Reply
    1. 庞海礁 Post author

      这个文中其实已经说过了,apiservice是实体对象,ApiServicePassthrough是它的一个抽象体,因为我们拒绝直接依赖。其次,抽象接口后可以适配不同的实现对象,以后有新的解决方案时,可以做到零切换成本,上层业务逻辑不需要修改任何代码,其永远只关心装饰器接口那层,做到上层和底层的业务抽离

      Reply
  7. Pingback: iOS开发见闻-第15期 - IT大道

  8. 匿名

    @protocol ApiService
    这一个接口的继承是多余的吧,直接使用ApiServiceProtocol就可以了啊

    Reply
    1. 庞海礁 Post author

      没记错的话,当时写ApiService是为了支持更多功能用的吧,忘记了

      Reply
  9. 小鸡啄蘑菇

    看了文章,也clone了demo,但还是无法理解跟体会其中的思想跟好处。。。关于DI,看了楼主添加的外链文章,天马行空,完全看不懂。。好郁闷啊博主 -. –

    Reply
    1. 庞海礁 Post author

      刚接触可能有点陌生,需要慢慢体会吧,多看看多想想,不能急的

      Reply
  10. Pingback: 谈谈依赖注入与面向接口编程思想 | 神刀安全网

  11. chaos

    装饰器模式在ReactiveCocoa里面有很深刻的使用,在ApiServicePassthrough的头文件中暴露url和param没必要,外部并不需要获取这两个参数,通过init方法参数传就好了。最后依赖注入那块,博主的Demo是难以实现的,接口和实现接口的主体应该是唯一绑定的,而Demo中GetApiService和PostApiService实现的是同一个协议。

    Reply
    1. 庞海礁 Post author

      ApiServicePassthrough中的url和param相当于属性角色,是允许修改和赋值的,通过init初始化当然可以,就是失去了作为属性的职责。关于依赖注入这个,一个接口可以有多个实现,就看你提前注入啥了!

      Reply
  12. Pingback: 谈谈依赖注入与面向接口编程 – 项目经验积累与分享

  13. Pingback: iOS开发见闻-第15期 – 项目经验积累与分享

  14. 沈源

    请问 在传入url和param和两个参数后 网络请求回调的内容 在VC里怎么获得 是在协议里传入返回值吗

    Reply
  15. 沈源

    楼主你好,看了你的博客 还是有个疑问 就是传入url和param参数后 怎么获得网络请求的回调内容 在vc里调用 是用代理返回吗

    Reply
  16. 匿名

    楼主你好,看了你的博客 还是有个疑问 就是传入url和param参数后 怎么获得网络请求的回调内容 在vc里调用 是用代理返回吗

    Reply

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注