代码中心化问题

将一个大型的项目拆分成多个Module或者新开的组件化项目,想要的预期是这些module之间是平级的关系。这样一来就可以使得业务相对集中,每个人都可以专注在一件事上。

同时,代码的耦合度也会随之降低,达到高度解耦状态,因为同级的module不存在依赖关系,在编译上就是隔离的,这会让组件间的依赖非常清楚,同时也具有更高的重用性,组件强调复用,模块强调职责划分。他们没有非常严格的划分。

达到可复用要求的模块,那么这个模块就是组件。每个组件的可替代性、热插拔、独立编译都将可行。

代码中心化在Android组件化中的问题体现

貌似Android的组件化是非常简单且可行的,AS提供的module创建方式加gradle.properies 自定义属性可读,或者ext全局可配置的project属性亦或kotlin dsl 中kotlin的语法糖都为我们提供了application和library的切换。

然后将代码放在不同的仓库位置最好是单独git仓库级别的管理隔离,就能达到我们想要解决的一系列问题。

然而事情并不是想象的那么简单...

一些列的问题接踵而至,于我而言影响最深的就是应用设计时使用映射型数据库,导致集成模式和组件模式中复用出现问题;最终使用注解配合Java特性生成代码,虽然不完美但是依然解决了此问题。正当我为了胜利欢呼的时刻,一篇《微信Android模块化架构重构实践》,文章地址如下所示:

https://mp.weixin.qq.com/s/6Q818XA5FaHd7jJMFBG60w

随即闪现出了一个重要且紧急的问题,代码中心化的问题。

这个问题是怎么出现的呢?在微信Android模块化架构重构实践中是这样描述的。

然而随着代码继续膨胀,一些问题开始突显出来。首先出问题的是基础工程libnetscene和libplugin。基础工程一直处于不断膨胀的状态,同时主工程也在不断变大。同时基础工程存在中心化问题,许多业务Storage类被附着在一个核心类上面,久而久之这个类已经没法看了。此外当初为了平滑切换到gradle避免结构变化太大以及太多module,我们将所有工程都对接到一个module上。缺少了编译上的隔离,模块间的代码边界出现一些劣化。虽然紧接着开发了工具来限制模块间的错误依赖,但这段时间里的影响已经产生。在上面各种问题之下,许多模块已经称不上“独立”了。所以当我们重新审视代码架构时,以前良好模块化的架构设计已经逐渐变了样。

再看他们分析问题的原因:

翻开基础工程的代码,我们看到除了符合设计初衷的存储、网络等支持组件外,还有相当多的业务相关代码。这些代码是膨胀的来源。但代码怎么来的,非要放这?一切不合理皆有背后的逻辑。

在之前的架构中,我们大量适用Event事件总线作为模块间通信的方式,也基本是唯一的方式。使用Event作为通信的媒介,自然要有定义它的地方,好让模块之间都能知道Event结构是怎样的。这时候基础工程好像就成了存放Event的唯一选择——Event定义被放在基础工程中;接着,遇到某个模块A想使用模块B的数据结构类,怎么办?

把类下沉到基础工程;遇到模块A想用模块B的某个接口返回个数据,Event好像不太适合?那就把代码下沉到基础工程吧……

就这样越来越多的代码很“自然的”被下沉到基础工程中。

我们再看看主工程,它膨胀的原因不一样。分析一下基本能确定的是,首先作为主干业务一直还有需求在开发,膨胀在所难免,缺少适当的内部重构但暂时不是问题的核心。另一部分原因,则是因为模块的生命周期设计好像已经不满足使用需要。之前的模块生命周期是从“Account初始化”到“Account已注销”,所以可以看出在这时机之外肯定还有逻辑。

放在以前这不是个大问题,刚启动还不等“Account初始化”就要执行的逻辑哪有那么多。而现在不一样,再简单的逻辑堆积起来也会变复杂。此时,在模块生命周期外的逻辑基本上只能放主工程。

此外的问题,模块边界破坏、基础工程中心化,都是代码持续劣化的帮凶...

看完之后就陷入了沉思,这个问题不就是我们面临的问题吗?不仅是在组件化中,在很多形成依赖关系的场景中都有此类问题。

假设有user组建和分享组件,分享组件需要user组件提供数据。

