Mobile APP component architecture practice

foreword

For 中大型mobile APP development, 组件化it is a common project architecture method. In recent years, I have been using the same way to develop in work projects 组件化, and I have accumulated some experience and thinking in the process. It mainly comes from the problems encountered in the use of component development in daily development and the exchange and discussion with other development students.

This article uses the following questions to introduce the idea of ​​component-based development architecture and some common problems:

  • Why componentization is needed
  • Challenges and choices encountered in the componentization process
  • How to maintain a high-quality componentized project

Tip: The componentized engineering mentioned in this article refers to Multirepothe use of independent git warehouses to manage components.

What can componentization bring

Problems encountered by a single project architecture

Before the component architecture, the traditionally used engineering architecture was mainly a Monolithicsingle engineering architecture, that is, all codes were 单个代码仓库managed in it. Why did a single project architecture suddenly encounter problems after using it for so many years? This also introduces a background in the development of APP projects. Existing 中大型APPprojects are becoming more and more complex:

  • 多APP项目并存APP- There are multiple projects within the group , and different people APPhope to 复用quickly build new ones with existing component capabilities APP.
  • 功能增多- As the project features more and more, the amount of code increases. At the same time, more developers are required to participate in the project, which increases the cost of collaboration between development teams.
  • 多语言/多技术栈- Introducing more new technologies, such as using more than one 跨平台UItechnology for rapid service delivery, different programming languages, audio and video, and cross-platform frameworks, which increase the complexity of the entire project.

The above demands for business development have 单一工程架构brought many new technical requirements to the traditional methods:

Engineering efficiency

  • Too much project code will cause 编译速度slowness.
  • A single gitproject submission may bring more git提交冲突sums at the same time 编译错误.

Quality issues

  • 如何将git提交关联到对应的功能模块需求。发版时进行合规检查避免带入不规范的代码,对整个功能模块回滚的诉求。
  • 如何在单仓库中管控这么多开发人员的代码权限,尽可能避免不安全的提交并且限制改动范围。

更大范围的组件复用

  • 基础组件从支持单个APP复用到支持多个APP复用。
  • 不只是基础能力组件,对于业务能力组件也需要支持复用。(例如一个页面组件同时在多个APP使用)
  • 跨平台容器需要复用底层组件能力避免重复开发,同时不同跨平台容器API需要尽量保持统一,底层基础设施向容器化发展支持业务跨APP复用。

跨技术栈通信

  • 由于页面导航多技术栈混合共存,页面路由需要支持跨技术栈。
  • 跨组件通信需要支持跨语言/跨技术栈通信。

更好的解耦

  • 页面解耦。由于页面导航栈混合共存,页面自身不再清晰的知道上游和下游页面由什么技术栈搭建,所以页面路由需要做到完全解耦隔离技术栈的具体实现。
  • 业务组件间维持松耦合关系,可以灵活添加/移除,基于现有组件能力快速搭建出不同的APP。
  • 对于同一个服务或页面可以插件化方式灵活提供多种不同的实现,不同的APP宿主也可以提供不同的实现并且提供A/B能力。
  • 由于包体积限制和不同组件包含相同符号导致的符号冲突问题,在复用组件的时候需要尽可能引入最小依赖原则降低接入成本。

组件化架构的优势

基于以上这些问题,现在的组件化架构希望可以解决这些问题提升整个交付效率和交付质量

