iOS面向接口编程(面向protocol编程)

iOS面向接口编程(面向protocol编程)

前言

面向接口编程已经是老生常谈了,但是最近在做项目重构中发现,团队对面向接口编程的理解还停留在纸面,并没有进行实践。网上也没有简单易懂的解释iOS(objective-c)的面向接口编程的文章,特此整理以下。

什么是面向接口编程

本文不深入解释什么是面向接口编程,如有需要烦请自行百度&Google。摘抄一个网上的解释

    我们在一般实现一个系统的时候,通常是将定义与实现合为一体,不加分离的,
    我认为最为理想的系统设计规范应是所有的定义与实现分离,尽管这可能对系统中的某些情况有点麻烦。

iOS实现面向接口编程

先说实现,后面再解释原理。我们将设计分为:底层实现、接口层、上层调用,三个部分。

1.接口层

接口层是上下层的粘合剂,实现上下层的分离。

  • <b>接口层通过protocol声明底层需要实现的接口,同时也是上层可以调用的接口。</b>
@protocol PrintServerProtocol <NSObject>

- (void)printHello;
- (void)world;

@end

接口层通过单例持有底层服务

@interface ServerBridge()
@property (nonatomic, strong) id<PrintServerProtocol> printer;
+ (instancetype)shareInstance;
@end

@implementation ServerBridge
+ (instancetype)shareInstance {
    static id instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}
@end
  • <b>底层需要将自己的功能注入给接口层。</b>
extern void setPrintServer(id printer);

void setPrintServer(id printer) {
    [ServerBridge shareInstance].printer = printer;
}


  • <b>上层通过指定的入口调用下层的接口</b>
#define Printer printServer()
extern id printServer(void);

id printServer(void)
{
    return [ServerBridge shareInstance].printer;
}

2.底层

<b>底层需要遵循接口层协议,并实现protocol的声明的方法</b>


@interface PrintServer()<PrintServerProtocol>

@end

@implementation PrintServer

- (void)printHello {
    printf("%s - %s\n",NSStringFromClass([self class]).UTF8String, NSStringFromSelector(_cmd).UTF8String);
}

- (void)world {
    printf("%s - %s\n",NSStringFromClass([self class]).UTF8String, NSStringFromSelector(_cmd).UTF8String);
}

@end

<b>底层的服务需要注入到接口层,可以是底层setup时注入,也可以是二方的其他合适的位置注入</b>

@implementation ServerSetup

+ (void)load {
    setPrintServer([PrintServer new]);
    addLogServer([LogServer new]);
    addLogServer([LogServerNew new]);
}

@end

3.上层使用

上层只依赖接口层,通过接口层调用底层方法

    [Printer printHello];
    [Printer world];

面向接口编程的扩展

我们可以将底层SDK看做是接口功能服务提供方,上层是接口功能服务的使用方。当前的实现,是服务提供方有1个,服务使用方没有限制,可以任意使用。是一个1对多的模型。在这个基础上,我们还可以扩展出多种形式的1对多的模型。

多个底层接口提供方,按需调用其中1个

第一次扩展,我们希望,能够有多个底层接口提供方,上层业务使用时,可以按需调动。这一模型,很容易就想到,在接口层添加一个可变数组,通过维护数组实现相应的功能。

  • <b>接口层</b>

@interface ServerBridge()
@property (nonatomic, strong) NSMutableArray *logerServers;
@property (nonatomic, strong) id<LogServerProtocol> loger;

+ (instancetype)shareInstance;
- (void)addLogerServer:(id<LogServerProtocol>)player;

@end

@implementation ServerBridge

