SNModuleKit模块化规范

"SNModuleKit模块化规范,模块化是如何实施的,以及模块化开发规范"

Posted by SNLO on March 31, 2018

SNModuleKit模块化规范

本文是针对SNModuleKit框架下的模块化架构做出的规范

关键术语说明

模块

  • 模块,module,模块是一个通用概念,可能从功能或其他目的来区分。模块可以是子系统子领域,主要取决于上下文环境的用法。通常我们会说程序模块,功能模块,这实际上是在按照不同的标准对模块的内容和范围的不同定义。
  • 通常我们说的程序模块,是指的一段能够实现某个有价值目标的成员代码段,这样的东西,我们还有另一个称呼:例程,而例程有两种,即函数和过程,它们都能实现一个有价值的目标供其它的模块使用。
  • 而功能模块的说法一般在分析和设计阶段出现得比较频繁,通常我们用一个功能模块来说明一个功能所包含的系统行为,当我们觉得分析的颗粒度可能更大一些的时候我们可以用一个功能模块来表示一组功能的集合,这似乎让我们觉得,模块这个词的概念和“子系统”这个词的概念有些模糊,是的,事实上,有些大的模块会慢慢的让我们觉得称呼他们子系统更合适,或者一个子系统,我们会慢慢发现它还包含着一些模块。
  • 但是无论怎样,定义模块的原则应该是:高内聚和低耦合。
  • 使用方法:就是一个描述全局中问题的概念,至于全局是什么,这个随便,比如一个人,可以看成各种功能系统,那么模块就是各种呼吸系统、消化系统等;可以看成社会关系,模块就有劳动能力、生产关系等,全在于怎么看了。

组件

  • 组件,Component,首先说,组件已经不是一个抽象的概念了,是封装了一个或多个实体程序模块的实体。
  • 组件这个词通常是现在描述产品的时候出现,一个大的产品会有很多小的部分组成,而小的部分除了是一个大的组件的部分以外,自己可能还包含更小的组件,所以组件是递归的,那么组件到底是什么呢?最常见的组件就是我们已经写好的程序代码,任何一小段代码都可以是一个组件,它可以和其它代码段连接起来组成更大的一段程序代码,一个更大的组件,然后可能是一个函数,或者一个类程序单元,或者数个类单元文件的集成,当不同的组件的组装形成更大的组件时候,我们实际就是在做我们通常提到的一件事情:集成,软件中有很多集成工作要做,每日集成,重要版本集成等等,集成是什么呢?软件中,就是链编调试。这样一来,我们知道集成是需要对被集成的组件有规模要求的,换句话说,至少是一个单元文件,所以通常说到的组件就可以直观的理解为单元文件,或者可以组成软件的其他文件,以及编译后的文件。
  • 组件是面向对象里面的一个重用的概念,也称为构件,组件非常类似机械中构件概念,现在机械都是走向构件生成,通过不同构件组装成一个机械成品,软件目前也是这样的一个生成方式。
  • 维基百科上说,组件之间通过接口进行交互,这个听起来有些象插件,现实中也是这样,比如一个dll文件,可以说是插件,也可以说是组件。插件是是组件的一个子类,就是将组件中具有某些特点的组件归为插件,这些特点是:益于与系统分离,接口明晰,可以替换的程序模块。
  • 组件强调的是封装,利用接口进行交互。因为封装有不同层次的封装,对应不同层次的接口,(比如将一个人封装成一个组件,比如国家主席,多个人封装成一个组合,比如中央的常委们),所以组件所表述的范围和层次也是多种多样的,在谈论组件的时候一定要分辨清楚谈论的层次和范围。层次是相对的。你说地球是整个世界,但是将地球放到银河系中,地球就显得渺小了;你说物质世界是整个世界,但是人类的精神世界也是无比的浩瀚;你说物质世界和精神世界合起来是整个世界,但是历史又是那么的神秘和真实;你说物质世界、精神世界、历史时空是整个世界,但是科学家又说人类可以探察的宇宙物质仅占全部宇宙的百分之几。
  • 使用方法:就是一个描述系统中实体单元的概念。

插件

  • 根据对组件和模块的分析,插件属于组件,而且还是一个程序模块,也是一个功能模块。插件是一种电脑程序,通过和应用程序的互动,来替应用程式增加一些特定的功能。 插件必须依赖于应用程序才能发挥自身功能,仅靠插件是无法正常运行的。
  • 使用方法:满足一定接口规范的具有一定功能的程序模块。开发者可以在自己软件系统中设计相应的接口以匹配某个插件,也可以设计一定的接口规范,来让别人开发插件。插件和程序之间通过接口进行交互。