组件化架构通常具备以下优点:

  • 代码复用 - 功能封装成组件更容易复用到不同的项目中,直接复用可以提高开发效率。并且每个组件职责单一使用时会带入最小的依赖。
  • 降低理解复杂度 - 工程拆分为小组件以后,对于组件使用方我们只需要通过组件对外暴露的公开API去使用组件的功能,不需要理解它内部的具体实现。这样可以帮助我们更容易理解整个大的项目工程。
  • 更好的解耦 - 在传统单一工程项目中,虽然我们可以使用设计模式或者编码规范来约束模块间的依赖关系,但是由于都存放在单一工程目录中缺少清晰的模块边界依然无法避免不健康的依赖关系。组件化以后可以明确定义需要对外暴露的能力,对于模块间的依赖关系我们可以进行强约束限制依赖,更好的做到解耦。对一个模块的添加和移除都会更容易,并且模块间的依赖关系更加清晰。
  • 隔离技术栈 - 不同的组件可以使用不同的编程语言/技术栈,并且不用担心会影响到其他组件或主工程。例如在不同的组件内可以自由选择使用KotlinSwift,可以使用不同的跨平台框架,只需要通过规范的方式暴露出页面路由或者服务方法即可。
  • 独立开发/维护/发布 - 大型项目通常有很多团队。在传统单一项目集成打包时可能会遇到代码提交/分支合并的冲突问题。组件化以后每个团队负责自己的组件,组件可以独立开发/维护/发布提升开发效率。
  • 提高编译/构建速度 - 由于组件会提前编译发布成二进制库进行依赖使用,相比编译全部源代码可以节省大量的编译耗时。同时在日常组件开发时只需要编译少量依赖组件,相比单一工程可以减少大量的编译耗时和编译错误。
  • 管控代码权限 - 通过组件化将代码拆分到不同组件git仓库中,我们可以更好的管控代码权限和限制代码变更范围。
  • 管理版本变更 - 我们通常会使用CocoaPods/Gradle这类依赖管理工具来管理项目中所有的组件依赖。因为每一个组件都有一个明确的版本,这样我们可以通过对比APP不同版本打包时的组件依赖表很清晰的识别组件版本特性的变更,避免带入不合规的组件版本特性。并且在出现问题时也很方便通过配置表进行回滚撤回。

提示:组件化架构是为了解决单一工程架构开发中的问题。如果你的项目中也会遇到这些痛点,那可能就需要做组件化

组件化遇到的挑战

虽然组件化架构可以带来这么多收益,但不是只要使用组件化架构就可以解决所有问题。通常来讲当我们使用一种新的技术方案解决现有问题的时候也会带来一些新的问题,组件化架构能带来多少收益主要取决于整个工程组件化的质量。那在组件化架构中我们如何去评估项目工程的组件化架构质量,我们需要关注哪些问题。对于软件架构来讲,最重要的就是管理组件实体以及组件间的关系。所以对于组件化架构来讲主要是关注以下三个问题:

  • 如何划分组件的粒度、组件职责边界在哪里?
  • 组件间的依赖关系应该如何管理?
  • 组件间应该使用哪种方式调用和通信?

1. 组件拆分的粒度、组件职责边界在哪里?

某种程度上组件拆分粒度也是一种平衡的艺术,我们需要在效率质量之间找到一种相对的平衡。组件拆分粒度太粗:导致组件间耦合紧密,并不能利用更好的复用/解耦/提高编译速度这些优势。组件拆分粒度太细:导致需要维护更多的组件代码仓库、功能变更可能涉及多个组件代码的修改/发布,这些都会带来额外的成本,同时组件过多也会导致组件依赖查找过程变的更复杂更慢。

组件的职责也会影响我们对于组件的拆分方式:每个组件的定位是什么,应该包含什么样的功能,是否可以被复用,添加某个功能的时候应该创建新组件还是添加到现有组件,当组件复杂到一定程度时是否需要拆分出新个组件。

在拆分组件前需要提前去思考这些问题。

2. 组件间的依赖关系应该如何管理?

组件间的依赖方式主要分为直接强耦合依赖间接松耦合依赖强耦合依赖是对依赖的组件直接使用对应的API进行调用,这种调用方式优点是简单直接性能更好,缺点是一种完全耦合的调用方式。(基础组件通常使用这种方式)。松耦合依赖主要是通过通知URL SchemeObjC Runtime服务接口事件队列等通信方式进行间接依赖调用。虽然性能相对差一点,但这是一种相对耦合程度比较低并且灵活的依赖方式。(业务组件通常使用这种方式)

组件间的依赖关系很重要是因为在长期的项目开发演化过程中很容易形成一种复杂的网状依赖关系。虽然看似使用组件化的方式将模块拆分成不同的组件,但是组件间可能存在很多相互交叉的依赖耦合关系,很多组件都被其他组件直接依赖隐式间接依赖。这样我们就背离了组件化架构更好的解耦、更好的复用、更快速的开发/编译/发布的初衷。

所以我们需要制定一套规范去约束和规范组件间的依赖关系:两个组件之间是否可以依赖,组件间依赖方向,选择强耦合依赖还是松耦合依赖。

3. 组件间松耦合依赖关系应该使用哪种方式调用和通信?

