云课堂Android模块化实战--如何设计一个通用性的模块

社区编辑2018-05-25 10:54

如何设计一个通用性的模块

前言

每个开发者都会知道,随着项目的开发,会发现业务在不断壮大,产品线越来越丰富,而留给开发的时间却一直有限,在有限的时间,尽快完成某个功能的迭代。因此为了减少开发成本,保证业务功能复用,我们会将一些业务独立出来,比如直播间、消息等,做成单独的模块。所以想必都会都模块化开发有所了解。

本文的目的,并不是讲述如何处理模块化后的每个模块之间的通信问题,以及整个应用的架构问题,而是对于做了这么多模块后,对模块有个总结,在需要创建一个新的模块的时候,可以少走弯路,如何设计一个通用性的模块

模块定义

独立的业务模块

它和组件的区别是:

  • 组件:指的是单一的功能组件,如地图组件、支付组件、路由组件、分享组件等等。
  • 模块:指的是独立的业务模块,如直播间模块、消息模块、课程详情模块、课时学习模块等等。

模块可以依赖一些组件,模块与模块之间应该不要有依赖关系。

模块类图

在开始讲述模块的结构前,先看下完整的模块类图,分为内部元素,外部元素以及外部实现。 内部元素,只能在模块内部流动。 外部元素,在模块内部和外部之间流通的。 外部实现,这些类需要在模块外部定义。


领域驱动设计(DDD)的几个基本概念

在讲述模块内的元素之间,先了解几个DDD钟的基本概念:

  • 实体 当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。
  • 值对象 当一个对象用于对事务进行描述而没有唯一标识时,它被称作值对象(Value Object)。
  • 聚合根 Aggregate(聚合)是一组相关对象的集合,作为一个整体被外界访问,聚合根(Aggregate Root)是这个聚合的根节点。
  • 领域服务 一些重要的领域行为或操作,可以归类为领域服务。它既不是实体,也不是值对象的范畴。

模块元素

  • XXXModuleClient 向外提供服务的类,提供静态方法。
  • ModuleInstance 整个模块的最重要的类,模块使用前需要初始化它。
  • ModuleConfig 模块功能的配置项,是否需要打开某个功能等等。
  • ModuleDependency 模块的一些依赖项,就是模块自身无法完成的功能,或者模块内并不关心这个功能。如对于直播间来说,它并不知道参加课程这些逻辑,这些逻辑应该是课程模块所关心的。
  • LaunchData 针对每次服务可配置的选项。
  • UI视图 如Box,它只关心自己的行为,以及用于向页面展示的数据。
  • ActivityOfFragment 一些Box的组装者,负责将领域模型适配成视图模型,塞给Box。
  • ILogic 所有的业务逻辑都发生在这里,它可以调用具体的服务完成业务逻辑,也可以交给领域模型实现。
  • 领域模型 针对这个模块所抽象出来的数据模型,它不是简单get、set,还包含了业务逻辑。ILogic中可以引用住聚合根,其他的实体不应该被引用住。这样不对导致对象的引用散布在各处。
  • DataSource 数据来源,可以从数据库,也可以从服务器。最终要转化成对应的领域模型。
  • 服务对象 单个领域模型无法处理的逻辑一般会交给服务对象。

模块初始化

最开始能想到的便是,在应用启动的时候便对模块进行初始化,这样做的好处,简单、快捷。但是增加了应用启动耗费的时间,也增加了应用的内存。

更为通用常见的方式是,懒加载,在模块用到的时候在进行初始化。

因为模块的核心类是ModuleInstance,所有的服务都是靠它实现的。因此,模块初始化也就是这个单例对象的初始化。这样在需要模块服务的时候通过获取这个单例,如果单例未创建,则进行初始化即可。

在模块初始化的时候,我们将初始化的流程通过一个接口,暴露给第三方使用着,使其可以参与到模块初始化流程。

定义一个ModuleConfigAndDependency接口,提供两个回调方法。

public interface ModuleConfigAndDependency {
    //自定义修改模块config配置
    void applyConfig(Builder builder);

    //自定义配置模块依赖
    void applyDependency(Builder builder);
}

在调用ModuleInstance构造器之间,先创建一个Builder,通过解析一个双方约定好的配置文件,获取ModuleConfigAndDependency的实现类的名字,通过反射创建对象。将构建ModuleInstance的Builder传递出去,完成自定义配置。在Android中,约定的配置文件可以写在清单文件中。

模块的配置和依赖

模块的配置分为两种,一种是模块初始化完成后,配置就定了。另一种,是每次启动服务时的配置。两种的区别在于,作用域不同。一个针对全局的,一个针对每次服务。

全局的通过模块初始化的时候,修改Builder。

针对每次服务的,通过LaunchData配置。 在ModuleInstance中有一个存放LaunchData的Map。

依赖和配置类似,这里就不赘述了。

模块的领域模型

为什么需要自己的领域模型呢?

  • 既然叫做领域,那它关注的重心就是自身模块的业务,然后我们可以将一些业务逻辑下沉到领域模型中。
  • 与后端的隔离,有时候移动端与后端的开发可能是并行的,也可能移动端早于后端。那么接口的DTO会频繁的变动,移动端开发不得不去配合他们,影响自身的开发效率。如果移动端做好这层隔离,那么上层业务的开发完全不会受影响,只需要修改DataSource这层即可。

领域模型的构建来自DataSource,可以从数据库、也可以从服务器,最终的传递给上层的数据必须是领域模型。

模块的视图/View层

展现给用户的是由一块块区域组合而成的,我们将这些区域称之为Box,Box所关心的就只有两个,一个是用户行为,一个是视图模型

  • 对于行为,Box只是触发行为,比如点击,比如曝光,而具体的执行逻辑,交给外部实现。
  • 对于视图模型,Box只是将其展现出来,如文字。

模块的控制层/Presenter

之前View与数据之间会有耦合性,现在交给Logic来解耦。Logic可以通过定义接口的形式调用View的变化,也可以通过Message的形式通知。而View通过ILogic定义的接口来获取需要的数据,以及逻辑。

模块的边界

每个模块都有自己的上下文,因而少不了模块与外部之间对象转换。 因此需要定义这样一层隔离机制,来完成模块内的上下文与模块外的上下文之间的流通。

可以是共享内核的形式,两个上下文依赖部分共享的模型。这种方式,模块可能要依赖一些通用Model模块。 可以是防腐层的形式,一个上下文通过一些适配和转换与另一个上下文交互。这种方式需要在模块内定义一些简单的值对象。

所以我们规定模块内的类访问权限,对于使用者,它所能访问的有以下:

  • 暴露给使用者的服务接口
  • 用于方法调用时的依赖对象类

总结

好的模块设计,应该是奔着可复用,高内聚,低耦合的方式。最重要的是灵活,可配置,易于扩展。

以上是最近一年开发,对于模块设计的理解及总结。如有不足不对的地方,请多多指点~


本文来自网易实践者社区,经作者陈柏宁授权发布。