接口编程那些事

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

回复 沈源 取消回复

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