iOS 组件实现方案(Block)

组件概述

一、什么才是好架构

  1. 代码整齐,分类明确,没有 Common,没有 Core
  2. 不用文档,或很少文档,就能让业务方上手;
  3. 思路和方法要统一,尽量不要多元;
  4. 没有横向依赖;
  5. 对上层业务方有适当的限制,也给业务方创造灵活实现的条件;
  6. 易测试,易拓展,超前性;
  7. 接口少,接口参数少,高内聚;
  8. 高性能;

二、为什么要组件

1, 我们都知道最基本的代码设计原则:“Don’t repeat yourself!”,每一个工程都会有自己的架构,即使是刚入门的开发者,写几天代码也会发现要把一些常用的重复代码单独拿出来放在一个叫 Common 的地方,实现代码复用。

说到 App 架构,大概分三种:

  1. 第一种是 APP 开发并不需要什么狗P架构;

  2. 第二种是有自己 NB 的架构;

  3. 第三种是要模块化够好,每个模块应该有自己的架构。

第一种是一些个人开发者,个人能力很强,经常一个人很快搞出来一个APP,他的印象中不需要弄太多的框框框住自己,但是其实他也是有一套自己的架构。

第二种是一些公司或者大公司,有一套NB的架构对于团队的意义就比较大了,可以保证稳定迭代,保证规范和持久可维护性。

第三种是BAT这样的有很多业务的超级公司,或者一些先进的开源开发者们,模块化能够更好的实现跨app的代码和功能的复用, 能够更好的共享资源,避免重复造轮子。

为什么要模块化,已经很明显了。

三、组件设计的优点

  1. 解决代码和业务的耦合问题,实现代码的复用度,还可以实现真正的功能复用,比如同样的功能模块如果实现了自完备性,可以在多个app中复用;
  2. 组件独立运行,代码自用率高,迭代效率高;
  3. 拆分粒度小,业务隔离且业务并行开发,跨团队开发代码控制和版本风险控制的实现;
  4. 模块化对代码的封装性、合理性都有一定的要求,提升开发人员架构能力;
  5. 项目架构清晰,可持续扩充业务,明确团队开发的业务边界,增加团队协作效率;

<span id="mark1">四、组件设计三大原则</span>

解耦且独立原则:每个组件只做好一件事情,不要让 Common 出现,只依懒下层组件并能够独立于同级组件进行独立编译,同层组件不允许出现直接相互依懒或引用,即横向依懒。

稳定自完备性原则:越底层的组件,应该越稳定,越抽象,越具有高复用度。最直观表现就是API很久都不用变化,所有的变化因子不会暴露出来,避免传递给依赖它的模块,那么设计的时候就越抽象,需要抽象总结的能力,一般表现在底层组件的设计上。自完备性表现为如果 A B 组件都需要调用一个函数 FF 不太适合做成组件的时候,就应该在 A B 组件里都实现 F 功能,提升组件的复用度,自完备性有时候要优于代码复用,而更多时候也是为下沉做准备。

下沉且向下依赖原则:按照架构的层数从上到下依赖,不要出现下层模块依赖上层模块的现象,业务模块之间也绝对不耦合。随着业务增加,会发现新组件里的某些功能能从旧组件里直接复用,那么这时候下沉变得尤突出,下沉会让上层业务轻量和稳定的效果。

组件实现

一、架构演进

1、组件化之前各功能组件间的关系

在项目初期,一般为了能快速完成1.0需求功能上线,以及项目的业务模式、业务类型还没成型,组件化并没有那么迫切,不足矣去抽离一个组件式架构开发模式。随着某些业务的慢慢稳定及产品的一般形态确定下来后,一般在B轮融资或一年期的项目,就会考虑怎样粒度化,把一个大的项目拆成小的模块,技术上体现为可以在其自身内部进行功能开发,并且其自身是一个自完备的功能。而这个粒度化,就是组件的演进过程。如果不进行组件化就会出现下图所示业务功能的相互耦合。

下文中的 Req 代表 Request, Pro 代表 Provide

组件实现之前相互耦合的业务功能模块:

image

2、组件化之后各功能组件间的关系

image