具体是怎么体现的呢,我们来看一组图:

解决方式为分享组件依赖user组件,能解决问题,假设,有一个组件A,需要引用分享组件,就必须依赖分享组件和user组件,这就一举打破了组件编译隔离的远景,组件化将失去香味儿。

将user组件中的公共数据部分下沉到base组件,分享组件依赖base组件即可实现数据提供,然而当非常多的组件需要互相提供数据时,将出现中心化问题,只需要分享组件的B组件不得不依赖base组件,引入其他数据。也就造成了代码中心化下沉失去组件化的意义。

怎么解决代码中心化问题

微信面对这个痛心疾首的问题时发出了“君有疾在腠理,不治将恐深” 的感慨,但也出具了非常厉害的操作-.api化。

这个操作非常高级,做法非常腾讯,但是此文档中只提到了精髓,没有具体的操作步骤,对我们来讲依然存在挑战。

什么是代码中心化问题的.api方案

先看一下具体的操作过程是什么样的。上图3中,我们使用某种技术将user组件中需要共享数据的部分抽象成接口,利用AS对文件类型的配置将(kotlin)后拽修改为.api ,然后再创建一个同包名的module-api 组件用来让其他组件依赖,分享组件和其他组件以及自身组件在module模式下均依赖该组件,这样就能完美的将需要共享的数据单独出去使用了。

SPI 方式实现

这个有点类似SPI(Service Provider Interface)机制,具体可参考:

https://www.jianshu.com/p/46b42f7f593c

(来源上面的文档)

大概就是说我们可以将要共享的数据先抽象到接口中形成标准服务接口,然后在具体的实现中,然后在对应某块中实现该接口,当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;然后利用 ServiceLoader 来加载配置文件中指定的实现,此时我们在不同组件之间通过ServiceLoader加载需要的文件了。

利用ARouter

利用ARouter在组件间传递数据的方式+gralde自动生成module-api组件,形成中心化问题的.api化。假设我们满足上述的所有关系,并且构建正确,那我们怎么处理组件间的通信?

Arouter 阿里通信路由

@Route(path = "/test/activity")
public class YourActivity extend Activity {...
}跳转:ARouter.getInstance().build("/test/activity").withLong("key1", 666L).navigation()
// 声明接口,其他组件通过接口来调用服务
public interface HelloService extends IProvider {String sayHello(String name);
}// 实现接口
@Route(path = "/yourservicegroupname/hello", name = "测试服务")
public class HelloServiceImpl implements HelloService {@Overridepublic String sayHello(String name) {return "hello, " + name;}@Overridepublic void init(Context context) {}
}//测试
public class Test {@AutowiredHelloService helloService;@Autowired(name = "/yourservicegroupname/hello")HelloService helloService2;HelloService helloService3;HelloService helloService4;public Test() {ARouter.getInstance().inject(this);}public void testService() {// 1. (推荐)使用依赖注入的方式发现服务,通过注解标注字段,即可使用,无需主动获取// Autowired注解中标注name之后,将会使用byName的方式注入对应的字段,不设置name属性,会默认使用byType的方式发现服务(当同一接口有多个实现的时候,必须使用byName的方式发现服务)helloService.sayHello("Vergil");helloService2.sayHello("Vergil");// 2. 使用依赖查找的方式发现服务,主动去发现服务并使用,下面两种方式分别是byName和byTypehelloService3 = ARouter.getInstance().navigation(HelloService.class);helloService4 = (HelloService)ARouter.getInstance().build("/yourservicegroupname/hello").navigation();helloService3.sayHello("Vergil");helloService4.sayHello("Vergil");}
}

假如user组件的用户信息需要给支付组件使用,那我们怎么处理?

ARouter可以通过上面的IProvider注入服务的方式通信,或者使用EventBus这种方式。

data class UserInfo(val uid: Int, val name: String)
/**
*@author kpa
*@date 2021/7/21 2:15 下午
*@email billkp@yeah.net
*@description 用户登录、获取信息等
*/
interface IAccountService : IProvider {//获取账号信息 提供信息*fun getUserEntity(): UserInfo?
}//注入服务
@Route(path = "/user/user-service")
class UserServiceImpl : IAccountService {//...
}

在支付组件中

IAccountService accountService = ARouter.getInstance().navigation(IAccountService.class);
UserInfo bean = accountService. getUserEntity();

问题就暴露在了我们眼前,支付组件中的IAccountService和UserInfo从哪里来?

这也就是module-api 需要解决的问题,在原理方面:

将需要共享的数据和初始化数据的类文件设计为.api文件

打开AS-> Prefernces -> File Types找到kotlin(Java)选中在File name patterns 里面添加".api"(注意这个后缀随意开心的话都可以设置成.kpa)

举例:

data class UserInfo(val userName: String, val uid: Int)
interface UserService {fun getUserInfo(): UserInfo
}

生成包含共享的数据和初始化数据的类文件的module-api组件

这步操作有以下实现方式。