松耦合依赖通常可以使用通知URL SchemeObjC Runtime服务接口事件队列等方式通信进行间接调用,但是使用哪种方式更好业界也有很多争论,并且每种方式都有一些优缺点。通常在项目中会根据不同的使用场景至少会选择2种通信方式。

耦合程度低的方式例如URL Scheme,可以做到完全解耦相对比较灵活。但是无法利用编译时检查无法传递复杂对象、调用方/被调用方都需要对参数做大量的正确性检查和对齐。同时可能无法检测对应的调用方法是否存在。

耦合程度高的方式例如服务接口,需要对服务接口方法进行强依赖,但是可以利用编译时检查、传递复杂对象、并且可以更好的支持Swift特性。

我们需要在解耦程度容易使用安全上找到一种合适的方式。

提示:这里的耦合程度高是相对于耦合程度低的方式进行比较,相比直接依赖对应组件依然是一种耦合程度低的依赖关系。

组件化架构实践规范和原则

基于以上这些组件化架构的问题,需要一些组件化架构相关的规范和原则帮助我们做好组件化架构,后面主要会围绕以下三点进行介绍:

  • 组件拆分原则 - 拆分思想和最佳实践指导组件拆分
  • 组件间依赖 - 优化组件间依赖关系跨组件调用/通信方式的选择
  • 质量保障 - 避免在持续的工程演化过程中工程质量逐渐劣化。主要包含安全卡口和CI检查

工程实例

接下来以一个典型的电商APP架构案例来介绍一个组件化工程。这个案例架构具备之前所说现有中大型APP架构的一些特点,多组件、多技术栈、业务间需要解耦、复用底层基础组件。基于这个案例来介绍上面的三点原则。 APP分层架构

组件拆分原则

APP分层架构 The most important thing about component splitting is to help us sort out component responsibilities and the boundaries of component responsibilities. Component division also uses many common design principles and architectural ideas.

Split using layered thinking

Usually we can first use the idea of ​​layered architecture to 所有组件split vertically 多层组件, and the components at the upper level can only depend on the components at the lower level. Generally can be divided into at least 四层组件:

  • 基础层- Provide core basic capabilities that have nothing to do with upper-layer services. Can be used by upper-level components 直接依赖.
  • 业务公共层- Mainly includes page routing, public UI components, cross-component communication and service interfaces, which can be 直接依赖used by upper-level components.
  • 业务实现层- Business core implementation layer, including native pages, cross-platform containers, and business service implementation. Between components , it can only be used 不能直接依赖by calling 页面路由or across components .通信组件
  • APP宿主层- Mainly includes APP 主工程, startup process, page routing registration, service registration, SDK parameter initialization and other components, which are used to build and package to generate corresponding APP.

Hierarchy can be a good guide for us to split components. When splitting a component, we need to first identify which layer it should be in, which calling method it should be used by other components, and whether the newly added function will be generated to help us standardize the dependencies between 反向依赖components. At the same time, splitting components by level is also beneficial to the underlying basic components 复用.

The following scenarios are easy to identify using layered thinking:

Basic components depend on business components

Example: The network request initiated by the business in the APP usually needs to be carried 公共参数/Cookie.

  • 没有组件分层约束- The network library may rely on 登录服务obtaining user information and 定位服务latitude and longitude, and introduce a large number of dependencies into business components.
  • 有组件分层约束 - 网络库作为一个基础组件,它不需要关注上层业务需要携带哪些公共业务参数,同时登录/定位服务组件在网络库上层不能被反向依赖。这时候会考虑单独创建一个公共参数管理类,在APP运行时监听各种状态的变更并调用网络库更新公共参数/Cookie

业务组件间依赖方向是否正确

登录状态切换经常会涉及到很多业务逻辑的触发,例如清空本地用户缓存、地址缓存、清空购物车数据、UI状态变更。

  • 没有组件分层约束 - 可能会在登录服务内当登录状态切换时调用多个业务逻辑的触发,导致登录服务引入多个业务组件依赖。
  • 有组件分层约束 - 登录组件只需要在登录状态切换时发出通知,无需知道登录状态切换会影响哪些业务。业务逻辑应该监听登录状态的变更。

识别基础组件还是业务组件

虽然很多场景下我们很容易能识别处理出来一个功能应该归属于基础组件还是业务组件,例如一个UI控件是基础组件还是业务组件。但是很多时候边界又非常的模糊,例如一个添加购物车按键应该是一个基础组件还是业务组件呢。

  • 基础组件 - 如果不需要依赖业务公共层那应当划分为一个基础组件。
  • 业务组件 - 依赖了业务公共层或者网络库,那就应该划分为一个业务组件。