相比组件之前的关系图,组件之后的关系图第一感觉似乎变得更复杂了,没错,因为各功能组件都新增加了 APIRouter 两个东西,且依懒关系引用也似乎变得更缭绕。其实,组件化是会新增更多的中间层或协调器之类的东西,只有靠这些东西的独立声明和独立实现才得以实现组件化,而上图中 APIRouter 就是所谓的中间层。连接各组件之间的通信和调用关系。简化后的关系图如下:

image

上图是组件后的关系图,跟组件前的关系图对比,很明显各组件间不会有相互引用关系,所有引用通过一个叫 Middleware 的中间件进行连接。

3、引申出的架构全貌

组件化之前,第一要考虑的是分层,分层是根据依赖关系进行分层的,上层依赖下层,而分层是架构设计的一部分。下图是 iOS 项目的架构全貌:

image
a、五层架构概念
  1. System:系统层,所有系统为上层开发者提供的系统库和框架;
  2. Third:第三方层,项目都会依懒一些成熟稳定的第三方库进行开发,这一层为上层开发提供了很多系统不提供的功能,大大减少开发者的工作量;
  3. Trunk:主干层,即多端复用层,就是针对公司自已业务而封装的一层,这一层可以跨公司内部 APP 复用,是集公司技术精髓的一层;
  4. Base:真正的 APP 基础层,这一层只为该 APP 独有的功能特性而封装的基础层,为该 APP 上层业务提供底层支持或功能复用;
  5. Business:业务层,真正业务方所能触达的地方,所有业务上的逻辑开发,都在这一层进行。
b、技术和业务的分层

五层架构概念里业务层之下的都称为技术层。

  1. Technology Layer:技术层,该层只为业务层提供业务支持,该层不存在业务型代码,较业务层是一个个抽象的接口;
  2. Business Layer:业务层,在技术层的基本上,该层开发变得轻而易举,该只关注业务,不关注底层实现,比如支付的实现,网络请求是怎么得到数据的等等;
  3. 技术层和业务层通过 Middleware 中间件进行间接引用,不直接引用技术层。
c、多端复用

当一个公司有多个产品的时候,多端复用变得尤为重要,因为多个产品往往之间存在很多共性,那么在五层概念的第三层 Trunk 处再架设属于 APP2、APP3、... 的基础层为各 APP 上层业务服务。如图中与 MCUtilities 同层的 MCMUtilities

二、在 iOS 上的业务组件化具体实现

架构演进里说到的都是由下往上层的讲解,在具体开发中,或具体为某一业务抽离组件化的流程里,则是由上往下的思路。这里会有更多的具体实现代码展示,将使用 MCShop 组件进行讲解:

1、组件的 API

组件 API 是属于中间件 Middleware 的一部分,用于 对接外部实现对外提供实现 的重要桥梁,API 的设计好坏,极大地决定着组件的 解耦且独立原则,也最考验开发者组件设计能力,因为内部淋漓尽致的使用了各种回调<即函数指针>和传参,对这些指针参数的命名,需要极精准的定义,否则会让组件 API 难以理解。

