欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

深度剖析Android MVPR架构模式(一),androidmvpr,对对象进行单元测试需要创

来源: 开发者 投稿于  被查看 41629 次 评论:27

深度剖析Android MVPR架构模式(一),androidmvpr,对对象进行单元测试需要创


本文出自:Philosophical Hacker,译文出自:开发技术前线,译者:chaossss,校对者:Mr.Simple




全面的单元测试能提高内部系统的代码质量,因为系统的每一个组件都需要被测试,因此每个单元都需要在系统外被构建,在测试环境中进行测试。对对象进行单元测试需要创建该对象,提供该对象需要的依赖,并与它进行交互,最终检验测试环境的输出是否与预期一致。因此,为了让一个类易于进行单元测试,类的依赖必须明确,而且能够轻易地被替代和明确被调用和验证的责任。在软件工程领域中,这就意味着代码必须松耦合、高内聚,也就是说:设计优秀的。

——摘自《Growing Object Oriented Software Guided by Tests》,作者:Steve Freeman和Nat Pryce

最近,我在尝试让Google的IO App变得可单元测试,我这样做的其中一个原因是验证Freeman和Pryce在引用中对单元测试的总结。即使现在我还是没有把IOSched中的任何一个Activity重构,但我已经在重构代码的过程中感受到他们所说的东西了。

我现在在重构的Activity是SessionDetailActivity,如果你一直有在关注我的话就会知道我说的是哪个 Activity,但如果你只是第一次看我的博文,你可以看看下面这张图了解下SessionDetailActivity的界面是咋样的。

wKioL1XVbP3gYsrLAAA3j10l4ds286.jpg 

就像我在这个系列博文的序中所说,要让SessionDetailActivity可被单元测试,有几个麻烦必须解决。我在这个系列的上一篇博文中说过,对它动态构建的View进行单元测试是一个挑战,但在那篇博文中,我提到我解决这个问题的办法并不能治本,因为在View和Presenter之间存在着循环依赖。

循环依赖是Android应用架构存在大问题的征兆:Activity 和 Presenter 都违反了单一职责原则,它们至少需要完成两件事:为 View 绑定数据并对用户的输入作出相应。这也是为什么 SessionDetailActivity 这个类会作为 Android 开发的 Model 被使用,使得类的代码数超过1000行。

我坚信有更好的办法架构我们的应用,在接下来的博文里,我会提出一种拥有以下特性的新架构:


1. 将通常由Presenter和Activity负责的多重职责打破;

2. 打破一般存在于View间或Activity和Presenter之间的循环依赖;

3. 允许我们用构造方法对所有为用户展示数据以及相应用户输入的对象进行依赖注入;

4. 让UI相关的业务逻辑易于进行单元测试,而且不可能在没有必要的依赖时被构建以履行他们的职责,而且通过利用聚合和多态性修改对象的行为。


在这篇博文中,我会尝试总结开发新的Android应用架构的原因。

为什么需要新的架构?

Activity/Fragment/Presenter会变得臃肿

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/Fragment/Presenter通常在View间存在循环依赖

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之间相同的循环依赖。

wKioL1XVbP3gYsrLAAA3j10l4ds286.jpg

wKioL1XVbP3gYsrLAAA3j10l4ds286.jpg

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相关的业务逻辑。

就像我之前说的,我认为会有一种架构应用的办法不会有上面这些烈士,在下一篇博文中,我会提出可供选择的架构。


用户评论