分层思想可以很好的帮助我们管理组件间的依赖关系,并且明确每个组件的职责边界。

基础/业务组件拆分原则

划分基础/业务组件主要是为了强制约束组件间的依赖关系。以上面的组件分层架构为例:

  • 基础组件 - 基础组件被直接依赖使用,使用方调用基础组件对外暴露API直接使用。基础层业务公共层都为基础组件。
  • 业务组件 - 业务组件不可被直接依赖使用,只能通过间接通信方式进行使用。APP宿主层业务实现层都为业务组件。

提示:这里的业务组件并不包含业务UI组件

基础组件拆分

基础组件通常根据职责单一原则进行拆分比较容易拆分,但是会有一些拆分场景需要考虑:

使用插件组件拆分基础组件扩展能力

将核心基础能力和扩展能力拆分到不同的组件。以网络库为例,除了提供最核心的接口请求能力,同时可能还包含一些扩展能力例如HTTPDNS网络性能检测弱网优化等能力。但这些扩展能力放在网络库组件内部可能会导致以下问题:

  • 扩展能力会使组件自身代码变的更加复杂。
  • 使用方不一定会使用所有这些扩展能力违反了最小依赖原则。带来更多的包体积,引入更多的组件依赖,增加模块间的耦合度。
  • 相关的扩展能力不支持灵活的替换/插拔。

所以这种场景我们可以考虑根据实际情况将扩展能力拆分到相应的插件组件,使用方需要时再依赖引入对应插件组件。

业务组件拆分

业务页面拆分方式

针对业务页面可以使用技术栈业务域页面粒度三种方式进行更细粒度的划分,通常至少要拆分到技术栈业务域这一层级,页面粒度拆分根据具体页面复杂度和复用诉求。

  • 基于技术栈进行拆分 - 不同的技术栈需要拆分到不同的组件进行管理。
  • 基于业务域进行拆分 - 将同一个业务域的所有页面拆分一个组件,避免不同业务域之间形成强耦合依赖关系,同一个业务域通常会有更多复用和通信的场景也方便开发。例如订单详情和订单列表可放置在一起管理。
  • 基于页面粒度进行拆分 - 单个页面复杂度过高或需要被单独复用时需要拆分到一个单个组件管理。

提示:放置在单一组件内的多个页面之间也应适当降低耦合程度。

第三方库

第三方库应拆分单独组件管理

第三方库应使用独立的组件进行管理,一方面有利于组件复用同时避免多个重复第三方库导致符号冲突,另一方面有利于后续升级维护。

一些提示

减少使用通用聚合公共组件

为了避免拆分过多的组件,我们通常会创建聚合组件将一些代码量不多/功能相似的类放到同一个组件内,例如Foundation组件UI组件。但是很多时候会存在滥用的场景,应当警惕这类公共聚合组件。下面是一些公共聚合组件容易滥用的场景:

  • 添加一个新功能不知道应当加在哪里时,就加到公共聚合组件内,时间久了以后公共组件依赖特别多。
  • 公共组件添加了一个非常复杂的能力,导致复杂度变高或者引入大量依赖
  • 太多能力聚合到一起。例如将网络库、图片库这些能力放在同一个组件内
  • 基础/业务UI组件没有拆分。基础UI组件通常只提供最基础的UI和非常轻量的逻辑,业务组件通常会充当基础UI组件的数据源以及业务逻辑。

但是也不能完全避免使用聚合公共组件,不然会导致产生更多的小组件增加维护成本。但是我们将一个能力添加到公共聚合组件时可以根据以下几个条件来权衡:

  • 是否会引入大量新的依赖
  • 功能复杂度、代码数量,太复杂的不应该添加到公共组件
  • 能力是否需要被单独复用,需要单独复用就不应该添加到公共组件

第三方库考虑不直接对外暴露使用

当存在以下情况时可考虑对第三方库进行适当的封装避免直接暴露第三方库:

  • 使用方通常只需要使用少量API,第三方库会对外暴露大量API增加使用难度,同时可能导致一些安全问题
  • 对外隐藏具体实现,方便后续更换其他第三方库、自实现、第三方库发生Break Change变更时升级更容易
  • 需要封装扩展一些能力让使用方使用起来更容易