+ (instancetype)shareInstance {
    static id instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

#pragma mark loger
- (NSMutableArray *)logerServers {
    if (!_logerServers) {
        _logerServers = [NSMutableArray array];
    }
    return _logerServers;
}

- (instancetype)loger {
    // 针对多个server,根据条件取得合适的server
    id targetClass;
    if (arc4random()%2 == 0) {
        targetClass = [LogServer class];
    } else {
        targetClass = [LogServerNew class];
    }
    
    for (id loger in self.logerServers) {
        if ([loger isKindOfClass:[targetClass class]]) {
            return loger;
        }
    }
    return nil;
}

- (void)addLogerServer:(id<LogServerProtocol>)player {
    @synchronized(self) {
        if ( [player conformsToProtocol:@protocol(LogServerProtocol)] ) {
            [self.logerServers addObject:player];
        }
    }
}

@end

即,我们通过维护一个内部可变数组的方式,用随机值模拟实际的条件(如根据网络状况选择服务:lowNetQuality时选serverA,highNetQuality时选serverB)。

需要注意的是,Foundation框架下的NSMutable不是线程安全的,即便property中声明了atomic。当然,上述demo代码也只对setter做了简单的线程保护,并不能完全保证线程安全。

  • <b>底层</b>

底层实现同上,setup中也仅仅需要进行add操作

  • <b>上层</b>

上层调用同上,可以通过block或其他形式,将选择server的条件暴露给上层。需要注意的是,这可能会以另一种形式将上下层再次耦合起来。

多个底层接口提供方,顺序调用全部

这个方案实现起来相对复杂。一种可行的思路是,在接口层进行消息转发,在转发接口时,从servers数组中依次取出server,并进行转发。

底层接口暴露及封装

当底层某个接口需要向上层暴露时,只需要将该接口的定义在protocol中声明即可。

// 内部接口
@implementation PrintServer
- (void)innerPrintHello {
    printf("%s - %s\n",NSStringFromClass([self class]).UTF8String, NSStringFromSelector(_cmd).UTF8String);
}
@end

// 对外暴露
@protocol PrintServerProtocol <NSObject>
- (void)innerPrintHello;
@end

如果有重命名的需要,或者内部方法前后进行额外处理,则需要进行额外的封装如:

// 底层接口为:
- (void)printWorld;

// 希望暴露给上层的接口为:
- (void)world;

// 则需要进行内部的转发
- (void)world {
    // do something befor
    [self printWorld];
    // do something after
}

iOS面向接口实现原理

首先解释一个误区,很多iOS开发工程师认为protocol就是传递方法调用的(委托&代理),只有dataSource,delegate两种用法。这种理解很狭隘,会限制protocol使用的想象。

protocol只是声明了一组接口,遵从协议的一方需要对应实现,此外没有更多了。委托,代理,面向接口都是protocol的一种使用方式。

根据这种解释,面向接口编程就很好理解了。接口层声明了一组protocol,底层声明遵从协议,并实现相应方法。上层调用底层的实例/类,就可以直接调用对应的接口了。中间的接口层就是一层粘合剂,把下层的实例/类注入进来,上层拿注入好的实例/类调用对应方法即可。

delegate其实理解起来反倒更难一点。因为delegate的实现,和本文讲的接口分离,刚好是反着的。以我们通常使用的UITableViewDelegate为例:

<b>接口层</b>

@protocol UITableViewDelegate<NSObject, UIScrollViewDelegate>
// 接口方法······
@end

<b>底层</b>

// 服务注入
tableView.delegate = self;

// 接口实现
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    // 具体实现
}

<b>上层调用</b>

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    // ···
    if ( [self.delegate respondsToSelector:@selector(tableView:didDeselectRowAtIndexPath:)] ) {
        [self.delegate tableView:tableView didDeselectRowAtIndexPath:indexPath];
    }
    // ···
}

可以很明显的看到,我们平时代码里写的UITableViewDelegate的接口的实现,刚好对应面向接口编程中的底层服务。

本文Demo代码

代码

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

推荐阅读更多精彩内容

  • 依赖注入(Dependency Injection) 今天我们讨论的内容核心是面向接口编程,我决定还是要从依赖注入...
    zhiyi阅读 14,048评论 8 79
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,221评论 11 349
  • 十一黄金周,本来就是一个让人激动的节日,可是在我十一岁的这个黄金周,李老师决定领我们去西安进行背包课程。 在十月二...
    孙语湘阅读 263评论 1 3
  • 走一个人的路,会很孤寂。 走很多人的路,会很无趣。 虽说花开两朵,各表一枝。 但路途遥远,我们一起走吧! 我们战战...
    念你有时阅读 149评论 2 3