对于MVP、响应式编程以及事件总线的一些思考

来源:互联网 发布:世界十大网络病毒 编辑:程序博客网 时间:2024/04/28 07:46

最近工作的项目中使用到了一个简易版的EventBus,不过并不是我引入使用的,而项目的结构也不是MVP。我接手这个项目后,陆续根据需求改造了一些模块。

值得一提的是,在此之前我是事件总线(各种EventBus)的忠实粉丝,比如我自己写的基于注释处理自生成模板代码的RxBus,使用事件总线,可以大大降低回调复杂度,使多个模块间的通信变得简单等等。

但是,随着项目改造程度加大,我开始觉得不对劲了。

使用事件总线很可能会使项目工程的维护难度大大增加

在当前项目中,前任开发者在很多地方都使用到了事件总线(由于是一个自己写的EventBus,以下简称EBus)。其中一个最典型的例子就是:在当前的Fragment中所显示的一条数据,也可能会在其他Fragment、Activity中存在该条数据(即多个页面都有独自的数据源,但可能存在一些相同数据),而当用户在其中某个页面更改了该条数据后,将通过EBus将该数据发送到其他的Fragment、Activity中,在这些页面内收到消息后再更新各自数据源里的相同数据。

OK,如果目前持有数据源的Fragment、Activity仅有几个,那么使用EBus确实很方便。

但如果这些页面的数量增加、并且每个页面接收数据后的操作可能都不相同。同时,随着数据类型不同、项目功能增加,为了保持数据一致性、界面联动操作,这时候再继续使用EBus将许许多多的页面“串联”起来,那么麻烦来了——你会发现一个Fragment或者一个Activity将被注册了不计其数的EBus事件,并且发送事件的代码可能遍及项目的每个角落。

是的,我目前遇到的就是这样的困境——每当我要改动一个页面、一个Adapter或者一个接口,我都不得不仔仔细细地去寻找一个事件的发起点、接受者,去一个一个地改造。

表面上,EBus确实在写代码的过程中带来了极大的便利;但在后期维护代码的过程中,随着EBus的使用次数增多,反而使得各种功能点难以跟踪和控制。

后来,我看到一篇文章:放弃RxBus,拥抱RxJava(一):为什么避免使用EventBus/RxBus,其中提到的点我身同感受:

  • 由于是Event,在发布Event的时候就要做好准备可能并没有人接受这个Event, Subscribe的时候也要做好准备可能永远不会收到Event。Event无论顺序还是时间上都某种程度上不太可控。如果你将数据寄托在Event上然后就直接在Android其他生命周期方法中直接使用这个数据或成员变量。那么很有可能你会得到NPE。

  • EventBus看似将你的程序解耦,但是又有些过了。我们常常使用EventBus传数据,这已经是Dependency级别的数据而不是一个可以被解耦出来的模块。这样就造成了过多EventBus的代码会造成代码结构混乱,难以测试和追踪,违背了解耦的初衷。这时如果有意或无意的造成了Nested Event。那情况会更糟。

由于目前项目时间较紧,我并没有多余的时间去一一重构,而且EBus遍布全局的调用使得我不敢随便“乱动”——这种牵一发而动全身的操作,改动一个地方往往就意味着你要改动更多的地方和承受可能因为没找到EBus涉及到的地方而出现的隐藏Bug。

但是这件事给了我一个提醒:也许适当使用EventBus之类的事件总线,确实可以在写代码的时候少处理很多东西,但是随着项目架构的演进、考虑后期维护的难易度,也许事件总线并不是模块通信解耦的最优解。

这跟响应式编程有关吗?

说到响应式编程,相信很多人都会想起RxJava。是的,RxJava是一款实现函数响应式编程的利器,清晰的链式结构、简洁的线程切换、强大的操作符等等,上手后实在难以割舍。而其中最重要的是,RxJava等的响应式编程明确了一个事件的处理逻辑:即有一个事件的发送者(产生者),然后若干响应者(观察者)仅需去观察发送者获取事件,继而响应,即完成一次事件。

所以,EventBus之类的事件总线,其实也是响应式编程的一种实现,但EventBus和RxJava之间的不同点在哪里?为什么EventBus容易过度解耦?

我认为不同点在于RxJava的事件流概念和链式调用。RxJava并不是单纯的将事件发射、接收,而是将一个事件形成一次事件流(Stream),事件不再是“发射”,而是经过RxJava生成的Observable“通道”,让事件(数据)根据特定的通道流动到响应者,而链式调用则最大化地将该过程体现给开发者,开发者可以非常直观地看到事件的发送者源头在哪里,中间经过了什么操作,最后事件被谁相应,一目了然。

当然这也和开发者的设计息息相关。如果开发者过度的将事件的发送者提取、包装,恐怕只会变成一个简易的RxBus而已。

这跟MVP又有什么关系?

目前MVP的许多架构都是通过RxJava进行搭配的。经过上面的思考,我确实认为事件总线不宜多用,而应该多利用响应式编程来设计解耦。

于是,我又看到了一篇文章:拥抱RxJava(四):动手做一个Full Rx的 注册界面

是的,这篇文章的作者就是上一篇文章的作者。作者参照Jake Wharton提出的一种架构(暂称为Rx架构),来尝试实现这种一切通过RxJava控制的响应式架构。