以网络库为例:1.通常需要对接公司内部的API网关能力所以需要适当做一些封装,例如签名或者加密策略。2.使用方通常只需要用到一个通用的请求方法无需对外暴露太多API。3.为了安全通常需要对业务方隐藏一些方法避免错误调用,例如全局Cookie修改等能力。4.对外隐藏具体第三方库可以方便变更。

第三方库尽可能避免直接修改源码

第三方库组件尽可能不要直接修改源码,除修复Bug/Crash之外尽可能避免带入其他功能代码导致后面更新困难。需要添加功能时可以通过在其他组件内使用第三方库对外暴露的API进行能力扩展。

组件间依赖关系

业务组件间通信方式选择

松耦合通信方式对比

通信方式

基于以上表格中各种方案的优缺点,个人推荐使用URL Scheme协议作为页面路由通信方式,使用服务接口提供业务功能服务。通知订阅场景可使用通知RxSwift方式提供一对多的订阅能力。

服务接口

服务接口对应的实现和页面是否需要拆分

以购物车服务为例,购物车接口服务提供了添加购物车的能力。加车服务具体的实现应该放在购物车页面组件内还是独立出来放置在单独的组件。将购物车服务实现和购物车页面拆分的优点是购物车服务和购物车页面更好的解耦,都能单独支持复用。缺点是开发效率降低,修改购物车功能时可能会涉及到同时修改购物车服务组件和购物车页面组件。

所以在需要单独复用服务页面的场景时可考虑分别拆分出单个组件(例如购物车服务作为一种通用能力提供给上层跨平台容器能力)。但即使在同一个组件内也建议对服务和页面使用分层设计的方式进行解耦。

服务接口是否需要拆分

一般项目可能至少会有10+个服务接口,这些服务接口应该统一存放在单个组件还是每个接口对应一个组件。

  • 统一存放:优点是一起管理更快捷方便。缺点是所有接口对应一个组件版本,不能支持单一接口使用不同版本,不利于需要跨APP复用的项目。并且使用方可能会引入大量无用的接口依赖。
  • 分开存放:优点是每个接口可使用不同的版本并且使用方只需要依赖特定的接口。缺点是会产生更多的组件仓库,组件数量也会增加依赖查找的耗时。 所以大型项目选择分开存放的方式管理接口相对更合适一点。也可以考虑将大部分最核心的服务接口放置到一起管理。

支持Swift的服务接口实现推荐

使用Swift实现传统的服务接口模式通常会遇到以下两个问题:

  • 接口需要同时支持Objective-CSwift调用,同时希望使用Swift特性设计API。如何实现Objective-CSwift协议可以复用一个实例
  • Swift对于动态性支持比较弱,纯Swift类无法支持运行时动态创建只能在注册时创建实例

基于以上问题,个人推荐使用下面的方式实现接口服务模式:

  • 使用Objective-C协议提供最基础的服务能力,之后创建Swift协议扩展提供部分Swift特性的API
  • 接口实现类继承NSObject支持运行时动态初始化
// @objc协议
@objc public protocol JDCartService {
    func addCart(request: JDAddCartRequest, onSuccess: () -> Void, onFail: () ->) 
}
// swift协议
public protocol CartService: JDCartService {
    
    func addCart() async

    func addCart(onCompletion: Result<Data, Error>)

}

// 实现类
class CartServiceImp: NSObject, CartService {
    // 同时实现Objc和Swift协议
}

服务应该中心化注册还是分布式注册

中心化注册是在宿主APP启动时统一注册服务接口的对应实现实例,分布式注册是在组件内组件自身进行注册。个人推荐中心化注册的方式在宿主APP启动时统一进行注册管理,明确服务的实现方更清晰,同时避免不同组件包含同一个服务接口的不同实例导致的冲突。

组件版本兼容

谨慎使用常量、枚举、宏

因为组件编译发布的时候会生成二进制库,编译器会将依赖的常量、枚举、宏替换成对应的值或代码,所以当后续这些常量、枚举、宏发生变更的时候,已生成的二进制库并不会改变导致打包的时候依然使用的旧值,必须重新发布使用这些值的组件才行。所以应当尽量避免修改常量、枚举、宏值,如果已知后续可能会变更的情况下应避免使用常量、枚举、宏