  • 自己手动创建一个module-api 组件 显然这是不可取但是可行的

  • 使用脚本语言shell 、python 等扫描指定路径生成对应module-api

  • 利用Android 编译环境及语言groovy,编写gradle脚本,优势在于不用考虑何时编译,不打破编译环境,书写也简单

module-api 脚本

找到这些问题出现的原理及怎么去实现之后,从github上找到了优秀的人提供的脚本,完全符合我们的使用预期。

def includeWithApi(String moduleName) {def packageName = "com/xxx/xxx"//先正常加载这个模块include(moduleName)//找到这个模块的路径String originDir = project(moduleName).projectDir//这个是新的路径String targetDir = "${originDir}-api"//原模块的名字String originName = project(moduleName).name//新模块的名字def sdkName = "${originName}-api"//这个是公共模块的位置,我预先放了一个 新建的api.gradle 文件进去String apiGradle = project(":apilibrary").projectDir// 每次编译删除之前的文件deleteDir(targetDir)//复制.api文件到新的路径copy() {from originDirinto targetDirexclude '**/build/'exclude '**/res/'include '**/*.api'}//直接复制公共模块的AndroidManifest文件到新的路径,作为该模块的文件copy() {from "${apiGradle}/src/main/AndroidManifest.xml"into "${targetDir}/src/main/"}//复制 gradle文件到新的路径,作为该模块的gradlecopy() {from "${apiGradle}/api.gradle"into "${targetDir}/"}//删除空文件夹deleteEmptyDir(*new* File(targetDir))//todo 替换成自己的包名//为AndroidManifest新建路径,路径就是在原来的包下面新建一个api包,作为AndroidManifest里面的包名String packagePath = "${targetDir}/src/main/java/" + packageName + "${originName}/api"//todo 替换成自己的包名,这里是apilibrary模块拷贝的AndroidManifest,替换里面的包名//修改AndroidManifest文件包路径fileReader("${targetDir}/src/main/AndroidManifest.xml", "commonlibrary", "${originName}.api")new File(packagePath).mkdirs()//重命名一下gradledef build = new* File(targetDir + "/api.gradle")if(build.exists()) {build.renameTo(new File(targetDir + "/build.gradle"))}// 重命名.api文件,生成正常的.java文件renameApiFiles(targetDir, '.api', '.java')//正常加载新的模块include ":$sdkName"}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()}}/**
* rename api files(java, kotlin...)
**/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 = falsefile(path).withReader('UTF-8') { reader ->reader.eachLine {if (it.find(name)) {it = it.replace(name, sdkName)hasReplace = true}readerString <<= itreaderString << '\n'}if (hasReplace) {file(path).withWriter('UTF-8') {within ->within.append(readerString)}}return readerString}
}

使用

includeWithApi ":user"

Democomponent-api地址为:

https://github.com/kongxiaoan/component-api

关注我获取更多知识或者投稿

原文链接https://juejin.cn/user/2365804752418232

细究Android开发代码中心化所带来的问题相关推荐

  1. Android 组件化代码中心化问题之.api化方案

    theme: channing-cyan highlight: a11y-light 一.代码中心化问题 将一个大型的项目拆分成多个Module或者新开的组件化项目,想要的预期是这些module之间是 ...

  2. 黑客破译android开发代码真就那么容易?

    2019独角兽企业重金招聘Python工程师标准>>> 很多程序员辛辛苦苦开发出的android开发代码,很容易就被黑客翻译了. Google似乎也发现了这个问题,从SDK2.3开始 ...

