写点什么

不要再给 MVP 中 Prensenter 写接口了

2016 年 8 月 22 日

译者序:有关是否要让 Presenter 实现接口这个问题并没有很多讨论。antoiolg 曾在 GitHub 上发过一个MVP 实践,最早的提交是在2014 年四月,可以说是最早的优秀范例了。他让所有的Presenter 都实现了接口,并在View 层中坚持使用接口而不是实现类。而几个月前Google 竟发布了官方MVP 实践。此码一放,众神退让。Google 的做法是:首先写一个上帝接口BasePresenter,然后在每个功能模块里都写了协议类名为…Contract,在其中封装了模块下的View 接口和Presenter 接口,同时给View 设定了泛型,就是当前协议类中Presenter:

复制代码
/**
* 这个类声明了该模块下 View 和 Presenter 的协议
* BaseView 和 BasePresenter 都是很简单的上帝接口
*/
public interface AddEditTaskContract {
interface View extends BaseView<Presenter> {
void showEmptyTaskError();
...
}
interface Presenter extends BasePresenter {
void saveTask(String title, String description);
...
}
}

这种管理方式的好处是,将 View 和 Presenter 管理起来,强化其一一对应的关系,便于操作。这也是原文没有提到的观点。总的来说,不论是否以协议类的方式呈现,现在开发者喜欢让 Presenter 继承接口。而原文观点相反。

原文链接: http://blog.karumi.com/interfaces-for-presenters-in-mvp-are-a-waste-of-time/

我们在 Karumi 已经说了很久的 MVP 了。今天,我们讨论的是是否需要给 MVP 中的 Presenter 写接口。

这是 MVP 图解:

在上面的图解中 Model 包含了所有实现业务逻辑的代码。Presenter 负责实现展示逻辑,View 是抽象化视图的接口。

为什么在这种模式下 View 需要用接口来实现?

因为我们想将 View 的实现解耦。我们需要将编写 Presentation 层的框架抽象化,使它没有外部依赖。如果需要,我们应可以很轻松的修改视图的具体实现。我们应当遵守依赖原则以便进行单元测试。请记住,为了遵守依赖原则,高层次的概念 - 比如Presenter 的实现,不能依赖任何低层次的细节,比如View 的具体实现。

为什么使用接口有益于进行单元测试?

因为为了编写单元测试,所有的代码都应该关联到你的域,而不是外部系统,比如SDK 或某个框架。

让我们通过一个Android 中登录界面的例子来解释。

复制代码
/**
* 登录用例。给出登录所需的邮箱和密码。
*/
public class Login {
private LoginService loginService;
public Login(LoginService loginService) {
this.loginService = loginService;
}
public void performLogin(String email, String password, LoginCallback callback) {
boolean loginSuccess = loginService.performLogin(email, password);
if (loginSuccess) {
callback.onLoginSuccess();
} else {
callback.onLoginError();
}
}
}
/**
* LoginPresenter,实现了和用户登录接口相关联的 Presentation 逻辑。
*/
public class LoginPresenter {
private LoginView view;
private Login login;
public LoginPresenter(LoginView view, Login login) {
this.view = view;
this.login = login;
}
public void onLoginButtonPressed(String email, String password) {
if (!areUserCredentialsValid(email, password)) {
view.showInvalidCredentialsMessage();
return;
}
login.performLogin(email, password, new LoginCallback {
void onLoginSuccess() {
view.showLoginSuccessMessage();
}
void onLoginError() {
view.showNetworkErrorMessage();
}
});
}
}
/**
* 声明了 Presenter 可以对 View 进行的操作,不依赖 View 具体实现,避免造成耦合。
*/
public interface LoginView {
void showLoginSuccessMessage()
void showInvalidCredentialsMessage()
void showNetworkErrorMessage()
}
public class LoginActivity extends Activity implements LoginView {
.........
}

请不要关注代码语法,这些都是代码片段,几乎可以说是伪代码了。

为什么这里需要 View 接口?

因为你需要在单元测试中用一个测试对象替代 View 的实现。那么为什么需要在单元测试中这么做呢?因为你可不想 mock 一个 Android SDK 然后在单元测试里使用 LoginActivity。要记住所有包含 Android SDK 的测试都不是单元测试。

一旦这里的实现清晰了,我们就需要一个接口,这样就无需依赖具体的实现了。

有的开发者还给 Presenter 设计了接口。如果我们继续按照上面的例子来写,那么实现会是这样:

复制代码
public interface LoginPresenter {
void onLoginButtonPressed(String email, String password);
}
public class LoginPresenterImpl implements LoginPresenter {
....
}

或者是这样:

复制代码
public interface ILoginPresenter {
void onLoginButtonPressed(String email, String password);
}
public class LoginPresenter implements ILoginPresenter {
....
}

这个多余的接口会造成什么问题?

恕我直言,这个接口并没有什么用处,它只是使整个开发过程更加复杂混乱。为什么这么说?

  • 看看类名。当接口是多余的时,所起的名字就会很奇怪,对代码也没有语义的价值。
  • 如果我们修改了 Presentation 逻辑,那么我们还需要修改这个接口。改好之后,我们才能更新实现。就算我们使用高级先进的 IDE,这还是很浪费时间。
  • 程序的走向很难把控。这是因为每当你在 Activity(View 的实现)中,想要进入 Presenter 时,你需要使用的是接口,但你常常想进入的是实现类。
  • 接口并没有提高项目的可测试性。Presenter 类可以通过任何 mocking 库由测试替身轻松替换,或是手动编写测试替身。我们总不能写一个依赖 Activity 并替换了 Presenter 的测试。

