以iOS实现Pipeline设计模式

前言

该文介绍Pipeline设计模式,使用场景,以及如何使用OC语言,用链式语法优雅的将Pipeline串联起来。

Pipeline设计

image

思想来源于责任链模式,如上图所示,有三个概念:

  • Port是产生数据对象;
  • Pipeline是处理数据对象的管道;
  • Packet是数据包,或者管道的上下文;

链路比较长的业务场景,大都可以套入这个模式;例如,

开播:
image
  1. 用户点击开播按钮产生一个点击事件,那么这个事件可以作为一个Port,用于产生点击事件
  2. Port产生数据后,会流向Pipeline,假如开播需要先检查该用户是否具备开播资格,那么检查具备开播资格的业务代码可以作为一个Pipeline
  3. 请求开播接口,以及接口返回后,需要判断是开播还是恢复直播等业务逻辑,可以作为一个Pipeline
  4. 判断当前开播的场景,初始化对应的组件,布局等

需求描述

分享模块,比如用户要分享一个文档链接,那么当用户点击分享的时候,得走过以下的链路节点:

  1. 检查是否已经登录
  2. 检查该文档是否可以生成链接
  3. 检查该用户是否有权限将一份文档生成链接
  4. 请求文档生成链接的接口,生成链接

优雅实现Pipeline设计模式

按照以上的需求描述,我们会创建以下几个类:

  1. ShareEventPort 产生用户点击分享事件的端口
  2. Packet 用于在Pipeline中传递的数据包,包含的信息有文档ID,事件来源,UserId,元数据,链接的信息
  3. CheckLoginPipeline 检查登录Pipeline
  4. CheckDocAttributePipeline 检查文档属性(检查文档是否可以生成链接)
  5. CheckUserAttributePipeline 检查用户的属性(检查用户是否有权限将一份文档生成链接)
  6. RequestDocLinkPipeline 请求文档生成链接的接口,生成链接

最后,使用PipelinePlumber(水管工)将Port和Pipeline串联起来,在最末端的throwPacketBlock接收数据。

- (void)setupPipeline
{
    // pipeline
    CheckLoginPipeline *checkLoginPipeline = [CheckLoginPipeline new];
    CheckDocAttributePipeline *checkDocAttributePipeline = [CheckDocAttributePipeline new];
    CheckUserAttributePipeline *checkUserAttributePipeline = [CheckUserAttributePipeline new];
    RequestDocLinkPipeline *requestDocLinkPipeline = [RequestDocLinkPipeline new];

    @weakify(self);
    self.plumber
    .addPort(self.shareEventPort)
    .addPipeline(checkLoginPipeline)
    .addPipeline(checkDocAttributePipeline)
    .addPipeline(checkUserAttributePipeline)
    .addPipeline(requestDocLinkPipeline)
    .throwPacketBlock = ^(id<Packet>  _Nonnull packet) {
        @strongify(self);
        self.generateLinkBlock(packet.linkString);
    };

}

复制代码

链式串联Pipeline工具介绍[PipelinePlumber]

ps;iOS开发交流技术群:欢迎你的加入,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长

在整套设计模式中,有三个非常重要的概念,分别是Port、Pipeline、Packet;我将他们抽象成三个协议。

抽象Packet特征

介绍

/// 数据协议
@protocol BasePacketProtocol <NSObject>

/// 端口号
@property (nonatomic, assign) NSInteger portNum;

@end
复制代码

Packet(数据包)肯定是由Port产生的,对于所有端口而言,具备的共同特征是“端口号”;所以Packet的数据协议中,只有一个portNum 属性,表示端口号。

用处:如上图一样,会有多个不同的Port,在Pipeline中可能需要判断Packet是哪个端口产生的数据,这时就需要用到端口号了。

使用

@protocol Packet <BasePacketProtocol>

@property (nonatomic, strong) NSDictionary *metaData;
@property (nonatomic, copy) NSString *docId;
@property (nonatomic, copy) NSString *entrance;
@property (nonatomic, copy) NSString *userId;

/// 这个信息在RequestDocLinkPipeline赋值
@property (nonatomic, copy) NSString *linkString;

@end
复制代码

当自己自定义一个Packet时,可以仿照上方代码,声明一个Packet协议继承BasePacketProtocol协议。这样Packet 即可表示Pipeline中的数据包(上下文)。

抽象Port特征

/// 端口协议
@protocol PortDelegate <NSObject>

@property (nonatomic, copy) void(^throwPacketBlock)(id packet, NSInteger portNum);

@end
复制代码
- (void)receiveShareEvent:(NSDictionary *)dic
{
    Packet *packet = [Packet new];
    packet.metaData = dic;
    packet.docId = dic[@"docId"];
    packet.userId = dic[@"userId"];
    packet.entrance = dic[@"entrance"];

    // 使用示例代码
    self.throwPacketBlock(packet, 100);
}