目前来看,这种架构再次将App的开发抽象化,即万物皆事件。界面的点击、变动和数据源的变化都看作是Observable,相应地,界面处和数据处都建立彼此对应的Observer,然后通过中间层(暂称)将两者通过订阅联系起来。这样做的好处有三点:

  1. 界面、数据仅需考虑自己的事件发送和接收事件后的处理,无需理会多余的问题
  2. 事件源和订阅者之间的事件流经过的操作都由中间层控制
  3. 用RxJava统一了从界面到数据、数据到界面的所有操作,统一化使得解耦操作更为便捷

这样一看,感觉就是MVP架构的再一次升华,我当即就开始尝试在自己的MVP demo中实现操作。但很快,我发现一些问题。

  • 为了实现界面事件的发送,比如Click事件,都需要用Observable将其封装一次,如RxBinding。如果一个界面的事件源较少,这没有问题;但如果一个界面的事件源较多,比如10个以上,那意味着需要生成10个以上的Observable,而Observable是在界面启动后就开始监听,这意味着一旦界面复杂化,一个界面就会生成可观的Observable在内存里,再考虑到往往一个Activity包装有多个Fragment的情况,Observable的数量将难以估计。

  • 同时,RxBingding之类的会对控件产生强引用,并且需要手动dispose才能释放,过多的使用RxBingding需要多费心机去管理其生命周期。

有时候,App的性能和内存,以及使用的技术都是需要衡量的。考虑到Android中界面的事件较多(如Click、LongClick、Touch等),这些即便在Activity、Fragment中通过listener也不会太影响架构的实行,而且相对之下简单的listener对比Observable反而显得轻量(复杂的回调地狱则要考虑设计问题,这时候用Observable代替好处更多),所以我最终还是选择目前的架构,不尝试全套的Rx架构。

但是这让我对MVP的架构有更深的了解:即V层依然是事件的发起、接收者,虽然需要通过调用presenter来实现;而P层同样的,虽然它同时拥有M、V两层,但应当是尽量组合两者,尤其是使用RxJava来处理时更要尽可能组合两者的事件流,而不是单独处理V层或者M层的问题,当V层存在不涉及有数据交互的操作时,应该由V层自己解决,反之M层亦然,P层应尽可能只是一个中间的组合者;另一方面,若V层存在数据操作等一些情况,哪怕不涉及M层,也应当由P层来操作。

总结

回到一开始的问题:如果目前有一个数据变动,而其他的Activity、Fragment等页面也要随着这个数据变动,并且其对应的数据源也要得到更新,怎么办?

其实问题的本质就是数据的传递和界面的相应,那么我们先看一个特殊情况:

  • 一个Activity和一个Fragment
    这种情况,如果数据变动是单向的,则对应设置回调;如果是双向的,彼此设置回调又容易陷入回调地狱。那么,在两者间设置一个中间层(类),每当改动一个数据,就往里面设置改动的数据;同时,两者都添加回调到其中的回调list,中间类每当收到数据则回调;这样回调就被中间层解耦了。用代码举个例子:
public class DataRepository {    private static final ArrayList<OnDataChangeListener> ListenerList = new ArrayList<>();    private Object mCurrentData;    public void update(Object data) {        mCurrentData = data;        for (OnDataChangeListener listener : ListenerList)            if (listener != null)                listener.onUpdate(mCurrentData);    }    public void addDataChangeListener(OnDataChangeListener listener) {        ListenerList.add(listener);    }    public void removeDataChangeListener(OnDataChangeListener listener) {        ListenerList.remove(listener);    }    public interface OnDataChangeListener<T> {        void onUpdate(T data);    }}

在Activity和Fragment都分别注册这个回调即可

那么如果是使用RxJava呢?前面的链接文章中有提到一个理念,即数据源(或者说M层)应当暴露对应的Observable以供观察者监听。所以,在这种情况、并且在使用MVP架构的情况下,应当由两者的Model层暴露一个Observable,然后两者的M层来监听,P层做组合。因此,链接文章的作者也给出一种方法,就是上述的变种:

public class UserRepository {    private User actualUser;    private Subject<User> subject = ReplaySubject.createWithSize(1);    /**     *     *Get User Data from wherever you want Network/Database etc     */    public Observable<User> getUpdate(){        return subject;    }    public void updateUser(User user){        actualUser = user;        subject.onNext(actualUser);    }}
  • 多Activity、多Fragment

    到这里,这就是我之前遇到的问题。仔细一想,如果参考上述的模式,在M层都暴露一个Observable出来,反而显得Observable太多;另一方面,使用上述的中间层作响应,虽然解耦了,但貌似也是EventBus的复杂实现而已。

    是的,任何问题都应该按照需求来思考,有的问题也许有最优解,但有的问题永远只能选择较好策略,有得必有失。

    所以,EventBus并非不可取,在某些需求条件下,用EventBus反而更合适。但在我目前的架构下,尽量不使用EventBus反而比较合适;同时,使用以上的方法,虽然代码量稍有提升,但是对于追踪数据来源、后期维护等操作依然具有较好的可读性和维护性。

最后,RxJava虽然是好东西,但不是非要为了用RxJava而用,有的场景虽然可以用RxJava解决,但也许为了其他因素考虑反而并不是最好实现,凡事应当按具体问题具体分析。

原创粉丝点击