控件

  • 可视化的组件。比如UIViewControllerSNPopupViewUITableview等。

中间件

  • 中间件是提供系统软件和应用软件之间连接的软件,以便于软件各部件之间的沟通。

内聚

  • 块内联系

  • 模块内部各组成成分之间相互联系的强度。

耦合

  • 块间联系

  • 模块之间的依赖程度的度量,是模块独立性的直接衡量。包括紧密耦合、松散耦合 及无耦合。

什么是模块化

按一定原则将软件划分成若干个模块,使每个模块完成一个子功能,然后将这些模块组装起来就可以完成系统要求的功能,这个过程就是模块化。模块化的目的是降低系统复杂性。

为什么说模块化而不是组件化呢?本文认为,模块是组件的超集,模块中也许包含许多个组件,而一个复杂的系统如果只有组件化的存在,那可能存在N多个组件,系统依然复杂。如果把耦合度高的一些组件组合在一起,也就自然形成了模块。如果系统依然复杂,模块也会依然多?其实不然,模块的组成是随系统研发的生命周期而产生的,当周期为初期时,可能模块相对较大数目较少。随着周期的推进,可能模块会被细化数目增多,这并不会影响系统,因为模块被设计的原则就是高内聚低耦合,严格来说,一个模块不应该对另一个模块产生副作用。

为什么要实施模块化开发

当系统复杂度很高时,我们有必要在架构层面上去降低这种复杂性。那我们实施模块化,将各个不同的功能拆分开,或者根据某些约定的原则拆分,将重复相同的部分合并起来,此时组成的模块与模块间是低耦合的,模块内是高内聚的。这种架构尤其便利团队开发,团队中各个成员负责不同的模块,他们不再过多的相互干扰。对于那些都需要使用到的部分都被合并到了同个模块里,不会再彼此考虑相同的事情而产生不同的结论。这种模式有利于团队的开发效率,并且当某个功能或子系统模块出了问题,可以快速的进行替换,可以想象一下补丁。

下面来罗列下模块化的优势:

  • 独立性,模块内高内聚,模块间低耦合
  • 高效性,符合团队高效开发,可实现单个模块独立运行
  • 易维护,易测试
  • 复用性,模块间可实现资源共享
  • 信息隐蔽,模块作用范围由模块内部决定
  • 灵活性,模块的细腻程度由系统生命周期决定

SNModuleKit框架下,模块化是怎么实施的

先来看看SNModuleKit本身的架构图:1

SNModuleKit在Github上开源,这里有详细的介绍和使用说明。

首先导入框架,可以通过依赖管理工具导入。再使用框架中提供的模块模板工具创建新的模块,项目的模块化就开始了。

在模块化开始之前需要考虑的事:模块怎么划分。这个非常重要,划分恰当会直接减少之后模块拆分的工作难度,当然模块拆分我认为是避免不了的,因为模块的粒度总是由大到小你始终不确定未来会发生什么。在做模块划分的时候至少会考虑以下几个因数:

  1. 模块化实施前项目所处的阶段。
  2. 团队规模,各个团队成员各自的强项。
  3. 各个端口的情况,比如产品经理、后端和前端等。
  4. 项目未来整体大致的规划。
  5. 模块的各个组成部分应尽量遵守开-闭原则(设计模式几大原则之首)。

为什么要考虑这些个因数呢?那如果说项目在初期阶段,各模块被划分得比较大的话,在项目研发进行到一定程度,或者说需求累积到一定量时,整个模块内的业务量会大到让人头痛,以至于模块化都不复存在了,并且当对这种“超级”模块进行模块拆分时,会让你感觉浑身不舒服。

为什么要考虑模块化实施前项目所处的阶段呢?项目在不同的阶段,实施模块化都是不一样的。我们就拿简单的项目生命周期来说事:

  1. 规划阶段
  2. 计划阶段
  3. 实施阶段(研发阶段)
  4. 完成阶段(维护阶段)