复制代码

对于端口,这里只抽象出了抛数据的接口throwPacketBlock ,当端口产生数据时,调用throwPacketBlock ,填入两个参数,分别是Packet和端口号,即可往Pipeline传递数据了。

抽象Pipeline特征

@protocol PipelineDelegate <NSObject>

- (void)receivePacket:(id)packet throwPacketBlock:(void(^)(id packet))block;

@end
复制代码
// 使用示例代码

#define PassNextPipeline(packet) !block ? : block(packet)
#define BlockInPipelibe(packet)

- (void)receivePacket:(id<Packet>)packet throwPacketBlock:(void(^)(id packet))block
{
    // packet是接收到由Port或者上个Pipeline传过来的数据包
    // 调用block,将数据流向下一个Pipeline
    if ([self checkDocCanGenerateLinkWithDocId:packet.docId]) {
        PassNextPipeline(packet);
    } else {
        BlockInPipelibe(packet);
    }
}

复制代码

对于Pipeline来说,特性显而易见,就是输入和输出。

核心角色:PipelinePlumber

PipelinePlumber是一个水管工,负责将Port和Pipeline串联起来,让数据可以流通。

接口

/// pipeline的水管工
@interface PipelinePlumber : NSObject

/// 添加端口
- (PipelinePlumber *(^)(id<PortDelegate>port))addPort;

/// 添加pipeline
- (PipelinePlumber *(^)(id<PipelineDelegate> pipeline))addPipeline;

/// 抛出经过一系列pipeline的数据
@property (nonatomic, strong) void(^throwPacketBlock)(id packet);

@end
复制代码

实现

@property (nonatomic, strong) NSMutableArray<id<PortDelegate>> *portArr;
@property (nonatomic, strong) NSMutableArray<id<PipelineDelegate>> *pipelineArr;
复制代码

addPortaddPipeline 都是往数组添加对象。

为啥在Port中调用self.throwPacketBlock(packet, 100);就可往Pipeline传输数据?

    @weakify(self);
    [self.portArr enumerateObjectsUsingBlock:^(id<PortDelegate>  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        @strongify(self);
        obj.throwPacketBlock = ^(id<BasePacketProtocol> packet, NSInteger portNum) {
            @strongify(self);
            packet.portNum = portNum;
            [self handlePacket:packet];
        };
    }];
复制代码

监听了每个Port对象的throwPacketBlock,所以当在Port类中,调用self.throwPacketBlock(packet, 100);时,PipelinePlumber 内就可监听到,并且处理Packet流向Pipeline

- (void)receivePacket:(id<Packet>)packet throwPacketBlock:(void(^)(id packet))block实现中,调用block就可以将packet传向下一个Pipeline是如何实现的?

@interface NSObject(PipelinePlumber)

@property (nonatomic, strong) id<PipelineDelegate> plumber_nextPipeline;

@end
复制代码

Pipeline对象会有一个plumber_nextPipeline属性,用于指向下一个Pipeline;在数据结构上,Pipeline是用单链表串联起来的,所以可以通过plumber_nextPipeline指针,从APipe->BPipe。

- (void)handlePacket:(id)packet
{
    [self recurPipeline:self.pipelineArr.firstObject packet:packet];
}

- (void)recurPipeline:(id<PipelineDelegate>)pipeline packet:(id)packet
{
    if (!pipeline)
    {
        return;
    }

    @weakify(self);
    [pipeline receivePacket:packet throwPacketBlock:^(id  _Nonnull throwPacket) {
        @strongify(self);

        NSObject *dunmy = (NSObject *)pipeline;
        if (dunmy.plumber_nextPipeline) {
            [self recurPipeline:dunmy.plumber_nextPipeline packet:throwPacket];
        } else {
            !self.throwPacketBlock?:self.throwPacketBlock(throwPacket);
        }
    }];
}
复制代码

当Port类中调用self.throwPacketBlock(packet, 100);,会触发handlePacket 方法,然后调用recurPipeline 方法进行递归,实现packet从Port->APipe->BPipe->throwPacketBlock.

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

推荐阅读更多精彩内容

  • 1.面对对象的六大设计原则 单一职责:一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个...
    编程怪才_凌雨画阅读 547评论 0 2
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,535评论 28 53
  • 信任包括信任自己和信任他人 很多时候,很多事情,失败、遗憾、错过,源于不自信,不信任他人 觉得自己做不成,别人做不...
    吴氵晃阅读 6,187评论 4 8
  • 怎么对待生活,它也会怎么对你 人都是哭着来到这个美丽的人间。每个人从来到尘寰到升入天堂,整个生命的历程都是一本书,...
    静静在等你阅读 4,973评论 1 6