基础组件API向后兼容

  • 对外API需保证向后兼容,使用添加API的方式扩展现有能力,避免对原有API进行break change改动或移除
  • 使用对象封装传递参数和回调参数,避免对原有API进行修改

提示:特别是对于Objective-C这类动态调用的语言来讲,打包构建时并不能发现调用的方法不存在、参数错误这些问题。所以我们应当尽可能避免现有方法的变更。同时也推荐更多使用Swift编译器可以发现这些问题提示编译错误。

减少发布大版本

Cocoapods为例,组件发布大版本会导致依赖此组件的所有组件都必须同时升级到大的版本重新发布,这样会给组件使用放带来极大的更新成本。所以组件应该减少发布大版本,除非必须强制所有组件一定要升级。

优先选择接口服务减少暴露View类

当只关注API提供的能力并不关注API提供的形态时尽可能通过API的方式来暴露能力。因为暴露接口方法相比视图View,调用方只需要依赖接口方法相比依赖View类可以更小化的依赖,同时接口对于实现方未来扩展能力更灵活。以选择用户地址API为例,通常调用方并不关注实现方以弹窗的方式还是页面的方式提供交互能力让用户选择,只关注用户最终选择的地址数据。并且调用方不需要处理弹窗和页面的展示逻辑使用起来更方便,也便于实现方之后修改交互方式。

使用接口的方法
addressService.chooseAddress { address in

}
使用View的方式
let addressView = AddressView()
addressView.callback = { address in
    ///
}
addressView.show()

避免使用Runtime反射动态调用类

应当尽量避免使用反射机制在运行时使用其他类,因为类的实现方不能保证这个类一直存在,编译器也无法检测出错误。某些基于AOP的功能可能会使用到这种动态反射能力,但是大部分场景应该尽量避免。

第三方库

第三方库组件不允许依赖其他组件。

第三方库组件不允许依赖其他组件。

质量保障

通信方式

虽然前面讲到了很多规范和原则,但是并不能保证我们的这些规范和原则可以强制执行。所以我们需要在组件发布和应用打包阶段添加一些卡口安全检测,及时发现组件化依赖问题避免带入线上。

CI检查

组件发布

在组件发布时添加一个安全检查,避免不符合依赖规范的组件发布成功。通常我们可以添加以下依赖检查规则:

  • 第三方库不可依赖其他组件
  • 基础组件不可依赖业务组件
  • 业务组件不可直接依赖业务组件
  • 组件间通常不可相互依赖
  • 不允许组件层级间反向依赖

版本集成规范

集成系统需要将特定需求和组件版本关联到一起,打包时会根据版本需求自动加入对应的组件版本。避免开发同学直接修改组件版本引入不应该加入到版本的特性。

打包构建

在宿主APP打包时,提前检测出接口服务存在的问题,避免带入到线上。通常可以检测以下问题:

  • 服务接口对应的实现类不存在
  • 服务接口对应的实现类没有实现所有方法
  • 使用ObjC Runtime动态调用类和方法

线上异常上报

线上检查可以帮助我们在灰度发布的及时发现问题及时修复,通常可以发现以下问题:

  • 路由跳转对应的页面不存在
  • 接口服务对应的实现类不存在
  • 接口服务对应的方法不存在

可量化指标

我们可以通过一些指标来量化整个工程组件化的健康程度,以下列出常见的一些指标:

基础组件依赖数量

组件依赖的所有基础组件总数,当依赖的基础组件总数过高时应该及时进行重构。如果大量的业务组件都需要依赖非常多的基础组件,那可能说明基础组件的依赖关系出现了很大的问题,这时候需要对基础组件进行优化重构:

  • 考虑使用接口服务对外暴露能力,组件层级需要提升
  • 考虑将部分能力拆分出为独立的新组件

业务服务依赖数量

业务组件对其他业务服务组件的依赖数量。当业务组件依赖了其他业务服务调用时也会造成隐式的耦合关系,依赖过多时应当考虑是否应该对外暴露可监听变化的通知订阅以订阅观察的方式替代主动调用

错误依赖关系数量

错误的依赖关系应该及时优化改造。

一些常见的问题

基础组件应该直接暴露还是使用接口暴露

基础组件应该直接使用头文件API暴露还是使用接口间接暴露有时候很难权衡,但是可以根据一些特性来权衡选择:

API直接暴露
  • 功能单一/依赖少 - 一些工具类,例如Foundation
  • API复杂 - API非常多如果使用接口需要抽象太多接口,例如网络库日志
  • UI组件 - 需要直接暴露UIViewUI组件,例如UIKit
接口暴露
  • 可扩展性 - 基于接口可以灵活替换不同的实现,例如定位能力可以使用系统自带的API,也可以使用不同地图厂商的API
  • 减少依赖引入 - 降低使用方的接入成本,提高日常开发/组件发布效率
  • 可插拔能力 - 对应的能力可移除,同时也不影响核心业务 提示:这些以接口暴露的API还有一个优势是可以抽象成容器化的API,形成统一的标准规范。使用方调用同样的API,不同的APP可以提供不一样的实现。

小项目是否应该做组件化

个人认为小项目也可以做组件化,需要关注的是需要做到什么程度的组件化。通常来讲越大型越复杂的项目组件化拆分的粒度更细组件数越多。对于小项目来讲虽然早期做组件化的收益并不大,也需要适当考虑未来的发展趋势预留一定的空间,同时也需要适当考虑模块间的依赖关系避免后期拆分模块时很困难。刚开始做粒度比较粗的组件化,之后在项目发展中不断的调整组件化的粒度。也可以考虑使用类似Monorepo的方式来管理项目,代码都在一个仓库中管理,通过文件夹隔离规范模块间的依赖。

单一工程如何改造为组件化工程

一般来讲我们需要使用循序渐进逐步重构的策略对原有项目进行改造,但是有一些模块可以优先被组件化拆分降低整个组件化的难度:

  • 优先拆分出最核心的所有业务模块可能都需要使用的组件,这些组件拆分完成以后才能为之后业务模块拆分提供基础。例如FoundationUI组件网络库图片库埋点日志等最基础的组件。
  • 优先拆分不被其他组件依赖被其他组件依赖较少的模块组件,这些模块相对比较独立拆分起来比较高效并且对现有工程改造较小。例如性能监控微信SDK这类相对独立的能力。

组件化带来的额外成本

组件化架构可能会带来以下这些额外的成本:

  • 管理更多的组件git仓库
  • 每次组件发布都需要重新编译/发布
  • 由于组件使用方都是使用相应的组件二进制库,所以调试源码会变的更困难
  • 开发组件管理平台,管理组件版本、版本配置表等能力
  • 每个组件需要有自己的Example工程进行日常开发调试
  • 处理可能存在的组件版本不一致导致的依赖冲突、编译错误等问题
  • 需求可能会涉及到多组件改动,如何进行Code Review版本合入检查

Monorepo

我个人并没有在实际的项目中使用过Monorepo方式管理项目。Monorepo是将所有组件代码放在单个git仓库内管理,然后使用文件夹拆分为不同的组件。不同文件夹中的代码不能直接依赖使用,需要配置本地文件夹的组件依赖关系,在实现组件化的同时避免拆分太多的git仓库。不过个人认为Monorepo同时也需要解决以下几个问题:

  • 编译耗时优化 - 将所有源码放在单个工程中会导致编译变慢,所以必须优化现有工程编译流程,降低非必要的重复编译耗时。
  • 组件版本管理 - 在组件化工程中我们可以通过配置组件的特定版本来管理功能是否合入到版本中,但在Monorepo中只能通过分支Merge Request来管理特性是否合入,回滚也会更加繁琐。
  • 高质量CI流程 - 在单个仓库中,当一个开发者有仓库权限时他就可以修改该仓库的任意代码。所以必须完善代码合入规范,更高标准的Code Review集成测试检查自动化检查避免问题代码带到线上。

总结

个人认为并不存在一个完美的架构,我们自身的组织架构、业务、人员都在变动,架构也需要随着这个过程进行适当的调整和重构,最重要的是我们能及时发现架构中存在的问题并且有意愿/能力去调整避免一直堆积变成更大的技术债务。

同时工程架构的改变也会一定程度的改变开发人员的分工,对于大型工程来讲组件化的程度更高,每个开发人员的工作分工会更细。对于底层基础组件的开发,需要提供更多高性能/高质量的基础组件让上层业务开发人员更加效率的支撑业务,技术深度也会更加深入。对于上层业务开发,更多是使用这些底层基础组件,同时可能也需要掌握多种跨端UI技术栈快速支撑业务,技术栈会更广但是不会太深入。

Guess you like

Origin juejin.im/post/7240333779222167608