在阶段1和2的时候,尽量参与进去,参与不了的后期也要多分析他们梳理出来的文档。这样做的好处是你会预判整个项目未来具体长啥样子。既是心中有数也是具有全局观,模块划分才会有方向感,不至于把两个完全不相关的功能或者组件放到一个模块里,违背了“高内聚、低耦合”的设计原则。在阶段3的时候,如果之前就做好了模块化,那么这个阶段模块很有可能会增多会变小。当两人同时开发一个模块的时候,是时候考虑进行模块拆分了,拆分后各自负责各自负责的模块,互不干扰。如果在此之前没有进行模块化,也就是说是在项目研发到一半时才开始模块化,不要慌张,其实也很简单,只是多一些模块拆分和重组等类似的工作,不就是项目架构重构嘛,请一定要相信自己,为了项目将来的发展。在阶段4的时候,项目需求已经趋于完善了相对稳定了,这时有必要考虑从新审视每个模块,让它们接近完美。为以后的维护做准备,为项目突然新增需求做准备,比如说不就是新增加一个扫描二维码的功能嘛,好的完成了,在产品部刚出需求变更通知时。再比如某个模块出问题了,没关系立马用备用模块临时替换掉。审视每个模块时朝着这种“比如”去做就对了。

为什么要考虑模团队成员各自的强项呢?很简单,举个例子说明,团队中分别有小明和小花两名成员,小明擅长视频处理,小花擅长图形图像处理,如果在划分模块时把图形图像处理和视频处理划分到了同一个媒体文件处理模块里面,那么小明和小花将会在同一个模块下进行开发。在版本管理中他们将会不断的进行代码合并、代码冲突处理和进度进程阻塞等待等。虽然这种情况不可避免,有可能还是常有的事情,也没什么不可行的地方,但是既然我们的模块化可以避免这种情况发生,那为什么不去避免呢?试想,视频处理和图形图像处理被分别划分到不同的模块里,那他们分别负责不同的模块进行开发,相互不干扰,效率不是更高吗。

为什么要考虑项目未来整体大致的规划呢?其实说白了就是到底要不要模块化?要知道不是所有的项目都适合去做模块化,到底适不适合?各有各的说法,仁者见仁,非要强行在只有一个单一功能的小项目中实施模块化谁又能说什么呢。再比如,团队中这个端口就只有一个人,并且根据项目规划,未来不打算增加团队成员,这种情概况进行模块化,我觉得那个人实属不易。要知道模块化还是会增加不少的工作量的。

再来看看完成模块化后,项目的架构图:4

模块内目录结构

SNModuleKit中提供的模板创建的公共模块来举例说明:

  • Protocol:存放模块内部协议。
  • Actions:存放Action类,为中间件提供模块对外开放的功能接口。
  • Resources:存放.xcassets、.md等资源文件。
  • Utils:存放工具类。
  • Macro:存放宏定义文件,不同类型的宏建议在不同文件中实现。
  • Categories:存放该模块所有的分类,模块化应遵守多用组合少用继承的设计原则,而分类就是对装饰模式的具体实现,装饰模式可以有效的避免大量的继承。
  • MVVM:本模块内是采用MVVM的设计模式进行组织代码的,也可以采用MVC、MVP等主流设计模式,不影响模块化。
  • Vender:存放未经依赖工具管理但只在本模块中采用的第三方库(组件)。

模块化编码规范

  1. 在模块中可以使用MVC、MVVM、MVP等常用设计模式,因为每个模块之间都是相互独立的,耦合度几乎为零,它们仅因中间调度者的存在而相互有着弱联系。

  2. 严格按照模块目录结构进行文件管理,比如不能在工具类文件夹中存放宏定义文件等。

  3. 对于UIViewController的创建方式非特殊需求禁止采用懒加载的方式创建。以此来防止不必要的循环引用。

  4. 对提供给中间件的接口,严格按照SNMediatorKit官方文档进行编写。

  5. 命名应遵守Apple命名约定,特别是与内存管理规则NARC)有关的命名约定。命名长的,描述性的方法和变量名很好。

  6. 对变量属性的命名和声明应该是(采用反命名规则,如:应该是buttonSetting而不是settingButton,再如:应该是viewPerson而不是personView):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    @property (nonatomic, strong) UIButton * buttonSetting;
       
    self.buttonSetting;
       
    #pragma mark -- getter / setter
    - (UIButton *)buttonSetting {
        if (!_buttonSetting) {
            _buttonSetting = [UIButton buttonWithType:UIButtonTypeSystem];
        } return _buttonSetting;
    }
    

    而不应该是:

    1
    2
    3
    4
    5
    6
    7
    
    @interface StandardViewController () <UITableViewDelegate> {
        UIButton * _setBut;
        UIButton * _settingButton;
    }
       
    _setBut;
    _settingButton;
    
  7. 应该使用注释来解释为什么这段特殊的代码片段在这儿或者做了某事。注释必须保持最新。所有暴露在头文件里面的必须注释。

  8. 代码整洁应该是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    ifuser.isHappy{
       //做某事 
    } else {
       //做别的事情 
    }
       
    //块很容易读取 
    [UIView animateWithDuration:3.0f animations:^{
      // something
    } completion:^(BOOL finished) {
      // something
    }];
    

    而不应该是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    ifuser.isHappy{
       //做某事 
    } 
    else {
       //做别的事情 
    }
       
    //块很容易读取 ,不应该提行!
        [UIView animateWithDuration:3.0f
                         animations:^{
                             // something
        }
                         completion:^(BOOL finished) {
                             // something
        }];
    
  9. 在模块中,类名以及文件名的命名规范需遵守以下格式:

    1
    2
    3
    4
    5
    6
    
    // object name = module name + developer name + object definition + object attribute;
    /**
    ps: OrderBaseViewController = Order(module name) + Base(object definition) + ViewController(object attribute);
       
    ps: OrderSNViewController = Order(module name) + SN(developer name) + ViewController(object attribute);
    */
    
  10. 对API的命名要求:

  • 如果是系统分类或者第三方库分类的属性和接口的命名必须添加类似sn_(developer name)的前缀。

  • 其它的应该尽量言简意赅,(可以参考apple的格式)。

