如下内容为原创,欢迎转载,转载请注明
来自每天博客:http://www.cnblogs.com/tiantianbyconan/p/5892671.html
html
原文:https://medium.com/picnic-engineering/tackling-technical-debt-with-mvp-67e805ed5103#.couu0d5i0前端
免责申明:这篇博客并非讲关于怎么使用MVP的方式(上帝知道关于这些已经太多了)去写Android代码。而仅仅是个人我的经验,关于怎么转换咱们的表现层到MVP架构来帮助咱们解决一些累积的技术债务,并且在这个过程当中也会帮助咱们的app从一个原型转变成一个更具维护性的产品。java
任何从事Android工做足够久、项目足够大的开发者最有可能达到一个点,他们面对他们的代码库,以为应该有更好的实现方案。咱们在Picnic也是同样,在Android app开发开始后大约八个月,咱们到达了的那一刻,就在咱们向公众发布第一个版本的时候。android
这一刻正好是在咱们app推出的时候这也并不意外。直到那时,咱们以一个很是快的速度在前进,不断敲打咱们的键盘,从零开始构建一个完整的产品,尝试新的东西,结合用户反馈到咱们的app中,在天天的基础上增长和丢弃特性。git
为了跟上公司的速度咱们砍掉了这里那里的边边角角。这样的工做对咱们来讲很好,这也是咱们可以在这么短的时间内构建这个app的缘由之一。可是正如预期那样,最后这些决定的影响开始以技术债务的形式显示出来。幸运的是这些技术债务是在数月以内创建的,在app的性能和稳定性上面并无任何真正的影响。反而咱们是在其它领域开始注意到它:github
咱们已经有了一个很好的想法和一个易于理解的架构,用于网络层、错误处理和app内部模块通讯。可是像大多数Android开发者,咱们会对把太多的逻辑放进Activity和Fragment中会产生内疚。编程
旁注:这是Android开发者的共同的问题,而做为开发者须要在黑暗中摸索,由于Google对这个话题保持沉默。咱们从它们那里获得的第一个(算是)官方回复是来自Android团队的一个开发者在 Google+ post,说明咱们应该把核心的Android API做为一个‘系统框架’,意味着他们会带咱们手把手地到达Android核心的组件(Activity, BroadcastReceiver, Service 和 ContentProvider)。以后咱们作什么都是看咱们本身了。并且就在最近,Google终于提供了一系列的例子用来解决关于怎么构建一个Android app的共同问题,它着重于MVP。尽管只是beta,可是它能够在这里查看:Android Architecture Blueprints。后端
不管如何,这实际上是一件好事,由于这意味着咱们能够自由地去实验任何咱们喜欢的方式,而不是被强制在一个平台遵循一个特定的模式。api
如今讲回咱们的故事… 除非你处在Android开发世界的远古时期,你应该会注意到表现层架构是如今的热门。关于最好的方式是什么,每一个人甚至连他妈妈彷佛都有本身的观点。工做中标准的Android方式(相似MVC),到MVP,到经过data-binding的MVVM,全部的方式都沿用了 Uncle Bob 的 clean architecture。每一种方式围绕同意或者反对的意见都有一些有趣的讨论,可是有一件事咱们要明确知道,那就是咱们应该避免喝Kool-Aid(译者注:这里是比喻,表示很是愚昧地接受信奉某种观点或者思想)和指望其中一种是银色子弹(译者注:这里是比喻为具备极端有效性的解决方法)而后永远解决全部问题。网络
当在考虑怎么去重构咱们的表现层时,咱们已经有近一年的代码库的积累,咱们很清楚咱们的缺陷在哪里,而后咱们须要使用一个新的实现(以上主要表示一些可以解决咱们的技术债务的点)来达到咱们的目标。咱们在虚拟的项目中试玩了一些,体验了各类方法的不一样之处,而后最终决定使用MVP。从它的核心来讲,MVP自己仅仅是一个概念,而Android框架,根据设计,并不强制任何模式,咱们能够自由地选择实际的实现细节。
在Android团队中,首先咱们是不过分工程的信徒,让代码随着时间的推移天然地发展,而不是过早地在试图为本身不可预知的将来作准备的抽象之上增长抽象。正由于这个缘由,咱们选择另外一风味的MVP,使得能够最低限度地保持咱们的抽象层次。在代码级别,这意味着有一个单独的接口来表示View。全部其它的组件都是具体的类。你可能会问本身,怎么会只有View使用接口?考虑到咱们迫切的须要,这是真正受益于这样的接口的惟一的组件,由于咱们实际上有不一样的具体的Views来共享相同的接口。因此在咱们的案例中,这里的一个接口将被容许咱们去重用Presenters。一些MVP实现建议给全部组件(M,V和P)设置接口。尽管这样会工做得很完美,可是咱们在较早的阶段并不提倡,由于添加以后的成本是代码可读性和维护性,尤为是当咱们考虑到新入职对MVP陌生的初级开发者的时候,好处超过面向接口编程的方式。
相比其余,MVP实现是很是标准的。View(Activity,Fragment或者一个自定义View)负责创造和维护Presenter,而Presenter处理各类业务相关的逻辑(数据获取,存储,格式化等等),而后根据须要经过更新UI回调到View。在咱们的案例中,数据层已是至关模块化了,构造用于表示数据模型的POJOs,以及一个预先存在的控制层用于处理网络通讯。
这是一个很是标准的MVP设置,也由于它很简单,咱们能够在几周的时间内替换几乎咱们的全部的UI代码。由于咱们已经存在独立的数据层来处理全部与后端的API交互,因此真正须要重构的只是Views和Presenters的交互。
在重构的过程当中,咱们也学习了一些可能会派得上用场的东西:
生命周期:由于Presenter是View建立的,咱们须要确保彻底地理解View的生命周期,特别是由于它将最有可能去处理状态更新和异步数据。举个例子,每个Presenter应该在View destroyed的状况下有一个取消异步任务的方式,或者应该在用户暂停或者恢复视图事件时重置到原始状态等等。最后但一样重要的是,当View已经被销毁,试图从Presenter去更新View元素,始终须要注意可怕的NPEs。
保持Views尽量地愚蠢:咱们的Views应该再也不包含任何业务相关的逻辑。它应该只包含Android框架inflate和设置View的这些最低限度的东西。任何用户交互应该派发到Presenter。根据经验,若是你的views有任何其它方法去更新UI元素或者响应用户触发的事件,那么你可能应该去检查它们的实现。
保持Presenter尽量地纯粹:这一点,咱们的意思时你应该尽量地避免有Android相关的代码在你的presenters中。为这些组件编写纯粹的单元测试,而不须要使用其它如Robolectric等测试框架,这明显地获得了简化。这明显提及来比作起来容易得多,由于你终归会在某些地方遇到这种状况,举个例子,你将须要有一个Context的引用用来好比数据加载、访问strings文件等等。
那么,说了那么多,最终的结论是什么呢?总的来讲,我很高兴使用了MVP。它必定程度上帮咱们解决了咱们快速开发所累积的技术债务,而后,咱们准备了更多来针对第二阶段的开发。
一些值得一提的事情:
测试数:在重构以前,测试的数量用两只手均可以数得过来。这是一个巨大的任务来针对包含了全部逻辑如执行数据解析、格式化、网络请求、错误处理和管理本身的生命周期的Activity编写测试。仅思考若是在这些条件下编写测试就足以让咱们去寻找其它的方式了。一旦转换咱们的第一份代码到MVP,对此编写测试就变得碎片化了。经过一个清晰的合同明确什么View可以处理,咱们能够把本身的代码与Android UI框架隔离开,而后仅仅测试实际调用的是不是正确的方法,并给出每一个测试场景。如今实际的业务相关逻辑被放置在Presenters中,由于它们绝大多数都不须要有Android OS相关的认知(或者小部分相关的能够被mocked),咱们也能够针对它们编写很是有效率的单元测试,所以,在过去几个月里,咱们的测试用例从原来的10增长到900,并且还在增加中。
可预见性:这个是有一点软度量,可是很是强大的一点。针对UI,咱们选择并坚持一个通用的模式,我能够在代码库中得到可预见的好处。这意味着,不管是哪一种开发者眼里的UI元素(Activity,Dialog,Fragment等等),若是理解其中一个怎么工做,那也就能理解全部怎么工做。打开一个就算不是你写的文件也再也不会遇到让你以为惊喜的东西了。明确规定职责,每一单个的UI组件都遵循相同的明确的模式。让新入职的新开发者从第一天起就是高效的,这是很是宝贵的。
咱们别忘记MVP并不仅是用于表现层,可是做为前端开发人员,这里花费了咱们太多的时间。因此努力去寻找一个解决方案来给咱们带来更好的可预见性和在新的开发者加入咱们的时候也能让咱们快速迭代是值得的。通过全面的考虑,咱们能够有把握地说MVP是能够帮助咱们达到这个目标的一个重要的里程碑。
P.S. 若是你仍然渴望看到一些源代码,这里有一个咱们MVP实现‘忘记密码’用例的剥离下来的版本,展现MVP组件与用户的交互,用户点击‘重置密码’按钮进入他们的邮件地址(为保持代码的简洁,Android模版代码已经移除):
// BasePresenter.java (Base class for all our Presenters) public abstract class BasePresenter<V> { private WeakReference<V> mView; public void bindView(@NonNull V view) { mView = new WeakReference<>(view); } public void unbindView() { mView = null; } public V getView() { if (mView == null) { return null; } else { return mView.get(); } } protected final boolean isViewAttached() { return mView != null && mView.get() != null; } } // IForgotPasswordView.java (view interface) public interface IForgotPasswordView { void showLoading(); void hideLoading(); void setEmailText(String email); void showEmailNotValidError(); void showPasswordRequestOk(String message); void showPasswordRequestFail(); } // ForgotPasswordFragment.java (view implementation) public class ForgotPasswordFragment implements IForgotPasswordView, View.OnClickListener { // Triggered by the user clicking a button public void onResetPasswordClick() { String email = mEmailEditText.getText().toString(); // Forward all logic to the Presenter mPresenter.requestPasswordChange(email); } } // ForgotPasswordPresenter.java public class ForgotPasswordPresenter extends BasePresenter<IForgotPasswordView> { public void requestPasswordChange(String email) { if (!Utils.isEmailValid(email)) { // Make sure the view is still alive before trying to access it if(isViewAttached()) { getView().showEmailNotValidError(); } } else { requestPasswordChangeAsync(email); } } private void requestPasswordChangeAsync(String email) { // Update the view's UI elements if(isViewAttached()) { getView().hideKeyboard(); getView().showLoading(); // Call our API (results are posted back on an EventBus) api.forgotPassword(email); } } // Subscription to the event bus @Subscribe public void onEvent(final Event event) { if (isViewAttached()) { // Update the view's UI elements getView().hideLoading(); switch (event.getType()) { case FORGOT_PASSWORD_OK: getView().showPasswordRequestOk((String) event.getData()); break; case FORGOT_PASSWORD_FAILED: getView().showPasswordRequestFail(); break; } } } }