组件架构模式下开发,时时刻刻都应该有 组件设计三大原则 的思想。API 的设计亦如此,我们以 MCShopAPIMCShop.h 为例进行讲解:

  1. 满足 下沉且向下依赖原则MCShop 是向下依赖于 MCUtilities 进行开发,所以 API 头里只引入 MCUtilities

    #import "MCUtilities.h"
    
    @interface MCShop : NSObject
    
    + (instancetype)sharedShop;
    ...
    
  2. 我们为每个组件创建了一组 API,这组 API 定义了该组件所有 需要的外部实现声明<Require> 以及能 对外提供的内部实现<Provide> 的所有清单。

    #import "MCUtilities.h"
    
    @interface MCShop : NSObject
    
    + (instancetype)sharedShop;
    
    #pragma mark require
    
    // 活动详情
    @property (nonatomic, copy) MCBaseVC *(^require_eventDetailVC)(NSString *eventId, MCEventType eventType);
    
    // 买单
    @property (nonatomic, copy) MCBaseWKWebVC *(^require_buyOrderVC)(NSString *merchantNo, NSString *itemNo);
    
    // 商品详情
    @property (nonatomic, copy) MCBaseVC *(^require_groupbuyInfoVC)(NSString *gbId);
    
    // 全部商品
    @property (nonatomic, copy) MCBaseVC *(^require_allGroupbuyVC)(NSArray <NSDictionary *> *groupbuyInfoDics);
    
    // 券详情
    @property (nonatomic, copy) MCBaseVC *(^require_couponDetailVC)(NSString *couponId, NSString *category);
    
    // 券核销详情
    @property (nonatomic, copy) MCBaseVC *(^require_couponConsumeDetailVC)(NSString *couponId);
    
    // 室内导航
    @property (nonatomic, copy) MCBaseVC *(^require_navFindShopAndCarVC)(NSString *URL);
    
    // 排队信息
    @property (nonatomic, copy) MCBaseVC *(^require_appointmentBookInfoVC)(NSString *shopId);
    
    // 排队详情
    @property (nonatomic, copy) MCBaseVC *(^require_appointmentBookDetailVC)(NSString *serialNo, NSString *orderNo);
    
    #pragma mark provide
    
    // 找店首页
    - (MCBaseVC *)provide_searchShopVC;
    
    // 店铺详情
    - (MCBaseVC *)provide_shopDetailVC:(NSString *)shopId;
    - (MCBaseVC *)provide_shopDetailVC:(NSString *)shopId
              viewDidDisappearCallBack:(void (^)(BOOL favorited))viewDidDisappearCallBack;
    
    // 适用商家 for MCGroupbuy
    - (MCBaseVC *)provide_shopApplyForGroupbuyVC:(NSString *)gbid;
    
    @end
    
  3. 很明显 API 里只有 对接外部实现<require>对外提供实现<provide> 两部分。

    所有 require 是一组抽象接口定义;

    所有 provide 是一组具体功能的实现;

2、组件内部开发

组件内部其实是一个比较宽范的开发空间,在基本底层库以后,内部开发变得轻而易举,所有内部的功能模块,除引用其自身开发所创建的引用类外,只依懒于自身的 API 进行开发。MCShop 组件内部的找店首页控制器 MCShopVC 只要依懒自身的 MCShop.h 进行开发。

组件内部开发应该遵循 稳定自完备性原则

// 下面 7 个引入为找店首页控制器内部所持有的类。
#import "MCShopVC.h"
#import "MCShopModel.h"
#import "MCShopCell.h"
#import "MCShopNaviView.h"

#import "MCShopDetailVC.h"
#import "MCShopSearchVC.h"
#import "MCShopLocationVC.h"

// 下面 MCShop.h 为依懒自身组件进行开发,不会出现其它任何组件或组件内部的类或常量。
#import "MCShop.h"

static NSString *const kCellID = @"kCellID";

@interface MCShopVC ()
...

内部开发最明显的特征就是,组件内的所有头文件的所有引入类的前缀都为该组件名称<此也为组件命名规范下的表现>。

