Android架构设计之运行时依赖

来源:互联网 发布:遗传算法python实现 编辑:程序博客网 时间:2024/06/05 01:06

Fuck the dependence

很多时候我都想fuck thedependence!功能越来越多,类越来越多,协作越来越多,依赖也就越来越多。繁杂、混乱、庞大的依赖关系,导致代码可读性差、难以维护和扩展。我一度认为软件设计所有问题的罪魁祸首就是依赖,相反,拥有简洁明了、层次清晰依赖关系的代码,至少从设计上来说差不到哪去。

依赖方式

依赖无非就是两种,引用依赖和静态依赖,也就是对象依赖和类依赖。举个例子,比如我通过ContextManager.getContext()方法获取一个Context对象,然后调用该对象的startActivity方法,那么这里存在两个依赖,第一是静态依赖ContextManager类,第二是引用依赖Context类的一个对象。一般的,依赖大部分是引用依赖,因为很多静态依赖也是为了去获取引用。

依赖的建立

静态依赖很简单,调用了静态方法或者访问了静态字段,那就建立了依赖,否则没有依赖。

引用依赖的建立分为对象的创建和引用的传递两个过程,一共有三种方式。第一种,自己new出一个依赖的对象,拿到引用后使用。第二种,通过参数传进来,拿到引用后使用,常见的是通过构造方法和setXXX方法。第三种,通过静态依赖一个能够获取到所依赖对象引用的类(可能是同一个类,可能引发对象创建),来获取引用并使用,常见的如XXXManager.getInstance。

此标题只是个标签,表示后续的内容非常关键!!!

引用依赖的定义与实现

一般的,我们更倾向于依赖现有的东西而不去定义依赖。比如,一个Downloader类内部能监听下载失败的onDownloadFaile事件,然后需要startActivity跳转到失败界面,而context正好能startActivity,所以我们把Context传进来,把引用保存在一个mContext字段中,然后在需要的时候调用mContext.startActivity,没毛病、非常完美。

那么问题来了,Downloader类究竟需要依赖什么?是Context、还是startActivity、还是showFaileUI、还是onDownloadFaile?也就是说除了上边的依赖mContext对象外,这个场景还可以有另外三种设计,一是定义IStartActivity接口,然后由外部实现并传入,二是定义IShowFaileUI接口由外部实现并传入,三是定义IDownloadFaile接口由外部实现并传入。采用哪种方式好呢?其实好坏主要与Downloader类的职责范围有关,如果Downloader类的职责是只负责下载、不管UI,那就应该定义IDownloadFaile接口;如果其职责包含下载失败时的UI控制,那就应该定义IShowFaileUI接口;如果其还包含UI显示的职责,那么就可以直接将mContext传进来。定义IStartActivity接口不管怎么说,都很难有非常贴切的场景。

再看上边的例子,无论如何定义Downloader的依赖,最终都是要调用Context对象的startActivity方法,也就是实现是不变的,只是在概念上要加以区分。依赖的定义与实现,在一些场景下容易混淆,不加区分的合二为一。区分的关键在于明确类职能与类依赖,在定义任何类时都假定不知道依赖的具体实现,只知道自己依赖什么,然后去定义依赖;这种方法看似掩耳盗铃,实际上却是保障设计高内聚、低耦合代码的好习惯。

引用依赖的树形结构

理想的依赖层次是单向树状的,大对象负责创建小对象并依赖小对象,更大的对象创建大对象并且依赖大对象;反过来小对象不依赖大对象。如下图:


上图的依赖层次非常优雅,但是,实际上是不可能实现的。上图中BigC类依赖SmallCA类,如果BigC只是需要调用SmallCA类的getDownloadTasks方法获取下载任务,这种依赖是非常优雅的,因为这只是调用一个方法并获得一个返回值。但是,如果BigC需要调用SmallCA类的startDownloadTask方法,并且需要得到成功或失败的返回的话,依赖就变的复杂啦。因为下载是异步的,不能像getDownloadTasks一样直接返回,只能在SmallCA类内部下载完成或失败时回调到BigC中,通知成功或失败,这相当于形成了SmallCA对BigC的反相依赖;更糟糕的是,反相依赖一旦形成,就会对整个单向树形依赖的结构产生很大的冲击,因为SmallCA通过反向依赖通知BigC结果时,BigC可能并不能处理此结果,而是还要在反相依赖VeryBig进行通知,最终形成双向树形。

