原文链接 : MVPR: A FLEXIBLE, TESTABLE ARCHITECTURE FOR ANDROID (PT. 1)
全面的单元测试能提高内部系统的代码质量,因为系统的每一个组件都需要被测试,因此每个单元都需要在系统外被构建,在测试环境中进行测试。对对象进行单元测试需要创建该对象,提供该对象需要的依赖,并与它进行交互,最终检验测试环境的输出是否与预期一致。因此,为了让一个类易于进行单元测试,类的依赖必须明确,而且能够轻易地被替代和明确被调用和验证的责任。在软件工程领域中,这就意味着代码必须松耦合、高内聚,也就是说:设计优秀的。
Steve Freeman 和 Nat Pryce,也就是测试驱动的面向对象开发。
最近我在尝试让 Google 的 IO App 变得可单元测试,我这样做的其中一个原因是验证 Freeman 和 Pryce 在引用中对单元测试的总结。即使现在我还是没有把 IOSched 中的任何一个 Activity 重构,但我已经在重构代码的过程中感受到他们所说的东西了。
我现在在重构的 Activity 是 SessionDetailActivity,如果你一直有在关注我的话就会知道我说的是哪个 Activity,但如果你只是第一次看我的博文,你可以看看下面这张图了解下 SessionDetailActivity 的界面是咋样的。
就像我在这个系列博文的序中所说,要让 SessionDetailActivity 可被单元测试,有几个麻烦必须解决。我在这个系列的上一篇博文中说过,对它动态构建的 View 进行单元测试是一个挑战,但在那篇博文中,我提到我解决这个问题的办法并不能治本,因为在 View 和 Presenter 之间存在着循环依赖。
循环依赖是 Android 应用架构存在大问题的征兆:Activity 和 Presenter 都违反了单一职责原则,它们至少需要完成两件事:为 View 绑定数据并对用户的输入作出相应。这也是为什么 SessionDetailActivity 这个类会作为 Android 开发的 Model 被使用,使得类的代码数超过1000行。
我坚信有更好的办法架构我们的应用,在接下来的博文里,我会提出一种拥有以下特性的新架构:
将通常由 Presenter 和 Activity 负责的多重职责打破
打破一般存在于 View 间或 Activity 和 Presenter 之间的循环依赖
允许我们用构造方法对所有为用户展示数据以及相应用户输入的对象进行依赖注入
让 UI 相关的业务逻辑易于进行单元测试,而且不可能在没有必要的依赖时被构建以履行他们的职责,而且通过利用聚合和多态性修改对象的行为。
在这片博文中,我会尝试总结开发新的 Android 应用架构的原因。
Activity 和 Fragment(接下来我会统称为 Activities,但我说的也适用于 Fragment)是违反单一职责原则的典型:
处理 View 的事件
更新数据 Model
调用其他 View
与系统组件交互
处理系统事件
基于系统事件更新 View
正如 Richa 所说,这些职责大部分从 Activities 中剥离,但即使我们这样做了,Activities 还是违反了单一职责原则。即使是最简单的 Activities 还是需要将 Model 的数据和 View 绑定,并对用户输入作出相应,例如:
public class SessionDetailActivity extends BaseActivity implements
LoaderManager.LoaderCallbacks<Cursor>,
ObservableScrollView.Callbacks {
//...
@Override
protected void onCreate(Bundle savedInstanceState) {
//Responsibility 1: Responding to user's action (in this case, a click)
mAddScheduleButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
boolean starred = !mStarred;
SessionsHelper helper = new SessionsHelper(SessionDetailActivity.this);
showStarred(starred, true);
helper.setSessionStarred(mSessionUri, starred, mTitleString);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mAddScheduleButton.announceForAccessibility(starred ?
getString(R.string.session_details_a11y_session_added) :
getString(R.string.session_details_a11y_session_removed));
}
/* [ANALYTICS:EVENT]
* TRIGGER: Add or remove a session from My Schedule.
* CATEGORY: 'Session'
* ACTION: 'Starred' or 'Unstarred'
* LABEL: Session title/subtitle.
* [/ANALYTICS]
*/
AnalyticsManager.sendEvent(
"Session", starred ? "Starred" : "Unstarred", mTitleString, 0L);
}
});
//...
//Responsibility 2: Fetching and binding data to the view
LoaderManager manager = getLoaderManager();
manager.initLoader(SessionsQuery._TOKEN, null, this);
manager.initLoader(SpeakersQuery._TOKEN, null, this);
manager.initLoader(TAG_METADATA_TOKEN, null, this);
}
Google IOSched 应用中的 SessionDetailActivity 就是 Activity 即使只负责绑定数据到 View 中和响应用户输入也会变得臃肿的绝佳范例。即使我们把这部分代码从 SessionDetailActivity 中剥离,还是有一个类有700多行代码。不信我?你大可以去看看源码,Presenter 也会因为 Activity 那样的原因变得臃肿:Presenter 通常负责绑定数据以及响应用户输入,所以 Presenter 也需要像 Activity 那样通过剥离额外的职责被瘦身。
Activities 通常通过它们和 View 之间的循环依赖履行绑定数据到 View 和响应用户输入的职责(例如:作为 setContentView() 方法参数的 View)。下面是范例:
mAddScheduleButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
boolean starred = !mStarred;
SessionsHelper helper = new SessionsHelper(SessionDetailActivity.this);
showStarred(starred, true);
helper.setSessionStarred(mSessionUri, starred, mTitleString);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mAddScheduleButton.announceForAccessibility(starred ?
getString(R.string.session_details_a11y_session_added) :
getString(R.string.session_details_a11y_session_removed));
}
/* [ANALYTICS:EVENT]
* TRIGGER: Add or remove a session from My Schedule.
* CATEGORY: 'Session'
* ACTION: 'Starred' or 'Unstarred'
* LABEL: Session title/subtitle.
* [/ANALYTICS]
*/
AnalyticsManager.sendEvent(
"Session", starred ? "Starred" : "Unstarred", mTitleString, 0L);
}
});
SessionDetailActivity 持有对 mAddScheduleButton 的引用,而且 mAddScheduleButton 也持有对 SessionDetailActivity 的引用。我等会会说,这样的循环依赖限制我们通常用于 Activities 中实现 UI 相关的业务逻辑的方法。
MVP 的 Presenter 有着和它们和 View 相同的循环依赖,在我能详细解释之前,我必须简单地介绍传统 Android 应用架构中 View 和 MVP 模式中 View 的区别。
MVP 模式中的 View 就像我定义的,只是 MVP 模式三巨头其中之一,通常被定义为一个接口,而且一般会在 Activity,Fragment 或 Android 传统架构中的 View 中实现。Android 传统架构中的 View 就像它的名字,是一个 View 的子类。
使用 MVP 模式中的 View 和 Presenter 仅仅是在它们之间无形中重新创建了和 Android 传统架构中 View 和 Activities 之间相同的循环依赖。
Presenter 需要 MVP 模式中的 View 使得它们能绑定数据到 MVP 模式中的 View,MVP 模式 中的 View 需要对 Presenter 的引用,使得它能传递点击和其他 UI 相关的事件给 Presenter。Square 的博文就有存在着循环依赖的 MVP 模式的实现。
循环依赖在你想要为单元测试构建对象(或通常情况下)都会产生问题。然而,通常情况下,我们都不会把 MVP 模式的 View 和 Presenter 或 Activities 和 View 间的循环依赖当作问题,因为 Activities 和 Fragment 被系统初始化,而且因为我们并没有用依赖注入去注入 Activity 和/或 Fragment 的依赖。相反的是,我们只是初始化了 Activity 在 onCreate() 方法中需要的任何依赖:
public class MyActivity extends Activity implements MVPView {
View mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_browse_sessions);
//...
final Presenter presenter = new Presenter(this);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
presenter.onButtonClicked();
}
});
}
}
初始化在 onCreate() 方法中依赖的混合类,然而,限制我们使用组合和多态性去实现 UI 相关的业务逻辑。下面是一个你应该使用多态性实现 UI 相关的业务逻辑的例子:假设你开发了一个被用户使用的应用,而且用户在不同的等级时有不同的特权,那么他们需要通过邮件验证或回答其他用户提的问题以提高等级。我们可以想象有许多按钮用于完成依赖等级完成的不同功能,或 View 由用户等级决定的初始状态。多态性为我们提供整洁,可拓展的方式去实现这样的逻辑:我们创建一个 Presenter 用于为用户绑定不同的等级,不管用户在什么等级中,我们都能把 MVP 模式中的 View 传到特定的 Presenter 子类中,并让该子类处理相应的点击事件或者基于用户的等级呈现 UI。当然了,还有许多架构 Android 应用的方式,使得我们能够在存在 Presenter 和 MVP 模式中的 View 间循环依赖的情况下利用多态性,但这些方法都不够优雅,或者说他们为了完成单元测试作出了极大的贡献。
这篇博文剩下的篇幅已经不足以让我一一细述我记得的那些解决方法,但我能简要的说说为什么解决 MVP 模式中的 View 和 Presenter 间循环依赖的方法不理想。你可以想象我们可以只创建一个 MVP 模式的 View 或 Presenter,而没有它们履行职责所需的任何依赖。换句话说,我们可以像下面这样:
public class MyActivity extends Activity implements MVPView {
View mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_browse_sessions);
//...
final Presenter presenter = new Presenter();
//****
presenter.setView(this);
//****
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
presenter.onButtonClicked();
}
});
}
}
这样我们就能通过多态性解决上面提到的问题,但这并没有打破循环依赖。它能做的是允许我们在无效状态创建一个对象。这并不是最简洁的解决办法,把这放在 Freeman 和 Pryce 话里:
“创建或不创建,不需要尝试”
我们想要确保总是创建有效的对象,部分地创建对象然后通过设置它的属性完成它是脆弱的……
Presenter 和 Activities 违反了单一职责原则,他们常常负责绑定数据到 View 中和响应用户的输入,这些都会使 Activities 和 Presenter 变得臃肿。
Presenter 和 Activities 常常会因为他们和 View 间的循环依赖拥有多重职责,即使这样的循环引用不会带来什么问题,但这会更难以对 View 和/或 Presenter 进行单元测试,而且会限制我们使用多态性实现 UI 相关的业务逻辑。
就像我之前说的,我认为会有一种架构应用的办法不会有上面这些烈士,在下一篇博文中,我会提出可供选择的架构。
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。