iOS 应用开发技术
组件化与模块化

组件化与模块化

概念理解

模块化有什么好处?

  • 业务分层、解耦,使代码变得可维护;
  • 有效的拆分、组织日益庞大的工程代码,使工程目录变得可维护;
  • 便于各业务功能拆分、抽离,实现真正的功能复用;
  • 业务隔离,跨团队开发代码控制和版本风险控制的实现;
  • 模块化对代码的封装性、合理性都有一定的要求,提升开发同学的设计能力;
  • 在维护好各级组件的情况下,随意组合满足不同客户需求;(只需要将之前的多个业务组件模块在新的主App中进行组装即可快速迭代出下一个全新App)

如何组件化解耦

  • 基础功能组件:按功能分库,不涉及产品业务需求,跟库Library类似,通过良好的接口拱上层业务组件调用;不写入产品定制逻辑,通过扩展接口完成定制;
  • 基础UI组件:各个业务模块依赖使用,但需要保持好定制扩展的设计
  • 业务模块:业务功能间相对独立,相互间没有 Model 共享的依赖(去 Model 化)

常见的模块化方案

Target-Action, URL Router, Protocol

CTMediator

CTMediator (opens in a new tab) 方案的表象是通过 runtime 调度 Target-Action,但是 CTMediator 方案的本质是在不需要动业务代码的情况下,完成调度。

作者博客:

基于 CTMediator 的组件化方案,有哪些核心组成?

  • 中间件 CTMediator:集成就可以了
  • 扩展 CTMediator+ModuleName:扩展里声明了模块业务的对外接口,参数明确,这样外部调用者可以很容易理解如何调用接口。
  • 模块 Target_ModuleName:模块的实现及提供对外的方法调用 Action_methodName,需要传参数时,都统一以 NSDictionary * 的形式传入。

为什么 CTMediator 方案优于基于 Router 的方案?

Router 的缺点

在组件化的实施过程中,注册 URL 并不是充分必要条件。组件是不需要向组件管理器注册 URL 的,注册了 URL 之后,会造成不必要的内存常驻。注册 URL 的目的其实是一个服务发现的过程,在 iOS 领域中,服务发现的方式是不需要通过主动注册的,使用 runtime 就可以了。另外,注册部分的代码的维护是一个相对麻烦的事情,每一次支持新调用时,都要去维护一次注册列表。如果有调用被弃用了,是经常会忘记删项目的。runtime 由于不存在注册过程,那就也不会产生维护的操作,维护成本就降低了。由于通过 runtime 做到了服务的自动发现,拓展调用接口的任务就仅在于各自的模块,任何一次新接口添加,新业务添加,都不必去主工程做操作,十分透明。

在 iOS 领域里,一定是组件化的中间件为 openURL 提供服务,而不是 openURL 方式为组件化提供服务。如果在给 App 实施组件化方案的过程中是基于 openURL 的方案的话,有一个致命缺陷:非常规对象(不能被字符串化到 URL 中的对象,例如 UIImage)无法参与本地组件间调度。在本地调用中使用 URL 的方式其实是不必要的,如果业务工程师在本地间调度时需要给出 URL,那么就不可避免要提供 params,在调用时要提供哪些 params 是业务工程师很容易懵逼的地方。

为了支持传递非常规参数,蘑菇街的方案采用了 protocol,这个会侵入业务。由于业务中的某个对象需要被调用,因此必须要符合某个可被调用的 protocol,然而这个 protocol 又不存在于当前业务领域,于是当前业务就不得不依赖 public Protocol。这对于将来的业务迁移是有非常大的影响的。

CTMediator 的优点

调用时,区分了本地应用调用和远程应用调用。本地应用调用为远程应用调用提供服务。

组件仅通过 Action 暴露可调用接口,模块与模块之间的接口被固化在了 Target-Action 这一层,避免了实施组件化的改造过程中,对 Business 的侵入,同时也提高了组件化接口的可维护性。

方便传递各种类型的参数。

模块间去 Model 化(用字典传递)

组件间调用时,是需要针对参数做去model化的。如果组件间调用不对参数做去model化的设计,就会导致业务形式上被组件化了,实质上依然没有被独立。

假设模块A和模块B之间采用model化的方案去调用,那么调用方法时传递的参数就会是一个对象。

如果对象不是一个面向接口的通用对象,那么mediator的参数处理就会非常复杂,因为要区分不同的对象类型。如果mediator不处理参数,直接将对象以范型的方式转交给模块B,那么模块B必然要包含对象类型的声明。假设对象声明放在模块A,那么B和A之间的组件化只是个形式主义。如果对象类型声明放在mediator,那么对于B而言,就不得不依赖mediator。但是,大家可以从上面的架构图中看到,对于响应请求的模块而言,依赖mediator并不是必要条件,因此这种依赖是完全不需要的,这种依赖的存在对于架构整体而言,是一种污染。

如果参数是一个面向接口的对象,那么mediator对于这种参数的处理其实就没必要了,更多的是直接转给响应方的模块。而且接口的定义就不可能放在发起方的模块中了,只能放在mediator中。响应方如果要完成响应,就也必须要依赖mediator,然而前面我已经说过,响应方对于mediator的依赖是不必要的,因此参数其实也并不适合以面向接口的对象的方式去传递。

因此,使用对象化的参数无论是否面向接口,带来的结果就是业务模块形式上是被组件化了,但实质上依然没有被独立。

在这种跨模块场景中,参数最好还是以去model化的方式去传递,在iOS的开发中,就是以字典的方式去传递。这样就能够做到只有调用方依赖mediator,而响应方不需要依赖mediator。然而在去model化的实践中,由于这种方式自由度太大,我们至少需要保证调用方生成的参数能够被响应方理解,然而在组件化场景中,限制去model化方案的自由度的手段,相比于网络层和持久层更加容易得多。