程序的运行表现为方法的调用,方法的调用导致运行时的依赖,当方法的调用全部在一个线程中并且以树形的结构进行时,依赖层次必然也是优雅的单向树形。但是当方法的调用在多个线程中穿插,或者单个线程中的方法调用不是单向树形时,依赖关系就会相应的变的复杂。上边的描述可能比较复杂,简单来说就是,当你调用一个引用依赖的方法时,调用这个方法所应该达到的效果并没有在调用完这个方法时就马上体现出来,相反,效果可能在另外一个线程运行到一定的节点时才体现出来,或者在本线程运行到一定的节点才体现出来;又因为效果没有马上体现出来,那就需要在体现出来的时候进行通知,要通知就要传回调过去,传了回调就会形成反相依赖。

引用依赖与事件驱动

如果一个类存在一些调用后不能马上起效果的方法(包括构造方法),并且在起效果后还需要反相通知的话,那么我们称这个类具有事件驱动能力。具有事件驱动能力的类必然会对其构建者形成反相依赖(通过静态依赖发送事件出去的除外),不然其事件将没有意义。

如果你最先想到的是那些需要启动线程去完成职能的类,那你就大错特错了,因为你所定义的所有需要监听系统事件的类,都具有事件驱能力,别看它们都在主线程。比如,想要初始化一个Button,就必须传递给它一个OnClickListener,因为你初始化这个Button希望达到的效果是让它监听点击事件,初始化这个操作执行完后并没有达到预期效果;当它达到效果并通过OnClickListener回调时,已经是在其他很远的调用节点了(Looper)。类似的,其他任何自己写的监听系统事件的类都有事件驱动能力(这是废话)。

具有事件驱动能力的类越多,引用依赖就越复杂。严重的情况下,你甚至不知道一个对象会被哪些对象依赖,一个方法是从哪里调用过来的。所以,要想代码有好的引用依赖层次,一定要非常重视事件类,并在其反相依赖上面优化设计。

引用依赖之双向树形

上文中提到,由于具有事件驱动能力的类不可能只有一个,所以单向树形的引用依赖是不存在的。那么双向的树形够用吗?答案是肯定的,双向树形依赖的结构能适用于所有设计,而不必要把树上升到图。难点是,你怕不怕麻烦。。。


如上图,如果出现SmallAA依赖SmallBA的情况,这就不是树形了,而是变成了图。要想保持住树形的依赖结构,上级类就要代理所有下级类需要代理的全部依赖和职能。正常情况下,下级类的大部分依赖都会被上级类实现、无需代理,下级类的大部分职能都只被上级类使用,也无需代理;但是需要代理的往往还有很多。那么,想要树形依赖的话,就代理吧,android studio中有快捷键,编码成本不会高出多少。

前边提到,只有具有事件驱动能力的类,才需要反相依赖;没有事件驱动能力的类不要反向依赖上级,若有数据上的依赖可以通过方法参数传入,无需保存在字段中。对于需要反相依赖的,最好定义自身的依赖为一个接口,由上级实现并传入。

关于引用依赖

最常见的就是依赖注入了,其实只要建立依赖关系,就要进行依赖注入,这是个比较大的概念。比如通过构造方法传一个依赖进去,这就是注入;只不过现在有人把它框架化了,效果就是在对象的创建和传递两个方面,都更方便了,具体可以去专门查一下。

关于引用依赖一个不得不谈的问题就是:通过静态依赖管理引用依赖。常见的,如单例;复杂点的,管理一些引用的集合,然后通过调用静态方法并传入key值获取引用,提供静态方法的类一般叫做XXXManager;逼格高一点的,不直接管理引用,而是先让被引用的类实现一些特定的服务接口,然后管理接口实例的引用(其实是同一个对象的引用),就叫微服务式的吧。所有这些方法吧,有好处也有坏处,这里不详细去说,但不建议整体架构上过于依赖,特定的一个模块上就见仁见智了。

原创粉丝点击