3、组件的 Router

  1. Router 是组成中间件 Middleware 必不可少的一部分,所有组件的 APIRouter 组成 Middleware 的全部,决定着组件之间所有的通信<请求和响应>。

  2. Router 是写在主工程里的,主<壳>工程引入各组件,路由持有该组件的 API 单例,同时路由引入能提供该组件 API 里所有 Require 的组件,即为 API 寻找所有声明的实现,此为路由的精髓。依然以 MCShop 路由为例:

    MCBaseRouter.h

    #import "MCBaseRouter.h"
    #import "MCShop.h" // 路由引用的对应的组件
    
    @interface MCShopRouter : MCBaseRouter
    
    @property (nonatomic, strong) MCShop *shop; // 持有该组件的 API 单例
    
    @end
    

    MCBaseRouter.m

    #import "MCShopRouter.h"
    
    #import "MCBuyOrderVC.h" // 买单
    #import "MCCouponDetailVC.h" // 券详情
    #import "MCCouponConsumeDetailVC.h" // 券核销详情
    #import "WebVC.h"
    
    // 下面 3 个引入为:路由引入能提供该组件 API 里所有 Require 的组件
    #import "MCGroupbuy.h" // 商品组件
    #import "MCAppointment.h" // 美味不用等组件
    #import "MCEvent.h"// 活动组件
    
    @implementation MCShopRouter
    
    static MCShopRouter *shopRouter_;
    + (instancetype)sharedRouter {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            shopRouter_ = [[MCShopRouter alloc] init];
            shopRouter_.shop = [MCShop sharedShop];
        });
        return shopRouter_;
    }
    
    - (void)configRouter {
        MCShop *shop = self.shop;
        
        // 以下为组件 API 寻找所对应的实现<或指定实现以备内部调用>,此为寻找活动详情页面。
        shop.require_eventDetailVC = ^MCBaseVC *(NSString *eventId, MCEventType eventType) {
            // 上面引入活动组件后,活动详情页面只有活动组件能为其实现并提供该页面
            // 所以 MCShopRouter 找到 MCEvent 的 API 的 provide_eventDetailVC 方法得到该页面
            MCBaseVC *vc = [[MCEvent sharedEvent] provide_eventDetailVC:eventId eventType:eventType];
            return vc;
        };
        
        shop.require_buyOrderVC = ^MCBaseWKWebVC *(NSString *merchantNo, NSString *itemNo) {
            MCBuyOrderVC *vc = [[MCBuyOrderVC alloc] init];
            vc.merchantNo = merchantNo;
            vc.itemNo = itemNo;
            return vc;
        };
        
        shop.require_groupbuyInfoVC = ^MCBaseVC *(NSString *gbId) {
            MCBaseVC *vc = [[MCGroupbuy sharedGroupbuy] provide_groupbuyInfoVC:gbId];
            return vc;
        };
        
        shop.require_allGroupbuyVC = ^MCBaseVC *(NSArray<NSDictionary *> *groupbuyInfoDics) {
            MCBaseVC *vc = [[MCGroupbuy sharedGroupbuy] provide_groupbuyAllForShopVC:groupbuyInfoDics];
            return vc;
        };
        
        shop.require_couponDetailVC = ^MCBaseVC *(NSString *couponId, NSString *category) {
            MCCouponDetailVC *vc = [[MCCouponDetailVC alloc] init];
            vc.couponId = couponId;
            vc.category = category;
            return vc;
        };
        
        shop.require_couponConsumeDetailVC = ^MCBaseVC *(NSString *couponId) {
            MCCouponConsumeDetailVC *vc = [[MCCouponConsumeDetailVC alloc] init];
            vc.couponId = couponId;
            return vc;
        };
    
        shop.require_navFindShopAndCarVC = ^MCBaseVC *(NSString *URL) {
            WebVC *vc = [[WebVC alloc] initWithUrl:URL];
            return vc;
        };
        
        shop.require_appointmentBookInfoVC = ^MCBaseVC *(NSString *shopId) {
            MCBaseVC *vc = [[MCAppointment sharedAppointment] provide_appointmentBookInfoVC:shopId];
            return vc;
        };
        
        shop.require_appointmentBookDetailVC = ^MCBaseVC *(NSString *serialNo, NSString *orderNo) {
            MCBaseVC *vc = [[MCAppointment sharedAppointment] provide_appointmentBookDetailVC:serialNo orderNo:orderNo];
            return vc;
        };
    }
    
    @end
    
  3. 组件路由是精简的函数调用,不会有太多的逻辑处理。其目录只是为组件寻找实现入口或获得回调入口。

4、中间件的概念

Middleware 中间件其实就是 APIRouter 的集合体,两者之间完成相互的 RequestProvide 接口。

其主要表现为:

  1. 主要解决本地业务组件之间的通信问题;
  2. 从工程代码层面来说,组件化就是通过中间件解决组件间头文件直接引用、依赖混乱的问题;
  3. 纯中间件只负责挂接节点的通信问题,不应涉及挂接点具体业务的任何逻辑;

以上三点也是组件化技术层面的核心要素。

5、组件持续集成

严格的组件化后,每个组件有自已的 Git 仓库,独立更新及编译。

各独立的组件由 Cocoapods 工具进行版本管理,各组件更新至主工程由自动化脚本执行。

持续集成需要开发人员熟练 Cocoapods的使用和 Python 代码,此文主要在架构层面介绍组件化,其实现细节在此不做深入讲解。

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