模块化架构设计
前言
架构设计是一个不断演变的过程,当项目较小,或者项目刚刚起步的阶段,我们往往不需要关注架构设计,只有当软件膨胀到一定程度,我们才会针对当前业务,设计出适合当前阶段的架构。所以有的项目就会出现不断膨胀,不断重构的情况。那为什么不一开始就设计一个大而全的架构?我认为有以下几点:
1、架构是对当前业务进行的一层抽象,当业务不稳定,或者在高速迭代的过程中,我们没办法定义出稳定的抽象层。导致哪怕架构设计好,也会在业务迭代的过程中被破坏。
2、架构的过程事实上也是一个解耦的过程,而解耦越充分,相应的也会导致代码量膨胀、代码开发难度变大,所以在项目不是太复杂的阶段,适当的耦合可以提高开发效率。
适用项目
那什么样的情况适用架构设计呢?我认为以下几种情况可以考虑使用模块化重构项目:
1、多人协作开发的情况
2、项目足够复杂,且当前架构下很难进行维护
3、有插件化或者按需加载需求的项目
目标
架构目标
1、安全性和可靠性
软件运行不引起系统事故的能力,在模块化中当某一个模块出现异常,框架可以保证在不使用该模块能力的情况下正常运行。
2、可伸缩性和可扩展性
对于新增模块,或者移除模块项目的改动较少,我们可以通过较少的改动线性的增加需要,大多数情况下只需要简单的配置。
3、可定制性
可临时对架构能力进行扩展,或者能力的重新实现,来达到不同环境下使用的需要。这样的架构设计可以适用于更多的项目中使用。
4、可维护性
保证模块内高内聚,模块间低耦。
模块目标
1、可独立运行
模块可以作为一个独立的APP,运行使用。
2、可插拔
模块的插拔对项目无影响,只是能力的可用和无响应的区别。
3、可移植
模块可以通过简单的适配移植到其他项目中使用,与运行环境完全隔离。
架构设计
架构图
架构图解读
和大多数模块化不同,架构设计中我采用了两层抽象,一方面将项目APP与具体业务解耦,另外一方面将单个模块与框架能力解耦。APP层不再关心业务实现,它只负责业务模块的初始化、生命周期的调用、及适配业务模块所依赖的接口能力。
项目在编译期不再依赖具体的业务模块,而只在打包时候才会将业务模块打包进APK,所以在开发过程中我们无法感知到其他业务模块的存在。
在开发具体的业务模块时,我们无法感知到当前模块在哪个项目中,我们只能依赖框架能力的抽象接口来实现编程,另外我们也无法知道我们的行为要和其他哪一个模块进行交互,我们只能定义一组接口来描述我们的行为。当模块在具体的环境中运行时,由环境来适配模块所依赖的服务,就像使用第三方库的过程中需要进行初始化一样。
项目结构
项目结构解读
我将项目模块分为,业务模块,核心模块,业务组件模块来进行管理,关于他们的定义如下:
1、业务模块,是项目组成的基本单元,可以理解为一个项目由N个业务模块组合起来。另外业务模块也是完全独立的,可以看成一个小型APP,代码是高内聚的。
2、核心模块,是项目中可沉淀,可输出部分。它不依赖于具体的业务,可以在任何android项目中使用,比如我们常常使用的第三方库。
3、业务组件模块,和核心模块不同的是,它不能对外输出,是都当前项目能力的封装,依赖于当前项目的环境,它与核心模块一起属于框架能力层的具体实现。
Gradle构建项目管理
概述
对比上面的架构图和项目结构图,我们可以发现它们并不能完全对应得上。具体的业务模块缺少抽象层,APP仍然依赖具体的业务模块。我们需要针对每一个业务模块,实现一个抽象模块,然后APP依赖抽象模块,抽象模块打包时才依赖具体模块。但是这样做会让事情变得很麻烦,我们需要自动生成API模块,另外我们也需要针对每一个业务模块在编译时生成单独可运行的APK模块。具体的代码如下。
代码实现
/** * 依赖业务模块时,生成对业务模块的抽象模块,并依赖 * @param moduleName * @return */ def includeWithApi(String moduleName) { //先正常加载这个模块 include(moduleName) //生成测试APP includeWithDebugApk(moduleName) //找到这个模块的路径 String originDir = project(moduleName).projectDir String parentDir = project(moduleName).getParent().projectDir //原模块的名字 String originName=project(moduleName).name //这个是新的路径 String targetDir = "${parentDir}/build/${originName}-api" //todo 替换成自己的公共模块,或者预先放api.gradle的模块 //这个是公共模块的位置,我预先放了一个 新建的api.gradle 文件进去 String apiGradle = rootProject.projectDir // 每次编译删除之前的文件 deleteDir(targetDir) //复制.api文件到新的路径 copy() { from originDir into targetDir exclude '**/build/' exclude '**/res/' exclude '**/main/' exclude '**/test/' exclude '**/androidTest/' include '**/*.java' } //直接复制公共模块的AndroidManifest文件到新的路径,作为该模块的文件 copy() { from "${apiGradle}/ApiAndroidManifest.xml" into "${targetDir}/src/api/" rename("ApiAndroidManifest.xml", "AndroidManifest.xml") } //复制 gradle文件到新的路径,作为该模块的gradle copy() { from "${apiGradle}/api.gradle" into "${targetDir}/" } //删除空文件夹 deleteEmptyDir(new File(targetDir)) //修改包名 fileReader("${targetDir}/src/api/AndroidManifest.xml", "%REPLACE","com.vivo.${originName}.api"); //重命名一下gradle def build = new File(targetDir + "/api.gradle") def renameBuild = new File(targetDir + "/build.gradle") if (build.exists()) { build.renameTo(renameBuild) } fileReader("${renameBuild.getAbsolutePath()}","%REPLACE", "${project(moduleName).getParent()}:$originName") // 重命名.api文件,生成正常的.java文件 renameApiFiles(targetDir, '.api', '.java') //正常加载新的模块 include "${project(moduleName).getParent()}"+":build:"+"$originName"+"-api" } private void deleteEmptyDir(File dir) { if (dir.isDirectory()) { File[] fs = dir.listFiles(); if (fs != null && fs.length > 0) { for (int i = 0; i < fs.length; i++) { File tmpFile = fs[i]; if (tmpFile.isDirectory()) { deleteEmptyDir(tmpFile); } if (tmpFile.isDirectory() && tmpFile.listFiles().length <= 0) { tmpFile.delete(); } } } if (dir.isDirectory() && dir.listFiles().length == 0) { dir.delete(); } } } private void deleteDir(String targetDir) { FileTree targetFiles = fileTree(targetDir) targetFiles.exclude "*.iml" targetFiles.each { File file -> file.delete() } } private def renameApiFiles(root_dir, String suffix, String replace) { FileTree files = fileTree(root_dir).include("**/*$suffix") files.each { File file -> file.renameTo(new File(file.absolutePath.replace(suffix, replace))) } } //替换AndroidManifest里面的字段 def fileReader(path, name,sdkName) { def readerString = ""; def hasReplace = false file(path).withReader('UTF-8') { reader -> reader.eachLine { if (it.find(name)) { it = it.replace(name, sdkName) hasReplace = true } readerString <<= it readerString << '\n' } if (hasReplace) { file(path).withWriter('UTF-8') { within -> within.append(readerString) } } return readerString } } /** * 依赖业务模块时,针对每个业务模块生成一个Debug模块,该模块可以独立运行,是一个完整的APK * @param moduleName */ def includeWithDebugApk(String moduleName){ //找到这个模块的路径 String originDir = project(moduleName).projectDir String parentDir = project(moduleName).getParent().projectDir //原模块的名字 String originName=project(moduleName).name //这个是新的路径 String targetDir = "${parentDir}/App/${originName}-app" // 每次编译删除之前的文件 deleteDir(targetDir) if(!new File("${originDir}/src/apk/build.gradle").exists()){ new File(targetDir).deleteDir() return null } copy() { from originDir into targetDir exclude '**/build/' exclude '**/main/' exclude '**/api/' exclude '**/test/' exclude '**/androidTest/' include '**/*.java' include '**/*.xml' } copy() { from "${originDir}/src/apk/AndroidManifest.xml" into "${targetDir}/src/apk/" } //复制 gradle文件到新的路径,作为该模块的gradle copy() { from "${originDir}/src/apk/build.gradle" into "${targetDir}/" } //删除空文件夹 deleteEmptyDir(new File(targetDir)) //重命名一下APk TO Main def apkFile = new File(targetDir + "/src/apk") def renameApkFile = new File(targetDir + "/src/main") if (apkFile.exists()) { apkFile.renameTo(renameApkFile) } //正常加载新的模块 include "${project(moduleName).getParent()}"+":App:"+"$originName"+"-app" } include ':app', ':base', ':component:business:account', ':component:business:security', ':component:ui:card', ':component:business:basics', ':component:ui:map', ':component:ui:animation', ':core:other', ':component:business:buryingpoint', ':core:utils', ':core:ui', ':core:thread', ':core:net', ':core:db' includeWithApi ":business:carmode" includeWithApi ":business:hotel" includeWithApi ":business:transit" includeWithApi ":business:trip" includeWithApi ":business:wea