/ MODULE

我为什么开始组件化开发?

组件化(模块化)在规模较大移动端项目中,是一种很常见拆分手段。在过去几年包括美团等大厂,围绕功能拆分、协议制定、打包发布,都已经有了很成熟的方案,大家可以自行了解。但是针对开发规模较小的项目是否有组件化开发的必要,我从自己的角度谈一点心得。

混沌初开

通俗来讲这是一个一生二,再合二为一的故事。最初的App包含一个考试模块,后来由于业务需要,裂变出两个App,由两班人马分别来维护。考试模块逐渐进化成不同的形态。直到有一天突然说要合并?一脸懵逼?!!

因为是一棵树的两个分支,所以必然存在很多共同的代码。但是在迭代过程中出现的差异使得两端并不能很好的融合。尤其是在对另一方代码改动未知的情况下,贸然合并必然会出现意想不到的情况。比如项目一中改动了A类的初始化方法,项目二的代码就无法通过编译。这类有编译器提醒的问题其实是最简单的问题。最怕那些默不作声藏在底层,在执行时才发生的诡异Bug。而且在处理这类逻辑问题时,由于不清楚对方的业务需求,时间也紧的情况下,不能贸然更改代码或者做进一步抽象。所以此时想到了组件化。

理想与现实

如果存在组件化,那将是怎样的场景?两个分支依旧保有一些不可避免的相同代码,以及可以在各自的组件库里独立运行。所以最简单粗暴的办法就是将其中一个分支的所有代码重命名,用以避免出现相同命名冲突。重命名后对组件的独立运行不会造成影响。然后就可以愉快的合并了。

理想很美好,现实呢?现实是两摊没有组件化的代码。是不是我可以尝试将一摊先进行组件化,然后以组件化的方式集成呢?此时目标是提取出能够独立执行的考试模块,然而现实还真是挺残酷的。在主项目工程中用文件夹来区分的模块并没有如文件夹那般简单的隔离。所以将文件夹拖动到一个新的项目工程中,出现了大量依赖缺失的编译问题。

依赖缺失

依赖缺失大致有以下几种情况:

网络层缺失

一般的App都会针对网络层,使用AFNetWorking等常见框架进行封装,针对业务对每个请求封装,携带自定义的请求参数、封装返回值,统一调度维护等。所以需要将网络层进行组件化,使得不同的组件能够,有统一请求逻辑。这其实是也是一直没有组件化的绊脚石。

公用方法和变量的缺失

通常我们为了编码的便利,会针对字符串处理、日期转换、图像和视图变化,抽象一些工厂或者胶水代码。还有一些全局定义的宏和变量命名。这些代码会根据用途编写在各自的工具类中,然后随着开发者的使用分散在项目的各个角落。部份工具类代码只是简单的封装,使用率也并不是很高,所以也没有必要像网络层那样去做成单独的组件。我倾向于减少这类文件依赖,用宏或者定义中调用环境中。这是从旧代码想组件化过程中的怨念,如果一开始就是在组件化环境下开发,倒也无妨,注意命名规范以减少冲突即可。

基类依赖

iOS中我们会很常见的定义一个基类XXXBaseViewController,所有的页面控制器都会继承这个基类。我们通常会在这里做一些统一的页面生命周期管理,设置导航栏,管理页面层次和页面方向等。我个人是倾向于组合优于继承的设计理念。但是在控制器及相关逻辑的统一问题上,组合并没有太好的解决方案。所以还是会考虑将这部分代码组件化。我看公司北京的同事也是采用这种方案。

视觉统一

在远离刀耕火种的今天,App的交互方式已经基本定型。也就产生了包含但不限于以下几种基本组件。

  • 以MBProgressHUD为代表的Loading效果以及类似安卓的Toast效果。
  • 以MJRefresh为代表的上拉刷新和加载更多效果。
  • 空状态页面或者加载失败时自定义的重新加载页面等。

通常视觉方都会针对这些组件进行定制化处理,以满足App的整体设计风格。将这些组件定制化之后维护自己的组件也是合理的思路。由于在旧有项目中使用广泛,所以组件化这些代码时会不少改动和阻力,也会大大增加测试的压力。依据死火山原则,先不去动这些代码。但是在新的组件中,我该如何保证使用统一的组件呢?

这里我使用的是定义协议的方式,使用LJRouter来调用这些协议。拿最简单的Loading效果为例,在子模块中定义展示/隐藏Loading的方法并且调用。

//方法声明
LJRouterUseAction(displayHudView, void,(BOOL)show,(UIView *)container);
//方法调用
action_displayHudView_with_show_container(NO, self.containerView);

在主工程中,实现这类协议。根据LJRouter的设定,没有实现会出现运行时错误,保证开发者能够注意到未定义的协议不被遗漏。组件在独立运行时,也需要定义这类协议的实现,但不一定真的实现,最后由主工程保障就可以。

//方法实现
LJRouterRegistAction(@"显示或者隐藏Hud", displayHudView, void,(BOOL)show,(UIView *)container) {
    if(show){
        [ZDHUD showNormalHUDInView:container];
    }else {
        [MBProgressHUD hideHUDForView:container animated:YES];
    }
}

数据依赖

在页面中需要用到的用户的基本信息,在请求时需要携带的Token等参数。这些数据和状态并不应该由组件去独立获取。而是由主工程负责维护,组件知只是在需要时去获取。获取方式也与之前的协议类似。就不再累述。


LJRouterRegistAction(@"获取到公用Session字段",get_session, id ,(NSString *)key) {
    if([key isEqualToString:xxx]) {
        return [Session shared].xxx;
    }
    NSAssert(NO, @"No available key for Session");
    return nil;
}

实现效果

虽然在考试模块的移植过程中,我成功的失败了。但是留下了宝贵的财产,即把网络层抽象了出来。为后面的组件化开发踢开了门口的绊脚石。所以在新的功能开发的过程中,我还是果断尝试了组件化的形式。最终达到了理想的结果以及上述的经验。

以下是这次功能的测试时效果图,可以独立运行的音频播放功能。也许回头更换下接口,就能作为独立的音频电台App运行。

1526899861367

优点

所以到最后为什么要组件化呢?并不是为了组件化而组件化。谈谈个人体会到的实际优点:

1.维护成本降低。这也是我认为最大的优点。软件开发的重要一点是控制复杂度。

在面向对象编程领域中,单一功能原则(Single responsibility principle)规定每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。—— from Wiki

我们将单一的逻辑封装在一个方法里,单一的功能封装在一类里。相同的思路运用在模块里,也就形成了组件。组件保持独立和完整。将所有的变化也局限在组件里,后期的维护者不用担心改动影响到其他模块。也没有限制了去污染其它模块的能力。间接保障工程代码质量。

2.便于移植和复用。将组件切个分支,改些许功能就可以为其它工程服务。

3.编译调试速度快。在较大的项目或者混编中体会更明显。

缺点

理想的组件化项目可能因为组件而大卸八块,组件之间依赖会增多。维护的时候,会稍微比直接修改代码来的繁琐一些。另外,如果部分低频工具类代码没有被组件化,不同组件的开发人员会根据自己的习惯添加。导致会出现一些代码上的冗余。如果能避免组件间的冲突,这类冗余在我看来是可以接受的。

以上是我这段时间在开发方面的心得,欢迎探讨指正~