关于模块化的一些思考/记录

我这里所说的“模块化”可能是更加广义的理解概念,不限于具体的实现方式;通常模块化的目的是为了使得项目更加方便的管理,易于维护,这对于多人团队共同开发同一个项目或者项目业务体积日趋庞大的情况尤为重要。这里只是我对模块化的思考做一些随笔记录,并不是具体指导教程,如有想法欢迎交流。

物理隔离

所谓的物理隔离也就是代码文件的隔离,我认为这是实现模块化的第一首要任务,即保证你的代码文件相互分隔开来,一方面从人工维护的角度的来讲,这样更加容易找到目标,另一方面也是为了避免业务不相干的代码文件混在一起。

如果是在Android开发当中,那么这些代码文件如Android Studio工程里的java文件、资源文件等,在AS工程中我们有很多方法去做物理隔离,通常的一种做法是建立不同的module(library module), 每个业务对应一个module去处理;又或者是通过Product Flavor为不同的Flavor建立不同的sourceSets, 这样也可以达到在不同文件目录中管理不同的代码文件 的效果;又或者你使用的是插件化技术,那么每个模块是作为一个独立的插件(实际上是application module)。不管哪种方式,我们首先要做的就是从代码文件上将它们隔离开。

其中的一些做法可能是人为的将这些代码文件分开,如可能是通过建立不同的文件夹或package以区分,另一些做法可能是通过编译器强制性的将代码文件分开,如分到不同的library module/插件module当中。前者可以称为物理软隔离,为啥,因人为的改动会很容易打破这种隔离束缚,比如换一个人来维护…后者可以称为物理硬隔离, 这种往往很难去打破隔离束缚,因为假如缺少或移动文件会直接导致编译出错编译不过。很显然,物理硬隔离手段会更加有效。

逻辑隔离

逻辑隔离自然是相对于物理隔离而言的,其实很多文章中把这个叫做代码边界的划分,我这里把它分成了两步。因为要做到代码边界划分首先就要物理隔离,在物理隔离完成以后,你会发现虽然代码文件是隔离开了,但是往往总是有同学会做出你意想不到的事,比如他会把你辛辛苦苦分隔开的代码类又混在一起引用。对,就是“引用”这个词,你隔离之后的结果,或者是一个library module或者是其他导入的第三方库,那么这些东西是要给人使用的,既然要给人使用那么就会产生一个问题:哪些东西该暴露,哪些东西不该暴露?假如你是一个SDK开发者,当你提供library module给别人依赖之后,里面所有public的东西都是可以被访问到的,如果引用者可以随意使用它,那从理论上来讲这样是有滥用的风险的,从逻辑上来说就是并没有实现真正的“分开”,还是跟原来混在一块的使用没啥区别: 一改全改,改一发而动全身。这时就需要逻辑隔离,逻辑隔离说白了就是要从主观上想办法进行边界的控制,具体如何做还要看业务了,比如我可能只提供一个接口,你要访问我提供的方法就必须实现这个接口传递它的实现类给我,或者有一个类我把它的所有方法变为私有的,只有我给定的构造模型生产出来的实例你才可以使用,诸如此类吧。主要的目的就是减少与外界的接触点,减少不必要的暴露,做到可控,能内部处理的就内部处理。不同于物理隔离,逻辑隔离还是主观能动性大一些,能做到什么程度完全取决于自己的把控,当然一个很好的检测手段就是当你去修改隔离出来的模块的时候,依赖它的业务模块不会因此产生特别大的动荡(接口隔离原则ISP),这就说明你边界控制做的很好。当然实际当中并不是所有的事情都如意的,可能还需要一些人为的主观自我约束,就像代码规范一样,当有些时候不能通过代码上体现约束时,就需要开发者主观上当做规则去遵守(比如某个方法你不能去调用它,但是我又没有办法去约束你不去调它,这就要靠自觉了)。

相互通信

在模块通过物理隔离和逻辑隔离开以后,会存在相互通信的问题,一般会发生在两个模块之间不能直接互相访问/引用的情况,当然即便能直接接触的,为了解耦,多数情况下也是不建议去直接引用而是通过一种通信机制去访问。在Android中可以实现这样通信的工具或者库有很多种,这并不是什么很难的问题,难的问题在于通信的协议/约定,什么意思,比如我跟你对暗号,这暗号通过一个中间人传递,这传递之人便是通信方式(可能有很多种可以是信鸽也可以是手机电话),而这暗号便是协议或者说约定。现在主要的问题是这暗号存放在哪里,A模块跟B模块通信,假如使用Android中最简单的方式,我们可以用一个全局的Handler去发送消息,发送的消息内容可能是一个int常量值或者会携带一个obj对象,那么这个常量值代表什么意思,这个约定的协议放在哪里,或者我们用一些第三方的通信框架如EventBus,它也是需要预先定义一个通信的消息实体类的。此时的协议内容的定义,既不能放在A模块也不能放在B模块,可能需要放在第三个模块当中,它被A和B模块共同引用,当协议的定义发生变化的时候,A和B模块能同时感知。这里还会有一个问题,就是存放协议的模块可能会越来越膨胀,比如C和D这时也要通信,那你可能又要去更新这个定义协议的模块的内容,随着越来越多的业务模块需要通信需要定义新的协议,这个模块就会被不断的修改,这就会发生很奇怪的现象,这个模块本身并不做业务(只是定义约定)但却成为了高频修改的模块,这也是个问题。

工具模块与业务逻辑模块分开

工具模块指的是那些没有“灵魂”的模块,要与业务逻辑模块分开定义,之所以说没有“灵魂”,意思是这些代码类是纯工具类或者说是一个组件库,只能拿来被利用,不具备业务能力,它们很有可能是你为了提代码高复用性而抽取的一些公共代码部分。基本上这部分代码是比较固定的,而业务代码则是经常在修改的代码。通常业务模块作为项目中的library module存在被app module所引用(或者是插件化的方式存在被宿主使用),而工具类则可以直接作为远程依赖被引入编译。

面向接口编程,接口和实现分离

这个其实是要求你遵守 依赖倒置原则DIP: 高层模块不应该依赖低层模块,两个都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

在Java中,抽象就是指接口或者抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或者继承抽象类而产生的就是细节,也就是可以通过new产生的对象。高层模块就是调用端,低层模块就是具体实现类。

这条OO设计原则实际上已经告诉了我们模块化之间如何正确的处理依赖关系:

  • 模块间的依赖关系通过接口或者抽象发生,它们的实现类之间不发生直接依赖关系。
  • 模块间使用的接口/抽象类不应依赖于实现类。
  • 模块间使用的实现类应依赖接口或抽象类。

2020 年注定是不平凡的一年,刚开年就惨遭新冠肺炎的疯狂肆虐,但是我相信我们的政府、相信我们的国家,最终一定能战胜病魔!2020,中国加油!

发布了96 篇原创文章 · 获赞 57 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/lyabc123456/article/details/103866965
今日推荐