所以说,LoginPresenter 接口到底带来了什么呢?只有噪音啦。

我们应在何时使用接口呢?

当我们有多于一个实现时(在这里我们只有一个 Presenter 实现),我们应当使用接口。还有,当我们需要将我们的代码和某第三方库,比如某框架或 SDK 划清界线时也需要写接口。就算不使用接口,我们也可以使用 Composition 来生成抽象,但是直接使用 Java 接口自然轻松得多。我们建议在对某个概念有多个实现或是需要明确界线时使用接口。不然,还是不要添加多余代码了。记住接口的使用不是生成抽象、实现解耦的唯一方法。

那如果我想讲 View 实现与 Presenter 实现解耦呢?

你并不需要这样做。View 的实现是一个低层次的细节,Presenter 实现是一个高层次的抽象。实现细节可以依赖高层次抽象。你需要将你的域模型从执行框架中抽象出来,但你不需要反其道而行之。尝试对 View 实现与 Presenter 实现进行解耦只是浪费时间罢了。

我写了这个博客就是为了讨论这个话题的。欢迎大家评论讨论:)

PS:如果你在尝试 Android 应用和 Presenter 的另一种测试方式,我不建议你使用单元测试并使用测试替身替换 View。我更愿意使用这里所描述的方式,SUT 是整个Presentation 层,而不只是一个Presenter。(这里使用测试替身来替换用例)


感谢徐川对本文的策划和审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016 年 8 月 22 日 17:315286

评论 1 条评论

发布
用户头像
请问,如果需要对presenter进行mock呢?
2019 年 09 月 03 日 09:53
回复
没有更多了
发现更多内容

2021Java岗面试预备手册:涵盖20个技术栈​​​​,助你通往大厂的面试必备指南

Java成神之路

Java 程序员 架构 面试 编程语言

人人矿场APP开发|人人矿场系统软件开发

开發I852946OIIO

系统开发

如何拿到大厂offer——C++后台学习路线

赖猫

c++ Linux 面试 后台开发 后端开发

Laravel来信|Event

LeastCoding

laravel Event 观察者模式

泰山版震撼来袭!阿里巴巴2021年Java程序员面试指导小册已开源

Java架构追梦

Java 架构 面试 金三银四 跳槽

程序员成长第十一篇:弄懂需求

石云升

需求 28天写作 2月春节不断更

一文读懂区块链产业最新发展趋势

CECBC区块链专委会

大数据

区块链药品溯源平台-区块链医药追踪溯源

13530558032

什么!?金三银四,2021年阿里最新面试题惨遭泄露?

Java架构之路

Java 程序员 架构 面试 编程语言

话题讨论 | mongodb相比mysql拥有十大核心优势,为何国内知名度远不如mysql高?

杨亚洲(专注mongodb及高性能中间件)

MySQL 数据库 mongodb 话题讨论 分布式数据库mongodb

JAVA基础之JIT

且听且吟

区块链电子证照应用平台,区块链电子证照平台建设方案

13530558032

话题讨论 | 比特币攻击重现江湖,你准备好了吗?

程序员架构进阶

话题讨论 28天写作 2月春节不断更 话题王者 勒索攻击

算力蜂系统开发|算力蜂软件APP开发

开發I852946OIIO

系统开发

阿里/腾讯/京东等大厂Java近十万道最新面试题已被收录成册

周老师

Java 编程 程序员 架构 面试

翻译:《实用的Python编程》02_03_Formatting

codists

Python 人工智能 后端 数据结构与算法 格式化

什么是供应链,供应链有哪些核心指标

学志

技术 指标体系 供应链 电商平台

少儿学编程系列---如何使用turtle画风车

cloudcoder

2021阿里总监最新手码BAT等大厂面经!GitHub已标星86.2K

比伯

Java 编程 架构 面试 程序人生

不可思议!我靠这些笔记练手,竟然拿到了蚂蚁Java岗后端开发offer

Java成神之路

Java 程序员 架构 面试 编程语言

窝家恶补三月,字节跳动三面,终于喜提offer!分享面试感受

Java架构之路

Java 程序员 架构 面试 编程语言

全新角度剖析--iOS面试

行业缩水,光靠一份神仙般的“进阶面试宝典”,我居然拿到开发岗60K京东offer

Java成神之路

Java 程序员 架构 面试 编程语言

使用doom-emacs三个月后, 春节期间从零配置一份自己的emacs(附详细文档)

lmymirror

颠覆技术-智能合约的说明文

CECBC区块链专委会

区块链

五面阿里成功入职,复盘总结Java学习/面试自测指南开源分享

程序员小毕

Java 程序员 面试 分布式 大厂

阿里巴巴云原生应用安全防护实践与 OpenKruise 的新领域

阿里巴巴云原生

容器 运维 云原生 k8s 调度

电力行业区块链技术应用和产业布局

CECBC区块链专委会

区块链

膜拜!阿里2020全年面试总结小册Github爆火!涵盖高并发/分布式/中间件/数据库等Java全栈知识

程序员小毕

Java 程序员 架构 面试 高并发

GitHub上已获赞百万!阿里架构师10年磨一剑打造的Java面试小抄(2021版)开源分享

Java架构师迁哥

Java岗四面字节跳动成功之前,我都刷了那些面试题以及做了那些准备!

Java架构之路

Java 程序员 架构 面试 编程语言

不要再给MVP中Prensenter写接口了-InfoQ