关于模块化集成

项目模块化后,每个模块都可以独立运行。对于独立开发的模块都需要统一集成到项目中,整个项目才算完整。以下提供几种集成解决方案供参考:

  • 本地手动集成
  • CocoaPods集成
本地手动集成

独立开发的模块都放在本机上进行单独版本管理,等到需要集成到主项目时,再手动的拷贝过去放到主项目中,需要更新某个模块时再拷贝替换掉。这种方式来回的拷贝替换,对于大型的项目管理成本蛮高的,对于代码的版本管理和安全都是一种破坏。但有时候高明的方法往往都显得比较复杂,所以这是一种笨拙而简单的方案。

CocoaPods集成

通过CocoaPods集成类似于pod update更新项目所依赖的模块,就像更新所依赖的组件AFNetworking一样。此时的组件、模块还是别的什么,都是被添加到索引库能被pods索引的仓库。关键点是制作pod仓库,熟练了制作pod仓库之后,在项目中通过CocoaPods集成各个模块就易如反掌。通过CocoaPods集成也分两种情况,一种是集成公共仓库,一种是集成私有仓库,他们的区别就是:

  • 公共仓库是所有人都可以访问的,比如把仓库放到Github上,.podspec仓库索引文件也是放在CocoaPods开源的索引库中的,当然Github也提供私有库的创建只不过需要付费。在被微软收购之后不久宣布私有库免费,只限制每个库的协作人数。

  • 私有仓库是私人指定的有所有权的仓库,包括.podspec仓库索引文件也是放在指定的私有仓库中。

Podfile文件内容大致长这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Uncomment the next line to define a global platform for your project
platform :ios, '8.0'

inhibit_all_warnings!

#如果是私有库,需要添加私有索引库的镜像地址
source 'https://gitee.com/snlo/privateSpec.git' 
source 'https://github.com/CocoaPods/Specs.git'

target 'xx' do
  # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
  # use_frameworks!

  # Pods for AiteCube

  pod 'SNModuleKit'
  pod 'ModulePublic'
  
  pod 'ModuleLogin'
  pod 'ModuleFileHandle'

  target 'xxTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'xxUITests' do
    inherit! :search_paths
    # Pods for testing
  end

end

无论是公共仓库还是私有仓库都有个非常麻烦的地方,那就是每次仓库也就是模块更新、新增或者拆分等都需要对索引文件进行更新从新发布仓库版本。这里需要注意的是更新.podspec仓库索引文件时需要验证.podspec索引文件,而验证验证.podspec索引文件是非常耗时的操作。另外要是集成私有仓库还好,公共仓库的话CocoaPods开源的索引仓库更新起来还是挺费时的。关于如何制作pod仓库和如何创建私有索引库不是本文的重点,网上关于这方面的资料也是琳琅满目,建议参考CocoaPods官方指南少走弯路。

总结

两种解决模块化集成的方案都各自有各自的优缺点,建议在模块较稳定的情况才使用CocoaPods集成,在团队成员非常少和模块不稳定的情况下采用本地手动集成。

参考

维基百科

Apple官方文档

CocoaPods官方指南