  3. TokenGazer评级丨Monero:XMR缓慢发行损害矿工利益,去中心化治理带来社区分裂风险...

    2019年2月21日,TokenGazer发布了区块链项目Monero评级报告.当前TokenGazer往期的项目评级报告.深度研究报告.加密货币月报等均已收录在官方网站:www.tokengazer ...

  4. 深度 | 区块链对人工智能的变革:去中心化将带来数据新范式

    近年,从围棋到人类水平的语音识别,人工智能(AI)研究者终于在他们几十年一直努力探索的领域取得了突破.取得突破进展的关键一点是研究者们可以收集巨量的数据并「学习」这些数据,从而将错误率降低到可接受范围 ...

  5. 票据撮合区块链系统开发-去中心化的新型票据交易展现形式的系统

    一.系统简介 票据撮合系统是基于区块链技术打造的一款去中心化的新型票据交易展现形式的系统.主要为供需撮合,信用评级,分布式监管,数据存证和智能交易等.同时应用区块链技术与当前纸票,电票的优势相结合,将 ...

  6. android开发工具汉化版,【开源】EA极光 - 中文安卓开发工具

    原标题:[开源]EA极光 - 中文安卓开发工具 项目介绍 EA-Code是由业余爱好者编写的一款:中文安卓app开发工具. 工具完全由易语言+jdk开发,目前版本是2.0版本. 亮点介绍 1. 程序界 ...

  7. 无规矩不成方圆,代码规范到底有多重要?(附阿里最新Android开发代码规范手册)

    前言 代码规范是我们每个程序员要做的事,假设我们按照自己的喜好来写代码,那么很可能出现的问题就是我看不懂你的代码或者你看不懂我的代码,这样会给后续维护形成巨大的障碍.这个时候问题来了,如何让代码不分你 ...

  8. 区块链开发实战:如何从零打造一个去中心化应用

    编译 | 区块链大本营 整理 | reason_W 区块链会是一场革命吗? 今天,很多成功的互联网企业都是在以中介者的形式存在.比如Google--你和互联网之间的中介,比如亚马逊--买家和卖家之间的 ...

  9. Web3 更应关注类型,而非去中心化程度/链游数字藏品开发

    Web3 的倡导者认为去中心化规模是空前的.过度的中心化会阻碍协调,削弱自由.民主和经济活力,去中心化是一种补救措施.但这个词本身过于模糊,无法成为最终目标.完成工作需要正确的去中心化,而我们担心We ...

最新文章

  1. vim的简单介绍与使用
  2. eclipse中生成的html存在中文乱码问题的解决方法
  3. C语言经典例83-求0—7所能组成的奇数个数
  4. 分隔list,让页面成多行多列遍列
  5. arcgis python脚本筛选与线共边的面_ArcGis Python脚本——遍历输出面或折线要素的折点坐标...
  6. python实现多表格合并_python 如何把两个表格数据,合并为一个呢?
  7. 异步数据加载和Tab选项卡
  8. centos7下yum安装mysql
  9. ssh远程登录Jupyter notebook(七月GPU服务器)
  10. 机器学习基石PLA相关
  11. 深度学习图像标注工具
  12. IBM/ORACLE/EMC在银行业的重要性
  13. 如何免费下载百度文库文档
  14. Burp Suite设置浏览器代理
  15. Android反编译工具与反编译步骤及常见问题
  16. 学会这三款软件,可以轻松完成录音转文字操作
  17. Windows系统必备的软件
  18. ValueError: Classification metrics can‘t handle a mix of continuous-multioutput and multiclass targe
  19. 训练时评估指标无变化,且预测值都一样
  20. 生命不可承受之重, 请关爱过劳人员

热门文章

  1. 微信小程序配置实现中英文国际化语言切换
  2. python-Excel多个表格合并
  3. 删除oracle所有表外键,Oracle中查询、禁用、起用、删除表外键
  4. eICIC和FeICIC
  5. java实现RabbitMQ消息队列
  6. 海贼王英文版 ONE PIECE百度网盘
  7. java系列之:获取当前时间和当前时间上一个小时的时间
  8. 学UG编程一定要把握的曲面驱动的办法、切削区域、切削类型
  9. 周易六十四卦——乾卦
  10. HDU - 1873 看病要排队(优先队列)