201710 TDD Coding Practice String Transformer - xiaoxianfaye/Courses GitHub Wiki

1 Problem

实现一个基于GUI的应用——字符串转换器。

用户在“Source String(源字符串)”文本框中输入一个英文字符串,然后选择所需要的转换器,点击“Apply(应用)”按钮,系统会按照用户所选择的转换器链对输入字符串进行转换,并将结果显示在“Result String(结果字符串)”文本框中。

目前有3个转换器:Upper(转换为大写)、Lower(转换为小写)和TrimPrefixSpaces(去除前缀空格)。

举个例子,用户输入“ hello, world. ”,并依次添加了Upper和TrimPrefixSpaces转换器到转换器链中,点击Apply按钮后的界面可参考下图:

string transformer

2 Showcase & Discuss

请学员展示自己之前的设计思路和实现方式,大家可以互相点评、讨论。以学员为主,讲师为辅。

3 Guide

《The Test Bus Imperative: Architectures That Support Automated Acceptance Testing》
Author: Robert C. Martin

Robert C. Martin也是《Clean Code》、《Agile Software Development, Principles, Patterns, and Practices》等书的作者。

《测试总线势在必行——设计支持自动化验收测试的架构》
译者:孙鸣

邓辉、孙鸣同时也是《Agile Software Development, Principles, Patterns, and Practices》、 《Balancing Agility and Discipline》、《Erlang In Anger》、《Learning You Some Erlang for Great Good!》等书的中文译者。

  • 《Agile Software Development, Principles, Patterns, and Practices》:《敏捷软件开发:原则、模式与实践》
  • 《Balancing Agility and Discipline》:《平衡敏捷与规范》
  • 《Erlang In Anger》:《硝烟中的Erlang》
  • 《Learning You Some Erlang for Great Good!》:《Erlang趣学指南》

3.1 Preface

3.1.1 自动化验收测试

敏捷方法,特别是测试驱动开发实践,已经唤醒了软件业对于自动化验收测试的认识。人们现在已经知道,在版本发布冲刺阶段的进度压力下,让大量的测试人员疯狂地手工运行那些枯燥乏味的测试脚本是根本不能保证软件质量的。

3.1.2 基于UI的自动化测试

市面上可以买到很多工具,这些工具可以帮助测试者编写通过用户界面来进行系统测试的自动化脚本。这样的工具就像一个机器人,它们被编程用来模拟测试人员——它们揿下按钮,选择菜单项,勾上复选框,在文本框里输入数据,然后观察屏幕。

这种自动化测试方法的问题在于:

  • 测试运行缓慢,无法频繁运行。
  • 测试是用一种和码字类似的语言写出来的,使得测试难以理解。
  • 测试非常脆弱,界面上每个微小的改变都会造成多个测试的失败或者无法运行。

3.1.3 测试总线

大概一个世纪以前,电话公司设计了包含内置测试装置的电话交换机,解决了类似的问题。这样的装置,又称作测试总线,能够让电话公司在晚间自动测试每条电话线,这样远在用户发现问题前,他们就把故障和对服务质量的影响给消除了。

软件开发商也开始采用类似的策略,用内置的测试总线来构架软件系统。他们使用类似Fit以及FitNesse (www.fitnesse.org)这样的工具,用一种业务人员易于理解的说明性语言来描述测试。

不过,要想构建出这样的自动化验收测试,在架构设计层面,至少要做到三个主要的隔离:

  • 绕过UI
  • 隔离测试总线
  • 隔离数据库

3.2 Three Major Architectural Separations

3.2.1 Bypassing the UI

在软件系统中,测试总线指的是一组API,通过这组API能够方便地编写单元测试和验收测试。这些测试描述了系统行为。单元测试描述了模块行为,而验收测试描述了功能特性行为。

测试总线的存在意味着系统中存在一些相应的结构,能够让测试去访问一些承载它们所描述行为的模块和子系统。例如,想要编写绕过UI去操作底层业务规则的测试,就得有这样一个API,UI 和测试都可以使用这个API调用业务规则。此外,这个API必须还要独立于UI。

test bus bypassing ui 1

在上图中,我们可以清晰地看到,测试和UI是可以互换的。它们使用同样的API,因此,测试可以通过所有的真实操作去驱动系统并指明所要求的行为细节。

test bus bypassing ui 2

然而,有些UI富含逻辑,很容易将业务规则包含进去。例如,有些系统会在UI中校验数据或者执行一些重要计算。在客户-服务器以及基于WEB的系统中,这种情况尤其普遍。此时,采用一般的设计策略往往会使得业务规则难以测试。不过,如果开发团队把可测试性作为一种强制的架构要求,那么他们就必须得提供能够让测试访问业务规则的机制。一种常见的解决方案就是在UI中创建另一个API来把校验、计算规则与UI的底层细节隔离

显然,这种API会约束UI的设计方式以及设计者使用客户端工具(例如JavaScript)的方式。设计者必须找到一种使得测试可以方便地访问客户端业务规则的方法,这种方法就是:把客户端业务规则和UI进行隔离,使得测试总线API可以调用这些业务规则。分离UI和业务规则一直以来都是一个好的设计目标:“模型—视图隔离原则告诉我们,模型(领域)对象不应当和视图对象有直接的依赖关系”。强制的测试总线要求会把好的设计目标变成一个必须要实现的需求

3.2.2 Isolating the Test Bus

基于每个API写的测试应当仅仅测试该API所涵盖的东西。针对Presentation API编写的测试不应该直接测试业务规则API涵盖的内容。原因和不要通过UI去测试业务逻辑一样。如果你通过Presentation API去测试业务规则,那么Presentation 层就会变得难以改变,因为这些改变会破坏测试。

这里,我们再一次看到,把测试总线当成一种强制要求会迫使我们达成好的设计目标。在子系统和层级间进行解耦一直以来都是良好设计的特征,不过当我们用自动化测试来明确系统的行为时,这种解耦就变成了一个需求,而不仅仅只是一个设计目标。

通过被隔离的API(而不是UI)运行这些测试极大地加快了测试的运行速度,使得测试可以在每次构建后都运行,一天能运行很多次。

3.2.3 Separating the Database

数据库是测试运行慢的另外一个原因。在大型数据库上的操作会比较慢,而同样的操作在小型数据库上就可能快很多。这里有一个简单的加速测试的策略,就是在一系列小型的、预先准备好的数据库上运行测试。测试系统只在每次测试前创建这些数据库,测试运行完毕即删除它。

即便是小型数据库,也需要通过磁头的旋转从磁盘上读写数据。这些磁盘操作和RAM上的同样操作相比,慢得不是一个数量级。所以,另外一个加速测试运行的策略就是从应用中将数据库分离出去,在测试中用一个只在RAM中存在的数据库替换它。

test bus separating database

上图展示了我们可以创建另外一个API来达到这个目标,业务规则可以使用这个API,无论是真实的数据库还是RAM中的数据库都来实现这个API

速度不是这种隔离带来的唯一好处。在RAM中运行测试用例能够让测试完全控制数据库中的内容。测试首先使用一个空白的数据库,调用特定的启动函数将RAM 数据库设置到一个已知的状态。而且,如果测试在一个RAM 数据库上运行,那么它们对数据的修改是不会持久化的,这使得测试既是独立的又是可重复运行的。对测试速度,测试可重复性以及测试独立性的要求使得这个分离成为了一个至关重要的架构需求。

3.3 Separation in the Small

在一个真实的测试驱动环境中,对隔离的要求会延伸到更低的层次,从主要的子系统到模块,到类,甚至到方法。作为编写验收测试的业务分析师,质量保证专家,和测试人员,以及编写单元测试的开发人员,将软件隔离成测试可以独立访问和操作的单元,已经成为一个急迫的需求。而这又进一步要求架构师,设计人员以及开发人员具有更高、更好的面向对象设计技能。面向对象的设计为我们提供了工具,原则以及模式,使得以上提到的自动化测试需要的那些隔离能够达成。

当我们把可测试性作为一个强制的架构设计要求时,它会迫使我们去遵循好的设计原则并降低我们系统的耦合

4 Analysis

分析:定义清楚问题是什么。

4.1 Problem Domain

String Transformer based on UI (基于UI的字符串转换器)

4.2 UI Elements

界面要素包括:

  • Source String:源字符串,单行文本框、可编辑
  • Result String:结果字符串,单行文本框、不可编辑、可拷贝选中文本
  • Available Transformers:可用转换器,Upper|Lower|TrimPrefixSpaces,列表、单选
  • Transformer Chain:转换器链,列表、单选
  • Add >>:添加转换器到转换器链,按钮
  • Remove <<:从转换器链移除转换器,按钮
  • Remove All:从转换器链移除所有转换器,按钮
  • Apply:将转换器链应用到源字符串,按钮
  • Exit:退出,按钮

4.3 Business Processes

业务流程包括:初始化、构建转换器链、将转换器链应用到源字符串。

4.3.1 Init

初始化完成后:

  • 在可用转换器列表中依次呈现Upper、Lower和TrimPrefixSpaces三个条目,并选中第一个转换器。
  • 转换器链列表、源字符串文本框、结果字符串文本框均为空。

4.3.2 Build Transformer Chain

构建转换器链包括:添加转换器、移除转换器和移除所有转换器。

4.3.2.1 Add a Transformer

4.3.2.1.1 Normal Business Process 1: Add the transformer which is not the last

在可用转换器列表中选中不是最后的一个转换器,点击Add按钮,选中的转换器被添加到转换器链列表末尾,转换器链列表选中新添加的转换器。可用转换器列表中的转换器条目不变,选中之前选中的转换器的下一个转换器。

4.3.2.1.2 Normal Business Process 2: Add the transformer which is the last

在可用转换器列表中选中最后一个转换器,点击Add按钮,选中的转换器被添加到转换器链列表末尾,转换器链列表选中新添加的转换器。可用转换器列表中的转换器条目不变,选中第一个转换器。

4.3.2.1.3 Abnormal Business Process 1: Add a transformer which has been already existed in the chain

在可用转换器列表中选中一个在转换器链列表中已存在的转换器,点击Add按钮,提示“待添加的转换器在转换器链中已存在”。转换器链列表中的转换器条目不变,选中已存在的转换器。可用转换器列表中的转换器条目不变,选中之前选中的转换器。

4.3.2.1.4 Abnormal Business Process 2: Add a transformer but none of the available transformers is specified

在可用转换器列表中未选中任何转换器,点击Add按钮,提示“请在可用转换器中指定一个转换器”。转换器链列表中的转换器条目不变,选中之前已经选中的转换器。可用转换器列表中的转换器条目不变,选中第一个转换器。

4.3.2.2 Remove a Transformer

4.3.2.2.1 Normal Business Process 1: Remove the transformer which is not the last when the chain has more than one transformers

当转换器链列表包含多于一个的转换器时,在转换器链列表中选中不是最后的一个转换器,点击Remove按钮,选中的转换器被移除出转换器链。可用转换器列表中的转换器条目不变,选中被移除的转换器。转换器链列表选中之前选中的转换器的下一个转换器。

4.3.2.2.2 Normal Business Process 2: Remove the transformer which is the last when the chain has more than one transformers

当转换器链列表包含多于一个的转换器时,在转换器链列表中选中最后一个转换器,点击Remove按钮,选中的转换器被移除出转换器链。可用转换器列表中的转换器条目不变,选中被移除的转换器。转换器链列表选中第一个转换器。

4.3.2.2.3 Normal Business Process 3: Remove the transformer when the chain has only one transformer

当转换器链列表仅包含一个转换器时,在转换器链列表中选中这个转换器,点击Remove按钮,选中的转换器被移除出转换器链,转换器链列表为空,无选中项。可用转换器列表中的转换器条目不变,选中被移除的转换器。

4.3.2.2.4 Abnormal Business Process 1: Remove a transformer when the chain is not empty but none of the transformers in the chain is specified

转换器链列表不为空,但在转换器链列表中未选中任何转换器,点击Remove按钮,提示“请在转换器链中指定一个转换器”。可用转换器列表中的转换器条目不变,选中之前已经选中的转换器。转换器链列表中的转换器条目不变,选中第一个转换器。

4.3.2.2.5 Abnormal Business Process 2: Remove a transformer when the chain is empty

转换器链列表为空,无选中项,点击Remove按钮,提示“转换器链为空”。可用转换器列表中的转换器条目不变,选中第一个转换器。

4.3.2.3 Remove All Transformers

4.3.2.3.1 Normal Business Process 1: Remove all transformers when the chain is not empty

转换器链列表不为空,无论转换器链列表是否选中转换器,点击Remove All按钮,所有转换器均被移除出转换器链,转换器链列表为空,无选中项。可用转换器列表中的转换器条目不变,选中第一个转换器。

4.3.2.3.2 Abnormal Business Process 1: Remove all transformers when the chain is empty

转换器链列表为空,无选中项,点击Remove All按钮,提示“请指定转换器链”。可用转换器列表中的转换器条目不变,选中之前选中的转换器。

4.3.3 Apply Transformer Chain

4.3.3.1 Normal Business Process 1: Apply the transformer chain

输入合法的源字符串,构建好非空的转换器链,点击Apply按钮,将转换器链中的转换器从上到下依次应用到源字符串上,得到最终的结果字符串。

4.3.3.2 Abnormal Business Process 1: Apply the transformer chain but the source string is empty

构建好非空的转换器链,但未输入源字符串或源字符串为空,点击Apply按钮,提示“请输入源字符串”,焦点定位到源字符串文本框。

4.3.3.3 Abnormal Business Process 2: Apply the transformer chain but the source string is illegal

构建好非空的转换器链,但输入了非法的源字符串,例如包含中文字符等,点击Apply按钮,提示“请输入合法的源字符串”,焦点定位到源字符串文本框,并全选高亮当前文本。

4.3.3.4 Abnormal Business Process 3: Apply the transformer chain but the transformer chain is empty

输入合法的源字符串,但转换器链为空,点击Apply按钮,提示“请指定转换器链”。可用转换器列表中的转换器条目不变,选中第一个转换器。

5 Design

设计:问题分析清楚以后,提出解决问题的逻辑框架。

5.1 Layered System

根据《测试总线势在必行》“Bypassing the UI”一节中的Figure 2,设计如下:

design layered system

  • View(视图层):Layout、Accept User Input / Output to User
  • Presenter(表示层):Client-side Presentation、Validation、Calculation
  • Business Logic(业务逻辑层):Business Logic

5.2 OOD

5.2.1 Presenter

5.2.1.1 Test Scenario

design presenter test scenario

5.2.1.2 Product Scenario

design presenter product scenario

5.2.2 BusinessLogic

design businesslogic

5.2.3 Global View

design global view

6 Implementation

6.1 Instructions

  1. TDD三步军规、小步。
  2. 测试:
    • 始终选择当前最有价值的测试用例;
    • 测试三段式:Arrange、Act、Assert;
    • 不要忘了测试代码也需要重构;
    • 合理使用TestSuite。
  3. 规范:
    • 代码目录规范:testsrc、src;
    • 类名命名规范:XyzTest、Xyz。
  4. 每次修改代码,都运行一下测试用例,注意红绿绿的节奏。
  5. 写代码应做到:
    • 简洁明了:概念定义清晰、准确;简明清晰地表达语义;
    • 表达到位:层次分明、表现意图;跟问题领域的概念、规则相吻合;
    • 安全可靠:状态、时序管理得当;具有必要的测试安全网络。
  6. 开始写一个测试用例前,先让学员思考如何写测试、如何验证。

6.2 Business Processes Summary

从交付角度,以一个业务流程为单位,将所有业务流程按端到端(View-Presenter-BusinessLogic)逐一实现。

业务流程汇总如下:

1 Init

2 Build Transformer Chain
  2.1 Add a Transformer
      2.1.1 Normal Business Process 1: Add the transformer which is not the last
      2.1.2 Normal Business Process 2: Add the transformer which is the last
      2.1.3 Abnormal Business Process 1: Add a transformer which has been already existed in the chain
      2.1.4 Abnormal Business Process 2: Add a transformer but none of the available transformers is specified
  2.2 Remove a Transformer
      2.2.1 Normal Business Process 1: Remove the transformer which is not the last when the chain has more than one transformers
      2.2.2 Normal Business Process 2: Remove the transformer which is the last when the chain has more than one transformers
      2.2.3 Normal Business Process 3: Remove the transformer when the chain has only one transformer
      2.2.4 Abnormal Business Process 1: Remove a transformer when the chain is not empty but none of the transformers in the chain is specified
      2.2.5 Abnormal Business Process 2: Remove a transformer when the chain is empty
  2.3 Remove All Transformers
      2.3.1 Normal Business Process 1: Remove all transformers when the chain is not empty
      2.3.2 Abnormal Business Process 1: Remove all transformers when the chain is empty

3 Apply Transformer Chain
  3.1 Normal Business Process 1: Apply the transformer chain
  3.2 Abnormal Business Process 1: Apply the transformer chain but the source string is empty
  3.3 Abnormal Business Process 2: Apply the transformer chain but the source string is illegal
  3.4 Abnormal Business Process 3: Apply the transformer chain but the transformer chain is empty

6.3 ViewImpl in Which Business Processes not Included

包括界面控件和布局但不包括业务流程的ViewImpl类。使用Java Swing库实现界面控件和布局。

代码详见ViewImpl(Only Layout, Modify Package).java。注意可能需要修改包路径。

运行起来,如下图所示:

viewimpl only layout

6.4 Business Processes

代码包路径:fayelab.tdd.stringtransformer.dummy.original

6.4.1 Init

初始化完成后:

  • 在可用转换器列表中依次呈现Upper、Lower和TrimPrefixSpaces三个条目,并选中第一个转换器。
  • 转换器链列表、源字符串文本框、结果字符串文本框均为空。

6.4.1.1 Presenter

由BusinessLogic提供的所有转换器经由Presenter推送给View显示,可用转换器列表选中索引为0的转换器。

6.4.1.1.1 Add a Test

PresenterTest.java

import junit.framework.TestCase;

import static java.util.Arrays.asList;

public class PresenterTest extends TestCase
{
    public void test_init()
    {
        ViewStub viewStub = new ViewStub();
        BusinessLogicStub businessLogicStub = new BusinessLogicStub();
        Presenter presenter = new Presenter(viewStub, businessLogicStub);

        presenter.init();

        assertEquals(asList("Upper", "Lower", "TrimPrefixSpaces"), viewStub.getAvailTranses());
        assertEquals(0, viewStub.getAvailSelectedIndex());
    }
}

在View这一层验证View拿到的可用转换器列表和可用转换器列表选中转换器的索引。强调一下,必须在View这一层验证。如果只在Presenter这一层验证,无法验证Presenter和View是否正确地进行了交互。

6.4.1.1.2 Write the Code

1 新增ViewStub类。

ViewStub.java

import java.util.List;

public class ViewStub implements View
{
    public List<String> getAvailTranses()
    {
        return null;
    }

    public int getAvailSelectedIndex()
    {
        return -1;
    }
}

2 ViewStub实现View接口,新增View接口类。

View.java

public interface View
{
}

Q:ViewStub实现View,那么viewStub.getAvailTranses()是View的方法,还是ViewStub的方法?
viewStub.getAvailTranses()是ViewStub的测试辅助方法,不是View的方法。假设viewStub.getAvailTranses()是View的方法,View的方法都是给Presenter使用的,那么站在Presenter的角度是否需要使用View的这个方法?换个问法,Presenter需要从View获取可用转换器列表吗?不需要。同理,viewStub.getAvailSelectedIndex()也是ViewStub的测试辅助方法。

3 新增BusinessLogicStub类。

BusinessLogicStub.java

public class BusinessLogicStub implements BusinessLogic
{
}

4 BusinessLogicStub实现BusinessLogic接口,新增BusinessLogic接口类。

BusinessLogic.java

public interface BusinessLogic
{
}

5 新增Presenter类。

Presenter.java

public class Presenter
{
    private View view;
    private BusinessLogic businessLogic;

    private int availSelectedIndex = 0;

    public Presenter(View view, BusinessLogic businessLogic)
    {
        this.view = view;
        this.businessLogic = businessLogic;
    }

    public void init()
    {
        view.presentAvailTranses(businessLogic.getAllTranses());
        view.setAvailSelectedIndex(availSelectedIndex);
    }
}
  • Presenter类的构造参数应该是“View view”和“BusinessLogic businessLogic”,而不是“ViewStub viewStub”和“BusinessLogicStub businessLogicStub”。Presenter是产品代码,不能引用测试代码,它只会看到View和BusinessLogic,看不到ViewStub和BusinessLogicStub。
  • 根据之前的业务流程分析可知,在某些流程中,新选中的转换器的索引依赖于之前选中的转换器的索引,因此Presenter中需要维护选中转换器的索引。这里先新增availSelectedIndex成员变量,用于保存可用转换器列表选中的转换器的索引,并初始化为0。
  • 在Presenter.init()方法中,从businesslogic获得所有转换器,推送给View显示为可用转换器列表,并告诉View可用转换器列表选中转换器的索引。

6 View接口新增presentAvailTranses()和setAvailSelectedIndex()方法。

View.java

import java.util.List;

    void presentAvailTranses(List<String> transes);

    void setAvailSelectedIndex(int index);

7 ViewStub类实现View新增方法。

ViewStub.java

    private List<String> availTranses;
    private int availSelectedIndex;

    @Override
    public void presentAvailTranses(List<String> transes)
    {
        availTranses = transes;
    }

    public List<String> getAvailTranses()
    {
        return availTranses;
    }

    @Override
    public void setAvailSelectedIndex(int index)
    {
        availSelectedIndex = index;
    }

    public int getAvailSelectedIndex()
    {
        return availSelectedIndex;
    }

在ViewStub.presentAvailTranses()方法中,把传入的transes保存到availTranses,并在getAvailTranses()方法中返回availTranses。这是一种打桩的方法,证明View.presentAvailTranses()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。

同理,在ViewStub.setAvailSelectedIndex()方法中,把传入的index保存到availSelectedIndex,并在getAvailSelectedIndex()方法中返回availSelectedIndex,证明View.setAvailSelectedIndex()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。

8 BusinessLogic接口新增getAllTranses()方法。

BusinessLogic.java

import java.util.List;

    List<String> getAllTranses();

9 BusinessLogicStub类实现BusinessLogic新增方法。

BusinessLogicStub.java

import java.util.List;

import static java.util.Arrays.asList;

    @Override
    public List<String> getAllTranses()
    {
        return asList("Upper", "Lower", "TrimPrefixSpaces");
    }

看上去BusinessLogicStub.get_all_transes()似乎做了产品代码要做的事情,但这里其实还是打桩。因为在产品场景中,all_transes有可能来自于分布式的服务端、数据库或者文件。在TDD开发时,这些耦合都要解除,否则无法测试。

6.4.1.1.3 Refactor the Code

将"Upper"、"Lower"和"TrimPrefixSpaces"字符串单独定义到一个常量文件中。

Trans.java

public class Trans
{
    static final String UPPER_TRANS = "Upper";
    static final String LOWER_TRANS = "Lower";
    static final String TRIM_PREFIX_SPACES_TRANS = "TrimPrefixSpaces";
}

PresenterTest.java

import static fayelab.tdd.stringtransformer.dummy.original.Trans.*;

    public void test_init()
    {
        ViewStub viewStub = new ViewStub();
        BusinessLogicStub businessLogicStub = new BusinessLogicStub();
        Presenter presenter = new Presenter(viewStub, businessLogicStub);

        presenter.init();

        assertEquals(asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS), viewStub.getAvailTranses());
        assertEquals(0, viewStub.getAvailSelectedIndex());
    }

BusinessLogicStub.java

    @Override
    public List<String> getAllTranses()
    {
        return asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS);
    }

6.4.1.2 BusinessLogicImpl

BusinessLogicImpl提供所有的转换器。

6.4.1.2.1 Add a Test

BusinessLogicImplTest.java

import junit.framework.TestCase;

import static java.util.Arrays.asList;
import static fayelab.tdd.stringtransformer.dummy.original.Trans.*;

public class BusinessLogicImplTest extends TestCase
{
    public void test_get_all_transes()
    {
        BusinessLogicImpl impl = new BusinessLogicImpl();
        assertEquals(asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS), impl.getAllTranses());
    }
}
6.4.1.2.2 Write the Code

新增BusinessLogicImpl类,实现BusinessLogic接口,实现BusinessLogic新增方法。

BusinessLogicImpl.java

import java.util.List;

import static java.util.Arrays.asList;
import static fayelab.tdd.stringtransformer.dummy.original.Trans.*;

public class BusinessLogicImpl implements BusinessLogic
{
    @Override
    public List<String> getAllTranses()
    {
        return asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS);
    }
}
6.4.1.2.3 Refactor the Code

无。

6.4.1.3 Test Suite

import junit.framework.Test;
import junit.framework.TestSuite;

public class AllTests
{
    public static Test suite()
    {
        TestSuite suite = new TestSuite(AllTests.class.getName());
        //$JUnit-BEGIN$
        suite.addTestSuite(BusinessLogicImplTest.class);
        suite.addTestSuite(PresenterTest.class);
        //$JUnit-END$
        return suite;
    }
}

6.4.1.4 ViewImpl

ViewImpl实现View接口,实现View新增方法。

ViewImpl.java

import java.util.List;

public class ViewImpl extends JFrame implements View
{
    ...

    @Override
    public void presentAvailTranses(List<String> transes)
    {
        lstAvail.setListData(transes.toArray(new String[] {}));
    }

    @Override
    public void setAvailSelectedIndex(int index)
    {
        lstAvail.setSelectedIndex(index);
    }

    ...
}

6.4.1.5 main

在PresenterTest的test_init()方法中,viewStub、businessLogicStub作为构造参数构造出测试环境中的presenter,并调用presenter.init()方法完成初始化。那么,如何构造产品环境中的presenter,并调用presenter.init()方法完成初始化呢?

ViewImpl.main()是整个程序的运行总入口,在这里完成上述过程是合适的。

ViewImpl.java

    public static void main(String[] args)
    {
        ViewImpl viewImpl = new ViewImpl();
        BusinessLogic businessLogicImpl = new BusinessLogicImpl();
        Presenter presenter = new Presenter(viewImpl, businessLogicImpl);
        presenter.init();

        centerShow(viewImpl);
    }

6.4.1.6 Running Result

运行起来,如下图所示:

viewimpl init

6.4.2 Build Transformer Chain

构建转换器链包括:添加转换器、移除转换器和移除所有转换器。

构建转换器链的代码主要集中在Presenter和View。

6.4.2.1 Add a Transformer

6.4.2.1.1 Normal Business Process 1: Add the transformer which is not the last

在可用转换器列表中选中不是最后的一个转换器,点击Add按钮,选中的转换器被添加到转换器链列表末尾,转换器链列表选中新添加的转换器。可用转换器列表中的转换器条目不变,选中之前选中的转换器的下一个转换器。

6.4.2.1.1.1 Presenter

####### 6.4.2.1.1.1.1 Add a Test Presenter.java

    public void test_add_not_the_last_trans()
    {
        ViewStub viewStub = new ViewStub();
        BusinessLogicStub businessLogicStub = new BusinessLogicStub();
        Presenter presenter = new Presenter(viewStub, businessLogicStub);
        presenter.init();
        viewStub.setAvailSelectedTrans(UPPER_TRANS);

        presenter.addTrans();

        assertEquals(asList(UPPER_TRANS), viewStub.getChainTranses());
        assertEquals(0, viewStub.getChainSelectedIndex());
        assertEquals(1, viewStub.getAvailSelectedIndex());
    }

在View这一层验证View拿到的转换器链列表、转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引。

####### 6.4.2.1.1.1.2 Write the Code 1 ViewStub

ViewStub.java

    public void setAvailSelectedTrans(String trans)
    {
    }

    public List<String> getChainTranses()
    {
        return null;
    }

    public int getChainSelectedIndex()
    {
        return -1;
    }

setAvailSelectedTrans()、getChainTranses()和getChainSelectedIndex()都是ViewStub的测试辅助方法,不是View的方法。

2 Presenter

Presenter.java

import java.util.ArrayList;
import java.util.List;

import static fayelab.tdd.stringtransformer.dummy.original.Trans.NONE_SELECTED_INDEX;

    private int chainSelectedIndex = NONE_SELECTED_INDEX;
    private List<String> availTranses;
    private List<String> chainTranses = new ArrayList<>();

    public void init()
    {
        availTranses = businessLogic.getAllTranses();
        view.presentAvailTranses(availTranses);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

    public void addTrans()
    {
        String availSelectedTrans = view.getAvailSelectedTrans();

        chainTranses.add(availSelectedTrans);
        chainSelectedIndex = chainTranses.indexOf(availSelectedTrans);
        availSelectedIndex = availTranses.indexOf(availSelectedTrans) + 1;

        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

其中,NONE_SELECTED_INDEX定义在Trans类中,值为-1,表示无选中索引。

Trans.java

    static final int NONE_SELECTED_INDEX = -1;
  • 除了已有的availSelectedIndex,Presenter类新增chainSelectedIndex、availTranses和chainTranses成员变量,它们全部都是Presenter需要维护的状态,含义如下:
    • availSelectedIndex: 可用转换器列表选中的转换器的索引,初始化为0。
    • chainSelectedIndex: 转换器链列表选中的转换器的索引,初始化为NONE_SELECTED_INDEX (-1)。
    • availTranses: 可用转换器列表,初始化为None。
    • chainTranses: 转换器链列表,初始化为[]。
  • 在init()方法中,从businessLogic获得的所有转换器保存到availTranses中,并将availTranses推送给View显示为可用转换器列表,并告诉View可用转换器列表选中转换器的索引。
  • 在addTrans()方法中,从View获取选中的可用转换器,添加到chainTranses末尾。计算转换器链列表选中转换器的索引并更新chainSelectedIndex。计算可用转换器列表选中转换器的索引并更新availSelectedIndex。将chainTranses推送给View显示为转换器链列表,并告诉View转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引。

3 View

View.java

    String getAvailSelectedTrans();

    void presentChainTranses(List<String> transes);

    void setChainSelectedIndex(int index);

4 ViewStub

ViewStub.java

    private String availSelectedTrans;
    private List<String> chainTranses;
    private int chainSelectedIndex;

    @Override
    public String getAvailSelectedTrans()
    {
        return availSelectedTrans;
    }

    public void setAvailSelectedTrans(String trans)
    {
        availSelectedTrans = trans;
    }

    @Override
    public void presentChainTranses(List<String> transes)
    {
        chainTranses = transes;
    }

    public List<String> getChainTranses()
    {
        return chainTranses;
    }

    @Override
    public void setChainSelectedIndex(int index)
    {
        chainSelectedIndex = index;
    }

    public int getChainSelectedIndex()
    {
        return chainSelectedIndex;
    }
  • 在getAvailSelectedTrans()方法中返回在setAvailSelectedTrans()方法中保存下来的availSelectedTrans。
  • 在getChainTranses()方法中返回在presentChainTranses()方法中保存下来的chainTranses。
  • 在getChainSelectedIndex()方法中返回在setChainSelectedIndex()方法中保存下来的chainSelectedIndex。

这些都是打桩的方法,证明View.getAvailSelectedTrans()、View.presentChainTranses()、View.setChainSelectedIndex()以及View.setAvailSelectedIndex()这些方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。

####### 6.4.2.1.1.1.3 Refactor the Code 1 重构测试代码。PresenterTest类抽取setUp()方法。

PresenterTest.java

public class PresenterTest extends TestCase
{
    private ViewStub viewStub;
    private BusinessLogicStub businessLogicStub;
    private Presenter presenter;

    @Override
    protected void setUp()
    {
        viewStub = new ViewStub();
        businessLogicStub = new BusinessLogicStub();
        presenter = new Presenter(viewStub, businessLogicStub);

        presenter.init();
    }

    public void test_init()
    {
        assertEquals(asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS), viewStub.getAvailTranses());
        assertEquals(0, viewStub.getAvailSelectedIndex());
    }

    public void test_add_not_the_last_trans()
    {
        viewStub.setAvailSelectedTrans(UPPER_TRANS);

        presenter.addTrans();

        assertEquals(asList(UPPER_TRANS), viewStub.getChainTranses());
        assertEquals(0, viewStub.getChainSelectedIndex());
        assertEquals(1, viewStub.getAvailSelectedIndex());
    }
}

2 重构产品代码。Presenter类抽取updateChainSelectedIndex()和updateAvailSelectedIndex()方法。

Presenter.java

    public void addTrans()
    {
        String availSelectedTrans = view.getAvailSelectedTrans();

        chainTranses.add(availSelectedTrans);
        updateChainSelectedIndex(availSelectedTrans);
        updateAvailSelectedIndex(availSelectedTrans);

        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

    private void updateChainSelectedIndex(String availSelectedTrans)
    {
        chainSelectedIndex = chainTranses.indexOf(availSelectedTrans);
    }

    private void updateAvailSelectedIndex(String availSelectedTrans)
    {
        availSelectedIndex = availTranses.indexOf(availSelectedTrans) + 1;
    }
6.4.2.1.1.2 ViewImpl

ViewImpl.java

    @Override
    public String getAvailSelectedTrans()
    {
        return lstAvail.getSelectedValue();
    }

    @Override
    public void presentChainTranses(List<String> transes)
    {
        lstChain.setListData(transes.toArray(new String[] {}));
    }

    @Override
    public void setChainSelectedIndex(int index)
    {
        lstChain.setSelectedIndex(index);
    }
6.4.2.1.1.3 Assembly

ViewImpl的各个事件响应方法的实现都是简单委托Presenter来做,不包含任何其他逻辑。在Add按钮的事件响应方法中调用Presenter.addTrans()方法。但是,ViewImpl中还没有Presenter的对象实例。

可以通过set方式将Presenter对象实例注入到ViewImpl中。之所以不直接在ViewImpl的构造方法中创建Presenter对象实例,是因为创建Presenter对象实例需要有View和BusinessLogic接口类对象的实例。如果直接在ViewImpl类的构造方法中创建Presenter对象实例,View就要知道BusinessLogic,而View本来是不需要知道BusinessLogic的。

在Presenter类的构造方法中调用View.setPresenter()方法,真正建立Presenter和View之间的双向联系。而Presenter只知道View接口类,因此setPresenter()是View接口类的方法。ViewImpl实现View.setPresenter()方法,将注入的Presenter对象实例保存成ViewImpl的成员变量。ViewStub也需要实现View.setPresenter()方法,只不过是空实现而已。

View.java

    void setPresenter(Presenter presenter);

ViewImpl.java

    private Presenter presenter;

    @Override
    public void setPresenter(Presenter presenter)
    {
        this.presenter = presenter;
    }

    private void btnAdd_actionPerformed(ActionEvent e)
    {
        presenter.addTrans();
    }

ViewStub.java

    @Override
    public void setPresenter(Presenter presenter)
    {
    }

Presenter.java

    public Presenter(View view, BusinessLogic businessLogic)
    {
        this.view = view;
        this.businessLogic = businessLogic;
        this.view.setPresenter(this);
    }
6.4.2.1.1.4 Running Result

运行起来,如下图所示:

viewimpl add transformer

6.4.2.1.2 Normal Business Process 2: Add the transformer which is the last

在可用转换器列表中选中最后一个转换器,点击Add按钮,选中的转换器被添加到转换器链列表末尾,转换器链列表选中新添加的转换器。可用转换器列表中的转换器条目不变,选中第一个转换器。

6.4.2.1.2.1 Presenter

####### 6.4.2.1.2.1.1 Add a Test PresenterTest.java

    public void test_add_the_last_trans()
    {
        viewStub.setAvailSelectedTrans(TRIM_PREFIX_SPACES_TRANS);

        presenter.addTrans();

        assertEquals(asList(TRIM_PREFIX_SPACES_TRANS), viewStub.getChainTranses());
        assertEquals(0, viewStub.getChainSelectedIndex());
        assertEquals(0, viewStub.getAvailSelectedIndex());
    }

在View这一层验证View拿到的转换器链列表、转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引。

####### 6.4.2.1.2.1.2 Write the Code Presenter.java

    private void updateAvailSelectedIndex(String availSelectedTrans)
    {
        int selectedIndex = availTranses.indexOf(availSelectedTrans);
        availSelectedIndex = selectedIndex == availTranses.size() - 1 ? 0 : selectedIndex + 1;
    }

####### 6.4.2.1.2.1.3 Refactor the Code Presenter类抽取isLastIndex()静态方法。

Presenter.java

    private void updateAvailSelectedIndex(String availSelectedTrans)
    {
        int selectedIndex = availTranses.indexOf(availSelectedTrans);
        availSelectedIndex = isLastIndex(selectedIndex, availTranses) ? 0 : selectedIndex + 1;
    }

    private static boolean isLastIndex(int index, List<?> list)
    {
        return index == list.size() - 1;
    }
6.4.2.1.3 Abnormal Business Process 1: Add a transformer which has been already existed in the chain

在可用转换器列表中选中一个在转换器链列表中已存在的转换器,点击Add按钮,提示“待添加的转换器在转换器链中已存在”。转换器链列表中的转换器条目不变,选中已存在的转换器。可用转换器列表中的转换器条目不变,选中之前选中的转换器。

6.4.2.1.3.1 Presenter

####### 6.4.2.1.3.1.1 Add a Test PresenterTest.java

    public void test_add_already_existed_in_chain_trans()
    {
        viewStub.setAvailSelectedTrans(UPPER_TRANS);
        presenter.addTrans();
        viewStub.setAvailSelectedTrans(LOWER_TRANS);
        presenter.addTrans();
        viewStub.setAvailSelectedTrans(LOWER_TRANS);

        presenter.addTrans();

        assertTrue(viewStub.isAddAlreadyExistedInChainTransNotified());
        assertEquals(asList(UPPER_TRANS, LOWER_TRANS), viewStub.getChainTranses());
        assertEquals(1, viewStub.getChainSelectedIndex());
        assertEquals(1, viewStub.getAvailSelectedIndex());
    }

在验证View拿到的转换器链列表、转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引之前,先验证View是否收到addAlreadyExistedInChainTrans的通知。

####### 6.4.2.1.3.1.2 Write the Code 1 ViewStub

ViewStub.java

    public boolean isAddAlreadyExistedInChainTransNotified()
    {
        return false;
    }

isAddAlreadyExistedInChainTransNotified()是ViewStub的测试辅助方法,不是View的方法。

2 Presenter

Presenter.java

    public void addTrans()
    {
        String availSelectedTrans = view.getAvailSelectedTrans();

        boolean isAddAlreadyExistedInChainTrans = false;
        if(chainTranses.contains(availSelectedTrans))
        {
            isAddAlreadyExistedInChainTrans = true;
            view.notifyAddAlreadyExistedInChainTrans();
        }
        else
        {
            chainTranses.add(availSelectedTrans);
        }
        updateChainSelectedIndex(availSelectedTrans);
        updateAvailSelectedIndex(availSelectedTrans, isAddAlreadyExistedInChainTrans);

        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

    private void updateAvailSelectedIndex(String availSelectedTrans, boolean isAddAlreadyExistedInChainTrans)
    {
        int selectedIndex = availTranses.indexOf(availSelectedTrans);
        if(isAddAlreadyExistedInChainTrans)
        {
            availSelectedIndex = selectedIndex;
        }
        else
        {
            availSelectedIndex = isLastIndex(selectedIndex, availTranses) ? 0 : selectedIndex + 1;
        }
    }

3 View

View.java

    void notifyAddAlreadyExistedInChainTrans();

4 ViewStub

ViewStub.java

    private boolean addAlreadyExistedInChainTransNotified;

    @Override
    public void notifyAddAlreadyExistedInChainTrans()
    {
        addAlreadyExistedInChainTransNotified = true;
    }

    public boolean isAddAlreadyExistedInChainTransNotified()
    {
        return addAlreadyExistedInChainTransNotified;
    }

在isAddAlreadyExistedInChainTransNotified()方法中返回在notifyAddAlreadyExistedInChainTrans()方法中保存下来的addAlreadyExistedInChainTransNotified。

这还是打桩的方法,证明View.notifyAddAlreadyExistedInChainTrans()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。

####### 6.4.2.1.3.1.3 Refactor the Code Presenter类抽取alreadyExistedInChain()方法。

Presenter.java

    public void addTrans()
    {
        String availSelectedTrans = view.getAvailSelectedTrans();

        boolean isAddAlreadyExistedInChainTrans = false;
        if(alreadyExistedInChain(availSelectedTrans))
        {
            isAddAlreadyExistedInChainTrans = true;
            view.notifyAddAlreadyExistedInChainTrans();
        }
        else
        {
            chainTranses.add(availSelectedTrans);
        }
        updateChainSelectedIndex(availSelectedTrans);
        updateAvailSelectedIndex(availSelectedTrans, isAddAlreadyExistedInChainTrans);

        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

    private boolean alreadyExistedInChain(String trans)
    {
        return chainTranses.contains(trans);
    }
6.4.2.1.3.2 ViewImpl

ViewImpl.java

import javax.swing.JOptionPane;

    @Override
    public void notifyAddAlreadyExistedInChainTrans()
    {
        JOptionPane.showMessageDialog(this, "The transformer to be added has been already existed in the chain.");
    }
6.4.2.1.3.3 Running Result

运行起来,如下图所示:

viewimpl add already existed in chain transformer

6.4.2.1.4 Abnormal Business Process 2: Add a transformer but none of the available transformers is specified

在可用转换器列表中未选中任何转换器,点击Add按钮,提示“请在可用转换器中指定一个转换器”。转换器链列表中的转换器条目不变,选中之前已经选中的转换器。可用转换器列表中的转换器条目不变,选中第一个转换器。

6.4.2.1.4.1 Presenter

####### 6.4.2.1.4.1.1 Add a Test PresenterTest.java

    public void test_add_trans_but_avail_trans_not_specified()
    {
        viewStub.setAvailSelectedTrans(UPPER_TRANS);
        presenter.addTrans();
        viewStub.setAvailSelectedTrans(LOWER_TRANS);
        presenter.addTrans();
        viewStub.setAvailSelectedTrans(NONE_SELECTED_TRANS);

        presenter.addTrans();

        assertTrue(viewStub.isAvailTransNotSpecifiedNotified());
        assertEquals(asList(UPPER_TRANS, LOWER_TRANS), viewStub.getChainTranses());
        assertEquals(1, viewStub.getChainSelectedIndex());
        assertEquals(0, viewStub.getAvailSelectedIndex());
    }

在验证View拿到的转换器链列表、转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引之前,先验证View是否收到availTransNotSpecified的通知。

####### 6.4.2.1.4.1.2 Write the Code 1 Trans

Trans.java

    static final String NONE_SELECTED_TRANS = null;

NONE_SELECTED_TRANS的值为null,表示无选中转换器。

2 ViewStub

ViewStub.java

    public boolean isAvailTransNotSpecifiedNotified()
    {
        return false;
    }

isAvailTransNotSpecifiedNotified()是ViewStub的测试辅助方法,不是View的方法。

3 Presenter

Presenter.java

import static fayelab.tdd.stringtransformer.dummy.original.Trans.NONE_SELECTED_TRANS;

    public void addTrans()
    {
        String availSelectedTrans = view.getAvailSelectedTrans();

        boolean isAvailTransNotSpecified = false;
        boolean isAddAlreadyExistedInChainTrans = false;
        if(availSelectedTrans == NONE_SELECTED_TRANS)
        {
            isAvailTransNotSpecified = true;
            view.notifyAvailTransNotSpecified();
        }
        else if(alreadyExistedInChain(availSelectedTrans))
        {
            isAddAlreadyExistedInChainTrans = true;
            view.notifyAddAlreadyExistedInChainTrans();
        }
        else
        {
            chainTranses.add(availSelectedTrans);
        }
        updateChainSelectedIndex(availSelectedTrans, isAvailTransNotSpecified);
        updateAvailSelectedIndex(availSelectedTrans, isAvailTransNotSpecified, isAddAlreadyExistedInChainTrans);

        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

    private void updateChainSelectedIndex(String availSelectedTrans, boolean isAvailTransNotSpecified)
    {
        if(isAvailTransNotSpecified)
        {
            return;
        }

        chainSelectedIndex = chainTranses.indexOf(availSelectedTrans);
    }

    private void updateAvailSelectedIndex(String availSelectedTrans, 
            boolean isAvailTransNotSpecified, boolean isAddAlreadyExistedInChainTrans)
    {
        if(isAvailTransNotSpecified)
        {
            availSelectedIndex = 0;
            return;
        }

        int selectedIndex = availTranses.indexOf(availSelectedTrans);
        if(isAddAlreadyExistedInChainTrans)
        {
            availSelectedIndex = selectedIndex;
        }
        else
        {
            availSelectedIndex = isLastIndex(selectedIndex, availTranses) ? 0 : selectedIndex + 1;
        }
    }

4 View

View.java

    void notifyAvailTransNotSpecified();

5 ViewStub

ViewStub.java

    private boolean availTransNotSpecifiedNotified;

    @Override
    public void notifyAvailTransNotSpecified()
    {
        availTransNotSpecifiedNotified = true;
    }

    public boolean isAvailTransNotSpecifiedNotified()
    {
        return availTransNotSpecifiedNotified;
    }

在isAvailTransNotSpecifiedNotified()方法中返回在notifyAvailTransNotSpecified()方法中保存下来的availTransNotSpecifiedNotified。

这还是打桩的方法,证明View.notifyAvailTransNotSpecified()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。

####### 6.4.2.1.4.1.3 Refactor the Code Presenter类抽取transNotSpecified()静态方法。

Presenter.java

    public void addTrans()
    {
        String availSelectedTrans = view.getAvailSelectedTrans();

        boolean isAvailTransNotSpecified = false;
        boolean isAddAlreadyExistedInChainTrans = false;
        if(transNotSpecified(availSelectedTrans))
        {
            isAvailTransNotSpecified = true;
            view.notifyAvailTransNotSpecified();
        }
        else if(alreadyExistedInChain(availSelectedTrans))
        {
            isAddAlreadyExistedInChainTrans = true;
            view.notifyAddAlreadyExistedInChainTrans();
        }
        else
        {
            chainTranses.add(availSelectedTrans);
        }
        updateChainSelectedIndex(availSelectedTrans, isAvailTransNotSpecified);
        updateAvailSelectedIndex(availSelectedTrans, isAvailTransNotSpecified, isAddAlreadyExistedInChainTrans);

        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

    private static boolean transNotSpecified(String trans)
    {
        return trans == NONE_SELECTED_TRANS;
    }

仔细观察addTrans()方法的代码,中间的一大段代码其实是在进行参数校验。围绕availSelectedTrans校验是否指定了可用转换器以及选中的可用转换器是否已存在于转换器链列表。

Wishful Thinking,希望这段代码长成下面的样子:

    validatingResult = validateAddTrans(availSelectedTrans);

其中,validatingResult包括校验是否成功以及校验失败原因这两部分内容。校验失败原因需要传递给updateChainSelectedIndex()和updateAvailSelectedIndex()这两个更新索引的方法。

在Validator.java中定义ValidatingResult类。

Validator.java ValidatingResult Class

public class Validator
{
}

class ValidatingResult
{
    enum FailedReason
    {
        NONE,
        AVAIL_TRANS_NOT_SPECIFIED,
        ADD_ALREADY_EXISTED_IN_CHAIN_TRANS
    }

    private boolean succeeded;
    private FailedReason failedReason;

    private ValidatingResult(boolean succeeded, FailedReason failedReason)
    {
        this.succeeded = succeeded;
        this.failedReason = failedReason;
    }

    public boolean isSucceeded()
    {
        return succeeded;
    }

    public FailedReason getFailedReason()
    {
        return failedReason;
    }

    public static ValidatingResult succeededResult()
    {
        return new ValidatingResult(true, FailedReason.NONE);
    }

    public static ValidatingResult failedResult(FailedReason failedReason)
    {
        return new ValidatingResult(false, failedReason);
    }
}

Presenter.java

import fayelab.tdd.stringtransformer.dummy.original.ValidatingResult.FailedReason;

    public void addTrans()
    {
        String availSelectedTrans = view.getAvailSelectedTrans();

        ValidatingResult validatingResult = validateAddTrans(availSelectedTrans);
        if(validatingResult.isSucceeded())
        {
            chainTranses.add(availSelectedTrans);
        }

        updateChainSelectedIndex(availSelectedTrans, validatingResult.getFailedReason());
        updateAvailSelectedIndex(availSelectedTrans, validatingResult.getFailedReason());

        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

    private ValidatingResult validateAddTrans(String availSelectedTrans)
    {
        if(transNotSpecified(availSelectedTrans))
        {
            view.notifyAvailTransNotSpecified();
            return ValidatingResult.failedResult(FailedReason.AVAIL_TRANS_NOT_SPECIFIED);
        }

        if(alreadyExistedInChain(availSelectedTrans))
        {
            view.notifyAddAlreadyExistedInChainTrans();
            return ValidatingResult.failedResult(FailedReason.ADD_ALREADY_EXISTED_IN_CHAIN_TRANS);
        }

        return ValidatingResult.succeededResult();
    }

    private void updateChainSelectedIndex(String availSelectedTrans, FailedReason failedReason)
    {
        if(failedReason == FailedReason.AVAIL_TRANS_NOT_SPECIFIED)
        {
            return;
        }

        chainSelectedIndex = chainTranses.indexOf(availSelectedTrans);
    }

    private void updateAvailSelectedIndex(String availSelectedTrans, FailedReason failedReason)
    {
        if(failedReason == FailedReason.AVAIL_TRANS_NOT_SPECIFIED)
        {
            availSelectedIndex = 0;
            return;
        }

        int selectedIndex = availTranses.indexOf(availSelectedTrans);
        if(failedReason == FailedReason.ADD_ALREADY_EXISTED_IN_CHAIN_TRANS)
        {
            availSelectedIndex = selectedIndex;
        }
        else
        {
            availSelectedIndex = isLastIndex(selectedIndex, availTranses) ? 0 : selectedIndex + 1;
        }
    }

继续观察validateAddTrans()方法,围绕availSelectedTrans,校验是否指定了可用转换器跟校验选中的可用转换器是否已存在于转换器链列表的代码存在重复。这两段代码都遵循同一种模式,模式用伪码描述如下:

    if(failedPredication(param))
    {
        failedAction();
        return ValidatingResult.failedResult(failedReason);
    }

    return ValidatingResult.succeededResult();

用函数式编程重构这部分代码。在Validator.java的Validator类中定义validateParam()静态方法。

Validator.java Validator Class

import java.util.function.Predicate;

import fayelab.tdd.stringtransformer.dummy.original.ValidatingResult.FailedReason;

public class Validator
{
    public static <T> ValidatingResult validateParam(T param, 
            Predicate<T> paramFailedPred, Runnable paramFailedAction, FailedReason paramFailedReason)
    {
        if(paramFailedPred.test(param))
        {
            paramFailedAction.run();
            return ValidatingResult.failedResult(paramFailedReason);
        }

        return ValidatingResult.succeededResult();
    }
}

Presenter.java

    private ValidatingResult validateAddTrans(String availSelectedTrans)
    {
        ValidatingResult validatingResult = Validator.validateParam(availSelectedTrans, Presenter::transNotSpecified, 
                view::notifyAvailTransNotSpecified, FailedReason.AVAIL_TRANS_NOT_SPECIFIED);
        if(!validatingResult.isSucceeded())
        {
            return validatingResult;
        }

        validatingResult = Validator.validateParam(availSelectedTrans, this::alreadyExistedInChain, 
                view::notifyAddAlreadyExistedInChainTrans, FailedReason.ADD_ALREADY_EXISTED_IN_CHAIN_TRANS);
        if(!validatingResult.isSucceeded())
        {
            return validatingResult;
        }

        return ValidatingResult.succeededResult();
    }

看上去代码量似乎并没有减少多少,但层次已经提升了。但两段validateParam代码依然存在重复,继续重构,让代码更简单。

  • 将param、paramFailedPred、paramFailedAction和paramFailedReason封装为参数校验规则(ParamValidatingRule)。
  • 多个参数的校验规则可以定义为一个列表,列表的每个元素都是一个ParamValidatingRule。
  • 为了不影响已有代码,先将已有的Validator.validateParam()静态方法重命名为validateParam2。新增validateParam()静态方法,输入参数类型为ParamValidatingRule。
  • 在Validator类中再提供一个validate(List rules)方法,顺序按照列表中的每一条规则校验参数。在这个过程中,有校验失败的就返回这个校验失败结果,整个过程都没有校验失败的就返回校验成功的结果。
  • 在Presenter.validateAddTrans()方法中,只需要定义规则列表,直接返回Validator.validate(List rules)方法的执行结果即可。
  • 删除不再有引用的Validator.validateParam2()方法。

Validator.java ParamValidatingRule Class

class ParamValidatingRule<T>
{
    private T param;
    private Predicate<T> failedPred;
    private Runnable failedAction;
    private FailedReason failedReason;

    static <T> ParamValidatingRule<T> paramValidatingRule(T param, Predicate<T> failedPred, 
            Runnable failedAction, FailedReason failedReason)
    {
        return new ParamValidatingRule<>(param, failedPred, failedAction, failedReason);
    }

    private ParamValidatingRule(T param, Predicate<T> failedPred, Runnable failedAction, FailedReason failedReason)
    {
        this.param = param;
        this.failedPred = failedPred;
        this.failedAction = failedAction;
        this.failedReason = failedReason;
    }

    public T getParam()
    {
        return param;
    }

    public Predicate<T> getFailedPred()
    {
        return failedPred;
    }

    public Runnable getFailedAction()
    {
        return failedAction;
    }

    public FailedReason getFailedReason()
    {
        return failedReason;
    }
}

Validator.java Validator Class

import java.util.List;

public class Validator
{
    public static ValidatingResult validate(List<ParamValidatingRule<?>> rules)
    {
        for(ParamValidatingRule<?> rule : rules)
        {
            ValidatingResult validatingResult = validateParam(rule);
            if(!validatingResult.isSucceeded())
            {
                return validatingResult;
            }
        }

        return ValidatingResult.succeededResult();
    }

    private static <T> ValidatingResult validateParam(ParamValidatingRule<T> rule)
    {
        if(rule.getFailedPred().test(rule.getParam()))
        {
            rule.getFailedAction().run();
            return ValidatingResult.failedResult(rule.getFailedReason());
        }

        return ValidatingResult.succeededResult();
    }
}

Presenter.java

import static java.util.Arrays.asList;
import static fayelab.tdd.stringtransformer.dummy.original.ParamValidatingRule.paramValidatingRule;

    private ValidatingResult validateAddTrans(String availSelectedTrans)
    {
        List<ParamValidatingRule<?>> rules = asList(
                paramValidatingRule(availSelectedTrans, Presenter::transNotSpecified, 
                        view::notifyAvailTransNotSpecified, FailedReason.AVAIL_TRANS_NOT_SPECIFIED),
                paramValidatingRule(availSelectedTrans, this::alreadyExistedInChain, 
                        view::notifyAddAlreadyExistedInChainTrans, FailedReason.ADD_ALREADY_EXISTED_IN_CHAIN_TRANS));

        return Validator.validate(rules);
    }

现在Presenter.validateAddTrans()方法中的代码真的很简单了。

这里运用了“计算描述与执行分离”的设计思想。定义“List rules”就是在描述计算,而“Validator.validate(rules)”就是在执行计算,是一个解释器。这种设计思想非常强大。它带来的好处是:一方面在描述层面可以很自由,可以任意组装,不受限于执行层面;另一方面在执行层面可以做很多优化,比如并发,或者解释成其他的结果,比如把“List rules”中的所有参数校验规则打印出来,只要再写一个解释器即可。需求变化的复杂性和代码修改的复杂性是线性关系,重要的是还不影响描述层面。

定义“List rules”的过程就是在用自定义的领域特定语言(DSL: Domain-Specific Language)定义一份规格说明(Specification)。这份规格说明是一个纯数据,是声明性的、抽象的。只要把这份规格说明传给解释器Validator解释执行,结果就出来了。这个领域特定语言最主要的语素是ParamValidatingRule(param, failedPred, failedAction, failedReason),当然还有List。它们是针对“字符串转换器的参数校验”这个特定问题领域抽象出来的计算模型。这是最本质的抽象,跟OO或函数式编程没有关系,因为它们都是实现层面的,不是设计层面的。事实上,我们现在使用函数式编程实现的,也可以改为用OO实现,但“List rules”描述不变。

我们可以使用实现语言(Python/Java/C等)内置的语言特性实现DSL,例如这里的ParamValidatingRule类和List,也可以自定义一套跟实现语言无关的小语言实现。

用DSL编写规格说明(Specification)也是在“编程”,只不过是在我们自己抽象出的针对“字符串转换器的参数校验”这个特定问题领域的计算模型上用DSL表达计算,即“编程”,跟我们用Java编程并没有本质区别。用Java编程不过是在Java语言提供的计算模型上用Java语言提供的DSL表达计算,即“编程”。

事实上马上就会看到并感受到,编写其他操作(例如Remove、Apply)的参数校验代码就是在这里抽象出来的计算模型上用DSL(ParamValidatingRule Class + List)在编程,编写Remove或Apply操作参数校验的规格说明(Specification)。

6.4.2.1.4.2 ViewImpl

ViewImpl.java

    @Override
    public void notifyAvailTransNotSpecified()
    {
        JOptionPane.showMessageDialog(this, "Specify an available transformer, please.");
    }

“JOptionPane.showMessageDialog”存在重复,抽取showInfo()方法。

ViewImpl.java

    @Override
    public void notifyAddAlreadyExistedInChainTrans()
    {
        showInfo("The transformer to be added has been already existed in the chain.");
    }

    @Override
    public void notifyAvailTransNotSpecified()
    {
        showInfo("Specify an available transformer, please.");
    }

    private void showInfo(String info)
    {
        JOptionPane.showMessageDialog(this, info);
    }
6.4.2.1.4.3 Running Result

如果使用Java Swing库实现界面控件和布局,这个异常业务流程永远不会出现。因为按目前已有的程序控制,可用转换器列表和转换器链列表在程序运行期间永远不会处于未选中任何转换器的状态。

但需要注意的是,这是由于选择使用Java Swing库实现界面控件,如果选择使用其他的库实现界面控件则未必如此。

6.4.2.2 Remove a Transformer

6.4.2.2.1 Normal Business Process 1: Remove the transformer which is not the last when the chain has more than one transformers

当转换器链列表包含多于一个的转换器时,在转换器链列表中选中不是最后的一个转换器,点击Remove按钮,选中的转换器被移除出转换器链。可用转换器列表中的转换器条目不变,选中被移除的转换器。转换器链列表选中之前选中的转换器的下一个转换器。

6.4.2.2.1.1 Presenter

####### 6.4.2.2.1.1.1 Add a Test PresenterTest.java

    public void test_remove_not_the_last_trans_when_chain_has_more_than_one_transes()
    {
        viewStub.setAvailSelectedTrans(UPPER_TRANS);
        presenter.addTrans();
        viewStub.setAvailSelectedTrans(LOWER_TRANS);
        presenter.addTrans();
        viewStub.setAvailSelectedTrans(TRIM_PREFIX_SPACES_TRANS);
        presenter.addTrans();
        viewStub.setChainSelectedTrans(LOWER_TRANS);

        presenter.removeTrans();

        assertEquals(asList(UPPER_TRANS, TRIM_PREFIX_SPACES_TRANS), viewStub.getChainTranses());
        assertEquals(1, viewStub.getAvailSelectedIndex());
        assertEquals(1, viewStub.getChainSelectedIndex());
    }

在View这一层验证View拿到的转换器链列表、可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引。

####### 6.4.2.2.1.1.2 Write the Code 1 ViewStub

ViewStub.java

    public void setChainSelectedTrans(String trans)
    {
    }

setChainSelectedTrans()是ViewStub的测试辅助方法,不是View的方法。

2 Presenter

Presenter.java

    public void removeTrans()
    {
        String chainSelectedTrans = view.getChainSelectedTrans();
        int tempChainSelectedIndex = chainTranses.indexOf(chainSelectedTrans);

        chainTranses.remove(chainSelectedTrans);
        availSelectedIndex = availTranses.indexOf(chainSelectedTrans);
        chainSelectedIndex = tempChainSelectedIndex;

        view.presentChainTranses(chainTranses);
        view.setAvailSelectedIndex(availSelectedIndex);
        view.setChainSelectedIndex(chainSelectedIndex);
    }

从View获取转换器链列表中选中的转换器。因为chainTranses是可变的,所以先计算这个转换器在转换器链列表中对应的索引,再从chainTranses中移除这个转换器。计算可用转换器列表选中转换器的索引并更新availSelectedIndex。计算转换器链列表选中转换器的索引并更新chainSelectedIndex。将chainTranses推送给View显示为转换器链列表。告诉View可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引。

3 View

View.java

    String getChainSelectedTrans();

4 ViewStub

ViewStub.java

    private String chainSelectedTrans;

    @Override
    public String getChainSelectedTrans()
    {
        return chainSelectedTrans;
    }

    public void setChainSelectedTrans(String trans)
    {
        chainSelectedTrans = trans;
    }

在getChainSelectedTrans()方法中返回在setChainSelectedTrans()方法中保存下来的chainSelectedTrans。

这还是打桩的方法,证明View.getChainSelectedTrans()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。

####### 6.4.2.2.1.1.3 Refactor the Code Presenter类抽取updateChainSelectedIndexForRemove()和updateAvailSelectedIndexForRemove()方法。

Presenter.java

    public void removeTrans()
    {
        String chainSelectedTrans = view.getChainSelectedTrans();

        updateChainSelectedIndexForRemove(chainSelectedTrans);
        chainTranses.remove(chainSelectedTrans);
        updateAvailSelectedIndexForRemove(chainSelectedTrans);

        view.presentChainTranses(chainTranses);
        view.setAvailSelectedIndex(availSelectedIndex);
        view.setChainSelectedIndex(chainSelectedIndex);
    }

    private void updateChainSelectedIndexForRemove(String chainSelectedTrans)
    {
        chainSelectedIndex = chainTranses.indexOf(chainSelectedTrans);
    }

    private void updateAvailSelectedIndexForRemove(String chainSelectedTrans)
    {
        availSelectedIndex = availTranses.indexOf(chainSelectedTrans);
    }

原来为Add操作服务的updateChainSelectedIndex()和updateAvailSelectedIndex()方法的方法名都增加“ForAdd”的后缀。

Presenter.java

    public void addTrans()
    {
        String availSelectedTrans = view.getAvailSelectedTrans();

        ValidatingResult validatingResult = validateAddTrans(availSelectedTrans);
        if(validatingResult.isSucceeded())
        {
            chainTranses.add(availSelectedTrans);
        }

        updateChainSelectedIndexForAdd(availSelectedTrans, validatingResult.getFailedReason());
        updateAvailSelectedIndexForAdd(availSelectedTrans, validatingResult.getFailedReason());

        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

    private void updateChainSelectedIndexForAdd(String availSelectedTrans, FailedReason failedReason)
    {
        if(failedReason == FailedReason.AVAIL_TRANS_NOT_SPECIFIED)
        {
            return;
        }

        chainSelectedIndex = chainTranses.indexOf(availSelectedTrans);
    }

    private void updateAvailSelectedIndexForAdd(String availSelectedTrans, FailedReason failedReason)
    {
        if(failedReason == FailedReason.AVAIL_TRANS_NOT_SPECIFIED)
        {
            availSelectedIndex = 0;
            return;
        }

        int selectedIndex = availTranses.indexOf(availSelectedTrans);
        if(failedReason == FailedReason.ADD_ALREADY_EXISTED_IN_CHAIN_TRANS)
        {
            availSelectedIndex = selectedIndex;
        }
        else
        {
            availSelectedIndex = isLastIndex(selectedIndex, availTranses) ? 0 : selectedIndex + 1;
        }
    }
6.4.2.2.1.2 ViewImpl

ViewImpl.java

    @Override
    public String getChainSelectedTrans()
    {
        return lstChain.getSelectedValue();
    }

    private void btnRemove_actionPerformed(ActionEvent e)
    {
        presenter.removeTrans();
    }
6.4.2.2.1.3 Running Result

运行起来,如下图所示:

viewimpl remove transformer

6.4.2.2.2 Normal Business Process 2: Remove the transformer which is the last when the chain has more than one transformers

当转换器链列表包含多于一个的转换器时,在转换器链列表中选中最后一个转换器,点击Remove按钮,选中的转换器被移除出转换器链。可用转换器列表中的转换器条目不变,选中被移除的转换器。转换器链列表选中第一个转换器。

6.4.2.2.2.1 Presenter

####### 6.4.2.2.2.1.1 Add a Test PresenterTest.java

    public void test_remove_the_last_trans_when_chain_has_more_than_one_transes()
    {
        viewStub.setAvailSelectedTrans(UPPER_TRANS);
        presenter.addTrans();
        viewStub.setAvailSelectedTrans(LOWER_TRANS);
        presenter.addTrans();
        viewStub.setAvailSelectedTrans(TRIM_PREFIX_SPACES_TRANS);
        presenter.addTrans();
        viewStub.setChainSelectedTrans(TRIM_PREFIX_SPACES_TRANS);

        presenter.removeTrans();

        assertEquals(asList(UPPER_TRANS, LOWER_TRANS), viewStub.getChainTranses());
        assertEquals(2, viewStub.getAvailSelectedIndex());
        assertEquals(0, viewStub.getChainSelectedIndex());
    }

在View这一层验证View拿到的转换器链列表、可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引。

####### 6.4.2.2.2.1.2 Write the Code Presenter.java

    private void updateChainSelectedIndexForRemove(String chainSelectedTrans)
    {
        int selectedIndex = chainTranses.indexOf(chainSelectedTrans);
        chainSelectedIndex = isLastIndex(selectedIndex, chainTranses) ? 0 : selectedIndex;
    }

####### 6.4.2.2.2.1.3 Refactor the Code 无。

6.4.2.2.3 Normal Business Process 3: Remove the transformer when the chain has only one transformer

当转换器链列表仅包含一个转换器时,在转换器链列表中选中这个转换器,点击Remove按钮,选中的转换器被移除出转换器链,转换器链列表为空,无选中项。可用转换器列表中的转换器条目不变,选中被移除的转换器。

6.4.2.2.3.1 Presenter

####### 6.4.2.2.3.1.1 Add a Test PresenterTest.java

    public void test_remove_a_trans_when_chain_has_only_one_transes()
    {
        viewStub.setAvailSelectedTrans(UPPER_TRANS);
        presenter.addTrans();
        viewStub.setChainSelectedTrans(UPPER_TRANS);

        presenter.removeTrans();

        assertEquals(asList(), viewStub.getChainTranses());
        assertEquals(0, viewStub.getAvailSelectedIndex());
        assertEquals(NONE_SELECTED_INDEX, viewStub.getChainSelectedIndex());
    }

在View这一层验证View拿到的转换器链列表、可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引。

####### 6.4.2.2.3.1.2 Write the Code Presenter.java

    private void updateChainSelectedIndexForRemove(String chainSelectedTrans)
    {
        if(chainTranses.size() == 1)
        {
            chainSelectedIndex = NONE_SELECTED_INDEX;
            return;
        }

        int selectedIndex = chainTranses.indexOf(chainSelectedTrans);
        chainSelectedIndex = isLastIndex(selectedIndex, chainTranses) ? 0 : selectedIndex;
    }

####### 6.4.2.2.3.1.3 Refactor the Code 无。

6.4.2.2.4 Abnormal Business Process 1: Remove a transformer when the chain is not empty but none of the transformers in the chain is specified

转换器链列表不为空,但在转换器链列表中未选中任何转换器,点击Remove按钮,提示“请在转换器链中指定一个转换器”。可用转换器列表中的转换器条目不变,选中之前已经选中的转换器。转换器链列表中的转换器条目不变,选中第一个转换器。

6.4.2.2.4.1 Presenter

####### 6.4.2.2.4.1.1 Add a Test PresenterTest.java

    public void test_remove_trans_when_chain_is_not_empty_but_chain_trans_not_specified()
    {
        viewStub.setAvailSelectedTrans(UPPER_TRANS);
        presenter.addTrans();
        viewStub.setChainSelectedTrans(NONE_SELECTED_TRANS);

        presenter.removeTrans();

        assertTrue(viewStub.isChainTransNotSpecifiedNotified());
        assertEquals(asList(UPPER_TRANS), viewStub.getChainTranses());
        assertEquals(1, viewStub.getAvailSelectedIndex());
        assertEquals(0, viewStub.getChainSelectedIndex());
    }

在验证View拿到的转换器链列表、可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引之前,先验证View是否收到chainTransNotSpecified的通知。

####### 6.4.2.2.4.1.2 Write the Code 1 ViewStub

ViewStub.java

    public boolean isChainTransNotSpecifiedNotified()
    {
        return false;
    }

isChainTransNotSpecifiedNotified()是ViewStub的测试辅助方法,不是View的方法。

2 Validator

Validator.java ValidatingResult Class

    enum FailedReason
    {
        ...

        CHAIN_TRANS_NOT_SPECIFIED
    }

3 Presenter

Presenter.java

    public void removeTrans()
    {
        String chainSelectedTrans = view.getChainSelectedTrans();

        ValidatingResult validatingResult = validateRemoveTrans(chainSelectedTrans);
        updateChainSelectedIndexForRemove(chainSelectedTrans, validatingResult.getFailedReason());
        if(validatingResult.isSucceeded())
        {
            chainTranses.remove(chainSelectedTrans);
        }
        updateAvailSelectedIndexForRemove(chainSelectedTrans, validatingResult.getFailedReason());

        view.presentChainTranses(chainTranses);
        view.setAvailSelectedIndex(availSelectedIndex);
        view.setChainSelectedIndex(chainSelectedIndex);
    }

    private ValidatingResult validateRemoveTrans(String chainSelectedTrans)
    {
        List<ParamValidatingRule<?>> rules = asList(
                paramValidatingRule(chainSelectedTrans, Presenter::transNotSpecified, 
                        view::notifyChainTransNotSpecified, FailedReason.CHAIN_TRANS_NOT_SPECIFIED));

        return Validator.validate(rules);
    }

    private void updateChainSelectedIndexForRemove(String chainSelectedTrans, FailedReason failedReason)
    {
        if(failedReason == FailedReason.CHAIN_TRANS_NOT_SPECIFIED)
        {
            chainSelectedIndex = 0;
            return;
        }

        if(chainTranses.size() == 1)
        {
            chainSelectedIndex = NONE_SELECTED_INDEX;
            return;
        }

        int selectedIndex = chainTranses.indexOf(chainSelectedTrans);
        chainSelectedIndex = isLastIndex(selectedIndex, chainTranses) ? 0 : selectedIndex;
    }

    private void updateAvailSelectedIndexForRemove(String chainSelectedTrans, FailedReason failedReason)
    {
        if(failedReason == FailedReason.CHAIN_TRANS_NOT_SPECIFIED)
        {
            return;
        }

        availSelectedIndex = availTranses.indexOf(chainSelectedTrans);
    }

4 View

View.java

    void notifyChainTransNotSpecified();

5 ViewStub

ViewStub.java

    private boolean chainTransNotSpecifiedNotified;

    @Override
    public void notifyChainTransNotSpecified()
    {
        chainTransNotSpecifiedNotified = true;
    }

    public boolean isChainTransNotSpecifiedNotified()
    {
        return chainTransNotSpecifiedNotified;
    }

在isChainTransNotSpecifiedNotified()方法中返回在notifyChainTransNotSpecified()方法中保存下来的chainTransNotSpecifiedNotified。

这还是打桩的方法,证明View.notifyChainTransNotSpecified()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。

####### 6.4.2.2.4.1.3 Refactor the Code 无。

6.4.2.2.4.2 ViewImpl

ViewImpl.java

    @Override
    public void notifyChainTransNotSpecified()
    {
        showInfo("Specify a transformer from the chain, please.");
    }
6.4.2.2.4.3 Running Result

如果使用Java Swing库实现界面控件和布局,这个异常业务流程永远不会出现。因为按目前已有的程序控制,可用转换器列表和转换器链列表在程序运行期间永远不会处于未选中任何转换器的状态。

但需要注意的是,这是由于选择使用Java Swing库实现界面控件,如果选择使用其他的库实现界面控件则未必如此。

6.4.2.2.5 Abnormal Business Process 2: Remove a transformer when the chain is empty

转换器链列表为空,无选中项,点击Remove按钮,提示“转换器链为空”。可用转换器列表中的转换器条目不变,选中第一个转换器。

6.4.2.2.5.1 Presenter

####### 6.4.2.2.5.1.1 Add a Test PresenterTest.java

    public void test_remove_trans_when_chain_is_empty()
    {
        presenter.removeTrans();

        assertTrue(viewStub.isChainEmptyNotified());
        assertEquals(asList(), viewStub.getChainTranses());
        assertEquals(0, viewStub.getAvailSelectedIndex());
        assertEquals(NONE_SELECTED_INDEX, viewStub.getChainSelectedIndex());
    }

在验证View拿到的转换器链列表、可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引之前,先验证View是否收到chainEmpty的通知。

####### 6.4.2.2.5.1.2 Write the Code 1 ViewStub

ViewStub.java

    public boolean isChainEmptyNotified()
    {
        return false;
    }

isChainEmptyNotified()是ViewStub的测试辅助方法,不是View的方法。

2 Validator

Validator.java ValidatingResult Class

    enum FailedReason
    {
        ...

        CHAIN_EMPTY
    }

3 Presenter

Presenter.java

    private ValidatingResult validateRemoveTrans(String chainSelectedTrans)
    {
        List<ParamValidatingRule<?>> rules = asList(
                paramValidatingRule(chainTranses, Presenter::emptyList, 
                        view::notifyChainEmpty, FailedReason.CHAIN_EMPTY),
                paramValidatingRule(chainSelectedTrans, Presenter::transNotSpecified, 
                        view::notifyChainTransNotSpecified, FailedReason.CHAIN_TRANS_NOT_SPECIFIED));

        return Validator.validate(rules);
    }

    private void updateChainSelectedIndexForRemove(String chainSelectedTrans, FailedReason failedReason)
    {
        if(failedReason == FailedReason.CHAIN_EMPTY)
        {
            chainSelectedIndex = NONE_SELECTED_INDEX;
            return;
        }

        if(failedReason == FailedReason.CHAIN_TRANS_NOT_SPECIFIED)
        {
            chainSelectedIndex = 0;
            return;
        }

        if(chainTranses.size() == 1)
        {
            chainSelectedIndex = NONE_SELECTED_INDEX;
            return;
        }

        int selectedIndex = chainTranses.indexOf(chainSelectedTrans);
        chainSelectedIndex = isLastIndex(selectedIndex, chainTranses) ? 0 : selectedIndex;
    }

    private void updateAvailSelectedIndexForRemove(String chainSelectedTrans, FailedReason failedReason)
    {
        if(failedReason == FailedReason.CHAIN_EMPTY)
        {
            availSelectedIndex = 0;
            return;
        }

        if(failedReason == FailedReason.CHAIN_TRANS_NOT_SPECIFIED)
        {
            return;
        }

        availSelectedIndex = availTranses.indexOf(chainSelectedTrans);
    }

    private static boolean emptyList(List<?> list)
    {
        return list.isEmpty();
    }

可以看到,removeTrans()方法不用修改,只需要修改validateRemoveTrans()方法中的“List rules”即可,但是要注意规则的顺序。在这里,校验转换器链是否为空要放在校验转换器链列表是否选中了转换器之前。updateChainSelectedIndexForRemove()和updateAvailSelectedIndexForRemove()增加CHAIN_EMPTY校验失败原因的处理分支即可。另外,新增emptyList()静态方法。

4 View

View.java

    void notifyChainEmpty();

5 ViewStub

ViewStub.java

    private boolean chainEmptyNotified;

    @Override
    public void notifyChainEmpty()
    {
        chainEmptyNotified = true;
    }

    public boolean isChainEmptyNotified()
    {
        return chainEmptyNotified;
    }

在isChainEmptyNotified()方法中返回在notifyChainEmpty()方法中保存下来的chainEmptyNotified。

这还是打桩的方法,证明View.notifyChainEmpty()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。

####### 6.4.2.2.5.1.3 Refactor the Code 无。

6.4.2.2.5.2 ViewImpl

ViewImpl.java

    @Override
    public void notifyChainEmpty()
    {
        showInfo("Specify the transformer chain, please.");
    }
6.4.2.2.5.3 Running Result

运行起来,如下图所示:

viewimpl remove transformer from empty chain

6.4.2.3 Remove All Transformers

6.4.2.3.1 Normal Business Process 1: Remove all transformers when the chain is not empty

转换器链列表不为空,无论转换器链列表是否选中转换器,点击Remove All按钮,所有转换器均被移除出转换器链,转换器链列表为空,无选中项。可用转换器列表中的转换器条目不变,选中第一个转换器。

6.4.2.3.1.1 Presenter

####### 6.4.2.3.1.1.1 Add a Test PresenterTest.java

    public void test_remove_all_transes_when_chain_is_not_empty()
    {
        viewStub.setAvailSelectedTrans(UPPER_TRANS);
        presenter.addTrans();
        viewStub.setAvailSelectedTrans(LOWER_TRANS);
        presenter.addTrans();

        presenter.removeAllTranses();

        assertEquals(asList(), viewStub.getChainTranses());
        assertEquals(0, viewStub.getAvailSelectedIndex());
        assertEquals(NONE_SELECTED_INDEX, viewStub.getChainSelectedIndex());
    }

在View这一层验证View拿到的转换器链列表、转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引。

####### 6.4.2.3.1.1.2 Write the Code Presenter.java

    public void removeAllTranses()
    {
        chainTranses.clear();
        chainSelectedIndex = NONE_SELECTED_INDEX;
        availSelectedIndex = 0;

        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

清空chainTranses。更新转换器链列表选中转换器的索引chainSelectedIndex为NONE_SELECTED_INDEX。更新可用转换器列表选中转换器的索引availSelectedIndex为0。将chainTranses推送给View显示为转换器链列表。告诉View转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引。

####### 6.4.2.3.1.1.3 Refactor the Code Presenter类抽取updateChainSelectedIndexForRemoveAll()和updateAvailSelectedIndexForRemoveAll()方法。

Presenter.java

    public void removeAllTranses()
    {
        chainTranses.clear();
        updateChainSelectedIndexForRemoveAll();
        updateAvailSelectedIndexForRemoveAll();

        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

    private void updateChainSelectedIndexForRemoveAll()
    {
        chainSelectedIndex = NONE_SELECTED_INDEX;
    }

    private void updateAvailSelectedIndexForRemoveAll()
    {
        availSelectedIndex = 0;
    }
6.4.2.3.1.2 ViewImpl

ViewImpl.java

    private void btnRemoveAll_actionPerformed(ActionEvent e)
    {
        presenter.removeAllTranses();
    }
6.4.2.3.1.3 Running Result

运行起来,如下图所示:

viewimpl remove all transformers

6.4.2.3.2 Abnormal Business Process 1: Remove all transformers when the chain is empty

转换器链列表为空,无选中项,点击Remove All按钮,提示“请指定转换器链”。可用转换器列表中的转换器条目不变,选中之前选中的转换器。

6.4.2.3.2.1 Presenter

####### 6.4.2.3.2.1.1 Add a Test PresenterTest.java

    public void test_remove_all_transes_when_chain_is_empty()
    {
        viewStub.setAvailSelectedTrans(LOWER_TRANS);
        presenter.addTrans();
        viewStub.setChainSelectedTrans(LOWER_TRANS);
        presenter.removeTrans();

        presenter.removeAllTranses();

        assertTrue(viewStub.isChainEmptyNotified());
        assertEquals(asList(), viewStub.getChainTranses());
        assertEquals(1, viewStub.getAvailSelectedIndex());
        assertEquals(NONE_SELECTED_INDEX, viewStub.getChainSelectedIndex());
    }
  • 在验证View拿到的转换器链列表、可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引之前,先验证View是否收到chainEmpty的通知。
  • 之所以先add LOWER_TRANS、再remove LOWER_TRANS这样构建空转换器链,是为了让availSelectedIndex不为0。上一个测试用例驱动出来的availSelectedIndex为0。这样验证更充分。

####### 6.4.2.3.2.1.2 Write the Code Presenter.java

    public void removeAllTranses()
    {
        ValidatingResult validatingResult = validateRemoveAllTranses();
        if(validatingResult.isSucceeded())
        {
            chainTranses.clear();
        }
        updateChainSelectedIndexForRemoveAll();
        updateAvailSelectedIndexForRemoveAll(validatingResult.getFailedReason());

        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

    private ValidatingResult validateRemoveAllTranses()
    {
        List<ParamValidatingRule<?>> rules = asList(
                paramValidatingRule(chainTranses, Presenter::emptyList, 
                        view::notifyChainEmpty, FailedReason.CHAIN_EMPTY));

        return Validator.validate(rules);
    }

    private void updateAvailSelectedIndexForRemoveAll(FailedReason failedReason)
    {
        if(failedReason == FailedReason.CHAIN_EMPTY)
        {
            return;
        }

        availSelectedIndex = 0;
    }

####### 6.4.2.3.2.1.3 Refactor the Code 无。

6.4.2.3.2.2 Running Result

运行起来,如下图所示:

viewimpl remove all transformers from empty chain

6.4.3 Apply Transformer Chain

6.4.3.1 Normal Business Process 1: Apply the transformer chain

输入合法的源字符串,构建好非空的转换器链,点击Apply按钮,将转换器链中的转换器从上到下依次应用到源字符串上,得到最终的结果字符串。

6.4.3.1.1 Presenter
6.4.3.1.1.1 Add a Test

PresenterTest.java

    public void test_apply_trans_chain()
    {
        viewStub.setAvailSelectedTrans(UPPER_TRANS);
        presenter.addTrans();
        viewStub.setSourceStr("Hello, world.");

        presenter.applyTransChain();

        assertEquals("HELLO, WORLD.", viewStub.getResultStr());
    }

在View这一层验证View拿到的转换结果字符串。

6.4.3.1.1.2 Write the Code

1 ViewStub

ViewStub.java

    public void setSourceStr(String str)
    {
    }

    public String getResultStr()
    {
        return null;
    }

setSourceStr()和getResultStr()都是ViewStub的测试辅助方法,不是View的方法。

2 Presenter

Presenter.java

    public void applyTransChain()
    {
        view.presentResultStr(businessLogic.transform(view.getSourceStr(), chainTranses));
    }

在applyTransChain()方法中,从View获取源字符串,跟chainTranses一起传给BusinessLogic进行转换,并将转换后的结果字符串推送给View显示。

3 View

View.java

    String getSourceStr();

    void presentResultStr(String str);

4 ViewStub

ViewStub.java

    private String sourceStr;
    private String resultStr;

    @Override
    public String getSourceStr()
    {
        return sourceStr;
    }

    public void setSourceStr(String str)
    {
        sourceStr = str;
    }

    @Override
    public void presentResultStr(String str)
    {
        resultStr = str;
    }

    public String getResultStr()
    {
        return resultStr;
    }
  • 在getSourceStr()方法中返回在setSourceStr()方法中保存下来的sourceStr。
  • 在getResultStr()方法中返回在presentResultStr()方法中保存下来的resultStr。

这些都是打桩的方法,证明View.getSourceStr()和View.presentResultStr()这些方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。

5 BusinessLogic

BusinessLogic.java

    String transform(String sourceStr, List<String> transes);

6 BusinessLogicStub

BusinessLogicStub.java

    @Override
    public String transform(String sourceStr, List<String> transes)
    {
        return "HELLO, WORLD.";
    }

这里容易出现两类问题:

  • 把转换逻辑写在了Presenter中。这是不对的。因为根据前面的设计,真正的转换逻辑应该在BusinessLogicImpl中实现。但此时也不应该在BusinessLogicImpl中写转换逻辑,因为现在是在测试驱动开发Presenter,所以只需要在BusinessLogicStub中打桩就可以了,不需要实现真正的转换逻辑。而且转换逻辑有可能在分布式的服务端实现。在TDD开发时,这些耦合都要解除,否则无法测试。
  • 把转换逻辑写在了BusinessLogicStub中。这也是不对的。因为真正的转换逻辑是产品代码,不应该写在测试代码中。
6.4.3.1.1.2 Refactor the Code

无。

6.4.3.1.2 BusinessLogicImpl

BusinessLogicImpl提供UPPER_TRANS转换器的转换逻辑。

6.4.3.1.2.1 Add a Test

BusinessLogicImplTest.java

    public void test_transform_upper()
    {
        BusinessLogicImpl impl = new BusinessLogicImpl();
        assertEquals("HELLO, WORLD.", impl.transform("Hello, world.", asList(UPPER_TRANS)));
    }
6.4.3.1.2.2 Write the Code

BusinessLogicImpl.java

    @Override
    public String transform(String sourceStr, List<String> transes)
    {
        return sourceStr.toUpperCase();
    }
6.4.3.1.2.3 Refactor the Code

重构测试代码。BusinessLogicImplTest类抽取setUp()方法。

    private BusinessLogicImpl impl;

    @Override
    protected void setUp()
    {
        impl = new BusinessLogicImpl();
    }

    public void test_get_all_transes()
    {
        assertEquals(asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS), impl.getAllTranses());
    }

    public void test_transform_upper()
    {
        assertEquals("HELLO, WORLD.", impl.transform("Hello, world.", asList(UPPER_TRANS)));
    }
6.4.3.1.3 ViewImpl

ViewImpl.java

    @Override
    public String getSourceStr()
    {
        return txtSourceStr.getText();
    }

    @Override
    public void presentResultStr(String str)
    {
        txtResultStr.setText(str);
    }

    private void btnApply_actionPerformed(ActionEvent e)
    {
        presenter.applyTransChain();
    }
6.4.3.1.4 Running Result

运行起来,如下图所示:

viewimpl apply transformer chain

6.4.3.2 Abnormal Business Process 1: Apply the transformer chain but the source string is empty

构建好非空的转换器链,但未输入源字符串或源字符串为空,点击Apply按钮,提示“请输入源字符串”,焦点定位到源字符串文本框。

6.4.3.2.1 Presenter
6.4.3.2.1.1 Add a Test

PresenterTest.java

    public void test_apply_trans_chain_when_source_str_is_empty()
    {
        viewStub.setAvailSelectedTrans(UPPER_TRANS);
        presenter.addTrans();
        viewStub.setSourceStr("");

        presenter.applyTransChain();

        assertTrue(viewStub.isSourceStrEmptyNotified());
        assertEquals("", viewStub.getResultStr());
    }

在View这一层先验证View是否收到sourceStrEmpty的通知,再验证View拿到的转换结果字符串是否为""。

6.4.3.2.1.2 Write the Code

1 ViewStub

ViewStub.java

    public boolean isSourceStrEmptyNotified()
    {
        return false;
    }

isSourceStrEmptyNotified()是ViewStub的测试辅助方法,不是View的方法。

2 Validator

Validator.java ValidatingResult Class

    enum FailedReason
    {
        ...

        SOURCE_STR_EMPTY
    }

3 Presenter

Presenter.java

    public void applyTransChain()
    {
        String sourceStr = view.getSourceStr();

        ValidatingResult validatingResult = validateApplyTransChain(sourceStr);
        String resultStr = validatingResult.isSucceeded() ? 
                businessLogic.transform(sourceStr, chainTranses) : "";

        view.presentResultStr(resultStr);
    }

    private ValidatingResult validateApplyTransChain(String sourceStr)
    {
        List<ParamValidatingRule<?>> rules = asList(
                paramValidatingRule(sourceStr, Presenter::emptyStr, 
                        view::notifySourceStrEmpty, FailedReason.SOURCE_STR_EMPTY));

        return Validator.validate(rules);
    }

    private static boolean emptyStr(String str)
    {
        return str.isEmpty();
    }

4 View

View.java

    void notifySourceStrEmpty();

5 ViewStub

ViewStub.java

    private boolean sourceStrEmptyNotified;

    @Override
    public void notifySourceStrEmpty()
    {
        sourceStrEmptyNotified = true;
    }

    public boolean isSourceStrEmptyNotified()
    {
        return sourceStrEmptyNotified;
    }

在isSourceStrEmptyNotified()方法中返回在notifySourceStrEmpty()方法中保存下来的sourceStrEmptyNotified。

这还是打桩的方法,证明View.notifySourceStrEmpty()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。

6.4.3.2.1.3 Refactor the Code

无。

6.4.3.2.2 ViewImpl

ViewImpl.java

    @Override
    public void notifySourceStrEmpty()
    {
        showInfo("Specify the source string, please.");
        txtSourceStr.requestFocus();
    }
6.4.3.2.3 Running Result

运行起来,如下图所示:

viewimpl apply transformer chain for empty source string

6.4.3.3 Abnormal Business Process 2: Apply the transformer chain but the source string is illegal

构建好非空的转换器链,但输入了非法的源字符串,例如包含中文字符等,点击Apply按钮,提示“请输入合法的源字符串”,焦点定位到源字符串文本框,并全选高亮当前文本。

6.4.3.3.1 Presenter
6.4.3.3.1.1 Add a Test

PresenterTest.java

    public void test_apply_trans_chain_when_source_str_is_illegal()
    {
        viewStub.setAvailSelectedTrans(UPPER_TRANS);
        presenter.addTrans();
        viewStub.setSourceStr("a中文b");

        presenter.applyTransChain();

        assertTrue(viewStub.isSourceStrIllegalNotified());
        assertEquals("", viewStub.getResultStr());
    }

在View这一层先验证View是否收到sourceStrIllegal的通知,再验证View拿到的转换结果字符串是否为""。

6.4.3.3.1.2 Write the Code

1 ViewStub

ViewStub.java

    public boolean isSourceStrIllegalNotified()
    {
        return false;
    }

isSourceStrIllegalNotified()是ViewStub的测试辅助方法,不是View的方法。

2 Validator

Validator.java ValidatingResult Class

    enum FailedReason
    {
        ...

        SOURCE_STR_ILLEGAL
    }

3 Presenter

Presenter.java

    private ValidatingResult validateApplyTransChain(String sourceStr)
    {
        List<ParamValidatingRule<?>> rules = asList(
                paramValidatingRule(sourceStr, Presenter::emptyStr, 
                        view::notifySourceStrEmpty, FailedReason.SOURCE_STR_EMPTY),
                paramValidatingRule(sourceStr, Presenter::illegalSourceStr, 
                        view::notifySourceStrIllegal, FailedReason.SOURCE_STR_ILLEGAL));

        return Validator.validate(rules);
    }

    private static boolean illegalSourceStr(String str)
    {
        return str.matches(".*[\u4e00-\u9fa5]+.*");
    }

4 View

View.java

    void notifySourceStrIllegal();

5 ViewStub

ViewStub.java

    private boolean sourceStrIllegalNotified;

    @Override
    public void notifySourceStrIllegal()
    {
        sourceStrIllegalNotified = true;
    }

    public boolean isSourceStrIllegalNotified()
    {
        return sourceStrIllegalNotified;
    }

在isSourceStrIllegalNotified()方法中返回在notifySourceStrIllegal()方法中保存下来的sourceStrIllegalNotified。

这还是打桩的方法,证明View.notifySourceStrIllegal()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。

6.4.3.3.1.3 Refactor the Code

无。

6.4.3.3.2 ViewImpl

ViewImpl.java

    @Override
    public void notifySourceStrIllegal()
    {
        showInfo("Specify the legal source string, please.");
        txtSourceStr.requestFocus();
        txtSourceStr.selectAll();
    }
6.4.3.3.3 Running Result

运行起来,如下图所示:

viewimpl apply transformer chain for illegal source string

6.4.3.4 Abnormal Business Process 3: Apply the transformer chain but the transformer chain is empty

输入合法的源字符串,但转换器链为空,点击Apply按钮,提示“请指定转换器链”。可用转换器列表中的转换器条目不变,选中第一个转换器。

6.4.3.4.1 Presenter
6.4.3.4.1.1 Add a Test

PresenterTest.java

    public void test_apply_trans_chain_when_chain_is_empty()
    {
        viewStub.setAvailSelectedTrans(LOWER_TRANS);
        presenter.addTrans();
        viewStub.setChainSelectedTrans(LOWER_TRANS);
        presenter.removeTrans();
        viewStub.setSourceStr("Hello, world.");

        presenter.applyTransChain();

        assertTrue(viewStub.isChainEmptyNotified());
        assertEquals("", viewStub.getResultStr());
        assertEquals(0, viewStub.getAvailSelectedIndex());
    }
  • 在View这一层先验证View是否收到chainEmpty的通知,再验证View拿到的转换结果字符串是否为"",以及可用转换器列表选中转换器的索引。
  • 之所以先add LOWER_TRANS、再remove LOWER_TRANS这样构建空转换器链,是为了让applyTransChain()之前的availSelectedIndex不为0。这样验证更充分。
6.4.3.4.1.2 Write the Code

Presenter.java

    public void applyTransChain()
    {
        String sourceStr = view.getSourceStr();

        ValidatingResult validatingResult = validateApplyTransChain(sourceStr);
        String resultStr = validatingResult.isSucceeded() ? 
                businessLogic.transform(sourceStr, chainTranses) : "";
        updateAvailSelectedIndexForApply(validatingResult.getFailedReason());

        view.presentResultStr(resultStr);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

    private ValidatingResult validateApplyTransChain(String sourceStr)
    {
        List<ParamValidatingRule<?>> rules = asList(
                paramValidatingRule(sourceStr, Presenter::emptyStr, 
                        view::notifySourceStrEmpty, FailedReason.SOURCE_STR_EMPTY),
                paramValidatingRule(sourceStr, Presenter::illegalSourceStr, 
                        view::notifySourceStrIllegal, FailedReason.SOURCE_STR_ILLEGAL),
                paramValidatingRule(chainTranses, Presenter::emptyList, 
                        view::notifyChainEmpty, FailedReason.CHAIN_EMPTY));

        return Validator.validate(rules);
    }

    private void updateAvailSelectedIndexForApply(FailedReason failedReason)
    {
        if(failedReason == FailedReason.CHAIN_EMPTY)
        {
            availSelectedIndex = 0;
        }
    }
6.4.3.4.1.3 Refactor the Code

无。

6.4.3.4.2 Running Result

运行起来,如下图所示:

viewimpl apply transformer chain for empty chain

6.5 BusinessLogicImpl (In Addition to Upper, Other Transformers and Transformer Chain)

6.5.1 Test Cases

1. test_get_all_transes (Done)
2. test_transform_upper (Done)
3. test_transform_lower
4. test_transform_trimPrefixSpaces
5. test_transform

6.5.2 Coding

6.5.2.1 Test Case 3: test_transform_lower

字符串转小写。

6.5.2.1.1 Add a Test

BusinessLogicImplTest.java

    public void test_transform_lower()
    {
        assertEquals("hello, world.", impl.transform("Hello, world.", asList(LOWER_TRANS)));
    }
6.5.2.1.2 Write the Code

BusinessLogicImpl.java

    @Override
    public String transform(String sourceStr, List<String> transes)
    {
        String resultStr = sourceStr;

        String trans = transes.get(0);
        if(UPPER_TRANS.equals(trans))
        {
            resultStr = sourceStr.toUpperCase();
        }
        else if(LOWER_TRANS.equals(trans))
        {
            resultStr = sourceStr.toLowerCase();
        }

        return resultStr;
    }

6.5.2.2 Test Case 4: test_transform_trimPrefixSpaces

字符串去除前缀空格。

6.5.2.2.1 Add a Test

BusinessLogicImplTest.java

import java.util.List;

    public void test_transform_trimPrefixSpaces()
    {
        List<String> transes = asList(TRIM_PREFIX_SPACES_TRANS);

        assertEquals("Hello, world.  ", impl.transform("  Hello, world.  ", transes));
        assertEquals("", impl.transform("  ", transes));
        assertEquals("Hello, world.  ", impl.transform("Hello, world.  ", transes));
    }
6.5.2.2.2 Write the Code

BusinessLogicImpl.java

    @Override
    public String transform(String sourceStr, List<String> transes)
    {
        String resultStr = sourceStr;

        String trans = transes.get(0);
        if(UPPER_TRANS.equals(trans))
        {
            resultStr = sourceStr.toUpperCase();
        }
        else if(LOWER_TRANS.equals(trans))
        {
            resultStr = sourceStr.toLowerCase();
        }
        else if(TRIM_PREFIX_SPACES_TRANS.equals(trans))
        {
            int firstNonSpaceCharIndex = -1;
            for(int i = 0; i < sourceStr.length(); i++)
            {
                if(sourceStr.charAt(i) != ' ')
                {
                    firstNonSpaceCharIndex = i;
                    break;
                }
            }

            resultStr = firstNonSpaceCharIndex == -1 ? "" : sourceStr.substring(firstNonSpaceCharIndex);
        }

        return resultStr;
    }
6.5.2.2.3 Refactor the Code

1 抽取upper()、lower()和trimPrefixSpaces()三个转换静态方法,以及findFirstNonSpaceCharIndex()静态方法。

BusinessLogicImpl.java

    @Override
    public String transform(String sourceStr, List<String> transes)
    {
        String resultStr = sourceStr;

        String trans = transes.get(0);
        if(UPPER_TRANS.equals(trans))
        {
            resultStr = upper(sourceStr);
        }
        else if(LOWER_TRANS.equals(trans))
        {
            resultStr = lower(sourceStr);
        }
        else if(TRIM_PREFIX_SPACES_TRANS.equals(trans))
        {
            resultStr = trimPrefixSpaces(sourceStr);
        }

        return resultStr;
    }

    private static String upper(String str)
    {
        return str.toUpperCase();
    }

    private static String lower(String str)
    {
        return str.toLowerCase();
    }

    private static String trimPrefixSpaces(String str)
    {
        int firstNonSpaceCharIndex = findFirstNonSpaceCharIndex(str);
        return firstNonSpaceCharIndex == -1 ? "" : str.substring(firstNonSpaceCharIndex);
    }

    private static int findFirstNonSpaceCharIndex(String str)
    {
        int firstNonSpaceCharIndex = -1;
        for(int i = 0; i < str.length(); i++)
        {
            if(str.charAt(i) != ' ')
            {
                firstNonSpaceCharIndex = i;
                break;
            }
        }
        return firstNonSpaceCharIndex;
    }

2 定义一个trans和转换方法的映射。

BusinessLogicImpl.java

import java.util.Map;
import java.util.LinkedHashMap;
import java.util.function.Function;

    private static Map<String, Function<String, String>> TRANS_FUNC_MAP = null;

    static
    {
        TRANS_FUNC_MAP = new LinkedHashMap<>();
        TRANS_FUNC_MAP.put(UPPER_TRANS, BusinessLogicImpl::upper);
        TRANS_FUNC_MAP.put(LOWER_TRANS, BusinessLogicImpl::lower);
        TRANS_FUNC_MAP.put(TRIM_PREFIX_SPACES_TRANS, BusinessLogicImpl::trimPrefixSpaces);
    }

    @Override
    public String transform(String sourceStr, List<String> transes)
    {
        return TRANS_FUNC_MAP.get(transes.get(0)).apply(sourceStr);
    }

3 getAllTranses()方法中返回映射的keys。

BusinessLogicImpl.java

    @Override
    public List<String> getAllTranses()
    {
        return asList(TRANS_FUNC_MAP.keySet().toArray(new String[] {}));
    }

4 用Stream重构findFirstNonSpaceCharIndex()方法。

BusinessLogicImpl.java

    private static int findFirstNonSpaceCharIndex(String str)
    {
        return str.chars()
                  .mapToObj(c -> asList(str.indexOf(c), c))
                  .filter(indexAndChar -> indexAndChar.get(1) != ' ')
                  .mapToInt(indexAndChar -> indexAndChar.get(0))
                  .findFirst()
                  .orElse(-1);
    }

6.5.2.3 Test Case 5: test_transform

根据转换器链进行转换。

6.5.2.3.1 Add a Test

BusinessLogicImplTest.java

    public void test_transform()
    {
        assertEquals("hello, world.  ", 
                     impl.transform("  Hello, world.  ", asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS)));
    }
6.5.2.3.2 Write the Code

BusinessLogicImpl.java

    @Override
    public String transform(String sourceStr, List<String> transes)
    {
        String resultStr = sourceStr;

        for(String trans : transes)
        {
            resultStr = TRANS_FUNC_MAP.get(trans).apply(resultStr);
        }

        return resultStr;
    }
6.5.2.3.3 Refactor the Code

用Stream重构transform()方法。

BusinessLogicImpl.java

    @Override
    public String transform(String sourceStr, List<String> transes)
    {
        return transes.stream()
                      .reduce(sourceStr, (resultStr, trans) -> TRANS_FUNC_MAP.get(trans).apply(resultStr));
    }

6.6 Review and Refactor the Code

从全局、整体上审视并重构代码。这一点很重要,站在全局整体的视角,而不再陷入局部细节。

重构主要集中在Presenter。

6.6.1 Validate

仔细观察,validateAddTrans()、validateRemoveTrans()、validateRemoveAllTranses()和validateApplyTransChain()方法中的最后一行代码都是相同的,不同的是构建“List rules”的代码。

以validateAddTrans()为例。新增buildParamValidatingRulesForAdd()方法,删除validateAddTrans()方法,addTrans改为调用Validator.validate()方法。

Presenter.java

    public void addTrans()
    {
        String availSelectedTrans = view.getAvailSelectedTrans();

        ValidatingResult validatingResult = Validator.validate(buildParamValidatingRulesForAdd(availSelectedTrans));
        if(validatingResult.isSucceeded())
        {
            chainTranses.add(availSelectedTrans);
        }

        updateChainSelectedIndexForAdd(availSelectedTrans, validatingResult.getFailedReason());
        updateAvailSelectedIndexForAdd(availSelectedTrans, validatingResult.getFailedReason());

        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

    private List<ParamValidatingRule<?>> buildParamValidatingRulesForAdd(String availSelectedTrans)
    {
        return asList(
                paramValidatingRule(availSelectedTrans, Presenter::transNotSpecified, 
                        view::notifyAvailTransNotSpecified, FailedReason.AVAIL_TRANS_NOT_SPECIFIED),
                paramValidatingRule(availSelectedTrans, this::alreadyExistedInChain, 
                        view::notifyAddAlreadyExistedInChainTrans, FailedReason.ADD_ALREADY_EXISTED_IN_CHAIN_TRANS));
    }

同理,validateRemoveTrans()、validateRemoveAllTranses()和validateApplyTransChain()做类似重构。

Presenter.java

    public void removeTrans()
    {
        String chainSelectedTrans = view.getChainSelectedTrans();

        ValidatingResult validatingResult = Validator.validate(buildParamValidatingRulesForRemove(chainSelectedTrans));
        updateChainSelectedIndexForRemove(chainSelectedTrans, validatingResult.getFailedReason());
        if(validatingResult.isSucceeded())
        {
            chainTranses.remove(chainSelectedTrans);
        }
        updateAvailSelectedIndexForRemove(chainSelectedTrans, validatingResult.getFailedReason());

        view.presentChainTranses(chainTranses);
        view.setAvailSelectedIndex(availSelectedIndex);
        view.setChainSelectedIndex(chainSelectedIndex);
    }

    private List<ParamValidatingRule<?>> buildParamValidatingRulesForRemove(String chainSelectedTrans)
    {
        return asList(
                paramValidatingRule(chainTranses, Presenter::emptyList, 
                        view::notifyChainEmpty, FailedReason.CHAIN_EMPTY),
                paramValidatingRule(chainSelectedTrans, Presenter::transNotSpecified, 
                        view::notifyChainTransNotSpecified, FailedReason.CHAIN_TRANS_NOT_SPECIFIED));
    }

    public void removeAllTranses()
    {
        ValidatingResult validatingResult = Validator.validate(buildParamValidatingRulesForRemoveAll());
        if(validatingResult.isSucceeded())
        {
            chainTranses.clear();
        }
        updateChainSelectedIndexForRemoveAll();
        updateAvailSelectedIndexForRemoveAll(validatingResult.getFailedReason());

        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

    private List<ParamValidatingRule<?>> buildParamValidatingRulesForRemoveAll()
    {
        return asList(
                paramValidatingRule(chainTranses, Presenter::emptyList, 
                        view::notifyChainEmpty, FailedReason.CHAIN_EMPTY));
    }

    public void applyTransChain()
    {
        String sourceStr = view.getSourceStr();

        ValidatingResult validatingResult = Validator.validate(buildParamValidatingRulesForApply(sourceStr));
        String resultStr = validatingResult.isSucceeded() ? 
                businessLogic.transform(sourceStr, chainTranses) : "";
        updateAvailSelectedIndexForApply(validatingResult.getFailedReason());

        view.presentResultStr(resultStr);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

    private List<ParamValidatingRule<?>> buildParamValidatingRulesForApply(String sourceStr)
    {
        return asList(
                paramValidatingRule(sourceStr, Presenter::emptyStr, 
                        view::notifySourceStrEmpty, FailedReason.SOURCE_STR_EMPTY),
                paramValidatingRule(sourceStr, Presenter::illegalSourceStr, 
                        view::notifySourceStrIllegal, FailedReason.SOURCE_STR_ILLEGAL),
                paramValidatingRule(chainTranses, Presenter::emptyList, 
                        view::notifyChainEmpty, FailedReason.CHAIN_EMPTY));
    }

6.6.2 operTrans

仔细观察,addTrans()、removeTrans()、removeAllTranses()和applyTransChain()方法中的代码都遵循同一种模式,模式用伪码描述如下:

    viewData = collectViewData();
    validatingResult = Validator.validate(buildParamValidatingRules(viewData));
    updatePresenterData(viewData, validatingResult);
    presentViewData();

用函数式编程重构这部分代码。在Presenter.java中新增OperData类,并新增operTrans()方法。

Presenter.java OperData Class

import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;

class OperData<T>
{
    private Optional<Supplier<Optional<T>>> collectViewDataFunc;
    private Function<Optional<T>, List<ParamValidatingRule<?>>> buildParamValidatingRulesFunc;
    private BiConsumer<Optional<T>, ValidatingResult> updatePresenterDataFunc;
    private Runnable presentViewDataFunc;

    static <T> OperData<T> operData(
            Optional<Supplier<Optional<T>>> collectViewDataFunc, 
            Function<Optional<T>, List<ParamValidatingRule<?>>> buildParamValidatingRulesFunc, 
            BiConsumer<Optional<T>, ValidatingResult> updatePresenterDataFunc, 
            Runnable presentViewDataFunc)
    {
        return new OperData<>(collectViewDataFunc, buildParamValidatingRulesFunc, 
                updatePresenterDataFunc, presentViewDataFunc);
    }

    private OperData(Optional<Supplier<Optional<T>>> collectViewDataFunc, 
                     Function<Optional<T>, List<ParamValidatingRule<?>>> buildParamValidatingRulesFunc, 
                     BiConsumer<Optional<T>, ValidatingResult> updatePresenterDataFunc, 
                     Runnable presentViewDataFunc)
    {
        this.collectViewDataFunc = collectViewDataFunc;
        this.buildParamValidatingRulesFunc = buildParamValidatingRulesFunc;
        this.updatePresenterDataFunc = updatePresenterDataFunc;
        this.presentViewDataFunc = presentViewDataFunc;
    }

    public Optional<Supplier<Optional<T>>> getCollectViewDataFunc()
    {
        return collectViewDataFunc;
    }

    public Function<Optional<T>, List<ParamValidatingRule<?>>> getBuildParamValidatingRulesFunc()
    {
        return buildParamValidatingRulesFunc;
    }

    public BiConsumer<Optional<T>, ValidatingResult> getUpdatePresenterDataFunc()
    {
        return updatePresenterDataFunc;
    }

    public Runnable getPresentViewDataFunc()
    {
        return presentViewDataFunc;
    }
}

需要注意的是,对于removeAllTranses()方法,由于不需要collectViewData这个步骤,也没有viewData,所以collectViewDataFunc本身的类型是一个Optional,而Supplier的泛型类型也是一个Optional。由此,buildParamValidatingRulesFunc的输入参数和updatePresenterDataFunc的第一个输入参数的类型都是Optional。

Presenter.java Presenter Class

    private <T> void operTrans(OperData<T> operData)
    {
        Optional<T> viewData = operData.getCollectViewDataFunc().orElse(() -> Optional.empty()).get();
        ValidatingResult validatingResult = Validator.validate(operData.getBuildParamValidatingRulesFunc().apply(viewData));
        operData.getUpdatePresenterDataFunc().accept(viewData, validatingResult);
        operData.getPresentViewDataFunc().run();
    }

修改addTrans()方法,新增collectViewDataForAdd()、updatePresenterDataForAdd()和presentViewDataForAdd()方法。将buildParamValidatingRulesForAdd()方法的输入参数由“String availSelectedTrans”改为“Optional viewData”。

Presenter.java Presenter Class

import static fayelab.tdd.stringtransformer.dummy.original.OperData.operData;

    public void addTrans()
    {
        operTrans(operData(Optional.of(this::collectViewDataForAdd), 
                           this::buildParamValidatingRulesForAdd, 
                           this::updatePresenterDataForAdd, 
                           this::presentViewDataForAdd));
    }

    private Optional<String> collectViewDataForAdd()
    {
        return Optional.ofNullable(view.getAvailSelectedTrans());
    }

    private List<ParamValidatingRule<?>> buildParamValidatingRulesForAdd(Optional<String> viewData)
    {
        String availSelectedTrans = viewData.orElse(NONE_SELECTED_TRANS);
        return asList(
                paramValidatingRule(availSelectedTrans, Presenter::transNotSpecified, 
                        view::notifyAvailTransNotSpecified, FailedReason.AVAIL_TRANS_NOT_SPECIFIED),
                paramValidatingRule(availSelectedTrans, this::alreadyExistedInChain, 
                        view::notifyAddAlreadyExistedInChainTrans, FailedReason.ADD_ALREADY_EXISTED_IN_CHAIN_TRANS));
    }

    private void updatePresenterDataForAdd(Optional<String> viewData, ValidatingResult validatingResult)
    {
        String availSelectedTrans = viewData.orElse(NONE_SELECTED_TRANS);
        if(validatingResult.isSucceeded())
        {
            chainTranses.add(availSelectedTrans);
        }
        updateChainSelectedIndexForAdd(availSelectedTrans, validatingResult.getFailedReason());
        updateAvailSelectedIndexForAdd(availSelectedTrans, validatingResult.getFailedReason());
    }

    private void presentViewDataForAdd()
    {
        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

修改removeTrans()方法,新增collectViewDataForRemove()、updatePresenterDataForRemove()和presentViewDataForRemove()方法。将buildParamValidatingRulesForRemove()方法的输入参数由“String chainSelectedTrans”改为“Optional viewData”。

Presenter.java

    public void removeTrans()
    {
        operTrans(operData(Optional.of(this::collectViewDataForRemove), 
                           this::buildParamValidatingRulesForRemove, 
                           this::updatePresenterDataForRemove, 
                           this::presentViewDataForRemove));
    }

    private Optional<String> collectViewDataForRemove()
    {
        return Optional.ofNullable(view.getChainSelectedTrans());
    }

    private List<ParamValidatingRule<?>> buildParamValidatingRulesForRemove(Optional<String> viewData)
    {
        String chainSelectedTrans = viewData.orElse(NONE_SELECTED_TRANS);
        return asList(
                paramValidatingRule(chainTranses, Presenter::emptyList, 
                        view::notifyChainEmpty, FailedReason.CHAIN_EMPTY),
                paramValidatingRule(chainSelectedTrans, Presenter::transNotSpecified, 
                        view::notifyChainTransNotSpecified, FailedReason.CHAIN_TRANS_NOT_SPECIFIED));
    }

    private void updatePresenterDataForRemove(Optional<String> viewData, ValidatingResult validatingResult)
    {
        String chainSelectedTrans = viewData.orElse(NONE_SELECTED_TRANS);
        updateChainSelectedIndexForRemove(chainSelectedTrans, validatingResult.getFailedReason());
        if(validatingResult.isSucceeded())
        {
            chainTranses.remove(chainSelectedTrans);
        }
        updateAvailSelectedIndexForRemove(chainSelectedTrans, validatingResult.getFailedReason());
    }

    private void presentViewDataForRemove()
    {
        view.presentChainTranses(chainTranses);
        view.setAvailSelectedIndex(availSelectedIndex);
        view.setChainSelectedIndex(chainSelectedIndex);
    }

对于removeAllTranses()方法,不需要collectViewData这个步骤,operTrans()方法的第一个参数传入Optional.empty()。新增updatePresenterDataForRemoveAll()和presentViewDataForRemoveAll()方法。将buildParamValidatingRulesForRemoveAll()方法由无参改为“Optional<?> emptyViewData”。

Presenter.java

    public void removeAllTranses()
    {
        operTrans(operData(Optional.empty(), 
                           this::buildParamValidatingRulesForRemoveAll, 
                           this::updatePresenterDataForRemoveAll, 
                           this::presentViewDataForRemoveAll));
    }

    private List<ParamValidatingRule<?>> buildParamValidatingRulesForRemoveAll(Optional<?> emptyViewData)
    {
        return asList(
                paramValidatingRule(chainTranses, Presenter::emptyList, 
                        view::notifyChainEmpty, FailedReason.CHAIN_EMPTY));
    }

    private void updatePresenterDataForRemoveAll(Optional<?> emptyViewData, ValidatingResult validatingResult)
    {
        if(validatingResult.isSucceeded())
        {
            chainTranses.clear();
        }
        updateChainSelectedIndexForRemoveAll();
        updateAvailSelectedIndexForRemoveAll(validatingResult.getFailedReason());
    }

    private void presentViewDataForRemoveAll()
    {
        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

仔细观察,presentViewDataForAdd()、presentViewDataForRemove()和presentViewDataForRemoveAll()方法的代码是一样的,新增presentViewDataForBuildTransChain()方法,替换这三个方法。

Presenter.java

    public void addTrans()
    {
        operTrans(operData(Optional.of(this::collectViewDataForAdd), 
                           this::buildParamValidatingRulesForAdd, 
                           this::updatePresenterDataForAdd, 
                           this::presentViewDataForBuildTransChain));
    }

    public void removeTrans()
    {
        operTrans(operData(Optional.of(this::collectViewDataForRemove), 
                           this::buildParamValidatingRulesForRemove, 
                           this::updatePresenterDataForRemove, 
                           this::presentViewDataForBuildTransChain));
    }

    public void removeAllTranses()
    {
        operTrans(operData(Optional.empty(), 
                           this::buildParamValidatingRulesForRemoveAll, 
                           this::updatePresenterDataForRemoveAll, 
                           this::presentViewDataForBuildTransChain));
    }

    private void presentViewDataForBuildTransChain()
    {
        view.presentChainTranses(chainTranses);
        view.setChainSelectedIndex(chainSelectedIndex);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

修改applyTransChain()方法,新增collectViewDataForApply()、updatePresenterDataForApply()和presentViewDataForApply()方法。将buildParamValidatingRulesForApply()方法的输入参数由“String sourceStr”改为“Optional viewData”。

为了让updatePresenterDataForApply()方法更名副其实,新增resultStr成员变量,并初始化为null。

Presenter.java

    private String resultStr;

    public void applyTransChain()
    {
        operTrans(operData(Optional.of(this::collectViewDataForApply), 
                           this::buildParamValidatingRulesForApply, 
                           this::updatePresenterDataForApply, 
                           this::presentViewDataForApply));
    }

    private Optional<String> collectViewDataForApply()
    {
        return Optional.ofNullable(view.getSourceStr());
    }

    private List<ParamValidatingRule<?>> buildParamValidatingRulesForApply(Optional<String> viewData)
    {
        String sourceStr = viewData.get();
        return asList(
                paramValidatingRule(sourceStr, Presenter::emptyStr, 
                        view::notifySourceStrEmpty, FailedReason.SOURCE_STR_EMPTY),
                paramValidatingRule(sourceStr, Presenter::illegalSourceStr, 
                        view::notifySourceStrIllegal, FailedReason.SOURCE_STR_ILLEGAL),
                paramValidatingRule(chainTranses, Presenter::emptyList, 
                        view::notifyChainEmpty, FailedReason.CHAIN_EMPTY));
    }

    private void updatePresenterDataForApply(Optional<String> viewData, ValidatingResult validatingResult)
    {
        resultStr = validatingResult.isSucceeded() ? businessLogic.transform(viewData.get(), chainTranses) : "";
        updateAvailSelectedIndexForApply(validatingResult.getFailedReason());
    }

    private void presentViewDataForApply()
    {
        view.presentResultStr(resultStr);
        view.setAvailSelectedIndex(availSelectedIndex);
    }

这里依然运用了“计算描述与执行分离”的设计思想。定义OperData就是在描述计算,而Presenter.operTrans就是在执行计算,是一个解释器。

定义OperData的过程就是在用自定义的领域特定语言(DSL: Domain-Specific Language)定义一份规格说明(Specification)。这份规格说明是一个纯数据,是声明性的、抽象的。只要把这份规格说明传给解释器Presenter.operTrans解释执行,结果就出来了。这个领域特定语言最主要的语素是OperData(collectViewData, buildParamValidatingRules, updatePresenterData, presentViewData)。它们是针对“操作转换器”这个特定问题领域抽象出来的计算模型。这是最本质的抽象,跟OO或函数式编程没有关系,因为它们都是实现层面的,不是设计层面的。事实上,我们现在使用函数式编程实现的,也可以改为用OO实现,但OperData描述不变。

我们可以使用实现语言(Python/Java/C等)内置的语言特性实现DSL,例如这里的OperData,也可以自定义一套跟实现语言无关的小语言实现。

用DSL编写规格说明(Specification)也是在“编程”,只不过是在我们自己抽象出的针对“操作转换器”这个特定问题领域的计算模型上用DSL表达计算,即“编程”,跟我们用Java编程并没有本质区别。用Java编程不过是在Java语言提供的计算模型上用Java语言提供的DSL表达计算,即“编程”。

编写addTrans、removeTrans、removeAllTranses、applyTransChain的代码就是在这里抽象出来的计算模型上用DSL(OperData)在编程,编写这些操作的规格说明(Specification)。

7 Summary

请学员先总结一下。

  1. TDD三步军规、小步。
  2. 测试:
    • 始终选择当前最有价值的测试用例;
    • 测试三段式:Arrange、Act、Assert;
    • 不要忘了测试代码也需要重构;
    • 合理使用TestSuite。
  3. 规范:
    • 代码目录规范:testsrc、src;
    • 类名命名规范:XyzTest、Xyz。
  4. 每次修改代码,都运行一下测试用例,注意红绿绿的节奏。
  5. 写代码应做到:
    • 简洁明了:概念定义清晰、准确;简明清晰地表达语义;
    • 表达到位:层次分明、表现意图;跟问题领域的概念、规则相吻合;
    • 安全可靠:状态、时序管理得当;具有必要的测试安全网络。

8 Homework

  1. 【必选】字符串转换器支持“Reverse”转换。举个例子:

    "Hello, world." -- Reverse --> ".dlrow , olleH"
    
  2. 【必选】新增“Add All”(添加所有转换器)功能。

9 My Homework

9.1 Reverse

字符串转换器支持“Reverse”转换。

主要修改BusinessLogicImpl。

代码包路径:fayelab.tdd.stringtransformer.dummy.reverse

9.1.1 Test Code

BusinessLogicImplTest.java

    public void test_get_all_transes()
    {
        assertEquals(asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS, REVERSE_TRANS), impl.getAllTranses());
    }

    public void test_transform_reverse()
    {
        assertEquals("  .dlrow ,olleH  ", impl.transform("  Hello, world.  ", asList(REVERSE_TRANS)));
    }

9.1.2 Product Code

Trans.java

    static final String REVERSE_TRANS = "Reverse";

BusinessLogicImpl.java

    static
    {
        TRANS_FUNC_MAP = new LinkedHashMap<>();
        TRANS_FUNC_MAP.put(UPPER_TRANS, BusinessLogicImpl::upper);
        TRANS_FUNC_MAP.put(LOWER_TRANS, BusinessLogicImpl::lower);
        TRANS_FUNC_MAP.put(TRIM_PREFIX_SPACES_TRANS, BusinessLogicImpl::trimPrefixSpaces);
        TRANS_FUNC_MAP.put(REVERSE_TRANS, BusinessLogicImpl::reverse);
    }

    private static String reverse(String str)
    {
        return new StringBuffer(str).reverse().toString();
    }

9.1.3 Run

运行效果如下图所示:

viewimpl reverse

9.2 Add All

新增“Add All”(添加所有转换器)功能。

代码包路径:fayelab.tdd.stringtransformer.dummy.addall

9.2.1 Business Process of Add All Transformers

无论转换器链列表是否为空,无论可用转换器列表是否选中转换器,点击Add All按钮,所有转换器均被添加到转换器链列表。转换器链列表和可用转换器列表完全一样。可用转换器列表中的转换器条目不变,选中第一个转换器。转换器链列表选中最后一个转换器。

9.2.2 Implementation

9.2.2.1 Presenter

9.2.2.1.1 Test Code

PresenterTest.java

    public void test_add_all_transes()
    {
        presenter.addAllTranses();

        assertEquals(asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS), viewStub.getChainTranses());
        assertEquals(0, viewStub.getAvailSelectedIndex());
        assertEquals(2, viewStub.getChainSelectedIndex());
    }

在View这一层验证View拿到的转换器链列表、可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引。

9.2.2.1.2 Product Code

需要注意的是,对于addAllTranses()方法,除了不需要collectViewData步骤,buildParamValidatingRules步骤也不需要,所以OperData类中的buildParamValidatingRulesFunc本身的类型也是一个Optional。当buildParamValidatingRulesFunc为Optional.empty()时,不做参数校验操作,也没有参数校验结果,因此OperData类中的updatePresenterDataFunc的第二个参数类型由ValidatingResult改为Optional。

为了做到小步,新增OperData2类和operTrans2()方法。

Presenter.java OperData2 Class

class OperData2<T>
{
    private Optional<Supplier<Optional<T>>> collectViewDataFunc;
    private Optional<Function<Optional<T>, List<ParamValidatingRule<?>>>> buildParamValidatingRulesFunc;
    private BiConsumer<Optional<T>, Optional<ValidatingResult>> updatePresenterDataFunc;
    private Runnable presentViewDataFunc;

    static <T> OperData2<T> operData2(
            Optional<Supplier<Optional<T>>> collectViewDataFunc, 
            Optional<Function<Optional<T>, List<ParamValidatingRule<?>>>> buildParamValidatingRulesFunc, 
            BiConsumer<Optional<T>, Optional<ValidatingResult>> updatePresenterDataFunc, 
            Runnable presentViewDataFunc)
    {
        return new OperData2<>(collectViewDataFunc, buildParamValidatingRulesFunc, 
                updatePresenterDataFunc, presentViewDataFunc);
    }

    private OperData2(Optional<Supplier<Optional<T>>> collectViewDataFunc, 
                      Optional<Function<Optional<T>, List<ParamValidatingRule<?>>>> buildParamValidatingRulesFunc, 
                      BiConsumer<Optional<T>, Optional<ValidatingResult>> updatePresenterDataFunc, 
                      Runnable presentViewDataFunc)
    {
        this.collectViewDataFunc = collectViewDataFunc;
        this.buildParamValidatingRulesFunc = buildParamValidatingRulesFunc;
        this.updatePresenterDataFunc = updatePresenterDataFunc;
        this.presentViewDataFunc = presentViewDataFunc;
    }

    public Optional<Supplier<Optional<T>>> getCollectViewDataFunc()
    {
        return collectViewDataFunc;
    }

    public Optional<Function<Optional<T>, List<ParamValidatingRule<?>>>> getBuildParamValidatingRulesFunc()
    {
        return buildParamValidatingRulesFunc;
    }

    public BiConsumer<Optional<T>, Optional<ValidatingResult>> getUpdatePresenterDataFunc()
    {
        return updatePresenterDataFunc;
    }

    public Runnable getPresentViewDataFunc()
    {
        return presentViewDataFunc;
    }
}

Presenter.java Presenter Class

    private <T> void operTrans2(OperData2<T> operData)
    {
        Optional<T> viewData = operData.getCollectViewDataFunc().orElse(() -> Optional.empty()).get();
        Optional<ValidatingResult> validatingResult = 
                operData.getBuildParamValidatingRulesFunc()
                        .flatMap(func -> Optional.of(Validator.validate(func.apply(viewData))));
        operData.getUpdatePresenterDataFunc().accept(viewData, validatingResult);
        operData.getPresentViewDataFunc().run();
    }

新增addAllTranses()、updatePresenterDataForAddAll()、updateChainSelectedIndexForAddAll()和updateAvailSelectedIndexForAddAll()方法。

Presenter.java

import static fayelab.tdd.stringtransformer.dummy.addall.OperData2.operData2;

    public void addAllTranses()
    {
        operTrans2(operData2(Optional.empty(), 
                             Optional.empty(), 
                             this::updatePresenterDataForAddAll, 
                             this::presentViewDataForBuildTransChain));
    }

    private void updatePresenterDataForAddAll(Optional<?> emptyViewData, Optional<?> emptyValidatingResult)
    {
        chainTranses.clear();
        chainTranses.addAll(availTranses);
        updateChainSelectedIndexForAddAll();
        updateAvailSelectedIndexForAddAll();
    }

    private void updateChainSelectedIndexForAddAll()
    {
        chainSelectedIndex = chainTranses.size() - 1;
    }

    private void updateAvailSelectedIndexForAddAll()
    {
        availSelectedIndex = 0;
    }

修改addTrans()方法,改为调用operTrans2()方法,并传入OperData2类的对象实例。将updatePresenterDataForAdd()方法的第二个输入参数由“ValidatingResult validatingResult”改为“Optional opValidatingResult”。

Presenter.java

    public void addTrans()
    {
        operTrans2(operData2(Optional.of(this::collectViewDataForAdd), 
                             Optional.of(this::buildParamValidatingRulesForAdd), 
                             this::updatePresenterDataForAdd, 
                             this::presentViewDataForBuildTransChain));
    }

    private void updatePresenterDataForAdd(Optional<String> viewData, Optional<ValidatingResult> opValidatingResult)
    {
        String availSelectedTrans = viewData.orElse(NONE_SELECTED_TRANS);
        ValidatingResult validatingResult = opValidatingResult.get();
        if(validatingResult.isSucceeded())
        {
            chainTranses.add(availSelectedTrans);
        }
        updateChainSelectedIndexForAdd(availSelectedTrans, validatingResult.getFailedReason());
        updateAvailSelectedIndexForAdd(availSelectedTrans, validatingResult.getFailedReason());
    }

修改removeTrans()方法,改为调用operTrans2()方法,并传入OperData2类的对象实例。将updatePresenterDataForRemove()方法的第二个输入参数由“ValidatingResult validatingResult”改为“Optional opValidatingResult”。

Presenter.java

    public void removeTrans()
    {
        operTrans2(operData2(Optional.of(this::collectViewDataForRemove), 
                             Optional.of(this::buildParamValidatingRulesForRemove), 
                             this::updatePresenterDataForRemove, 
                             this::presentViewDataForBuildTransChain));
    }

    private void updatePresenterDataForRemove(Optional<String> viewData, Optional<ValidatingResult> opValidatingResult)
    {
        String chainSelectedTrans = viewData.orElse(NONE_SELECTED_TRANS);
        ValidatingResult validatingResult = opValidatingResult.get();
        updateChainSelectedIndexForRemove(chainSelectedTrans, validatingResult.getFailedReason());
        if(validatingResult.isSucceeded())
        {
            chainTranses.remove(chainSelectedTrans);
        }
        updateAvailSelectedIndexForRemove(chainSelectedTrans, validatingResult.getFailedReason());
    }

修改removeAllTranses()方法,改为调用operTrans2()方法,并传入OperData2类的对象实例。将updatePresenterDataForRemoveAll()方法的第二个输入参数由“ValidatingResult validatingResult”改为“Optional opValidatingResult”。

Presenter.java

    public void removeAllTranses()
    {
        operTrans2(operData2(Optional.empty(), 
                             Optional.of(this::buildParamValidatingRulesForRemoveAll), 
                             this::updatePresenterDataForRemoveAll, 
                             this::presentViewDataForBuildTransChain));
    }

    private void updatePresenterDataForRemoveAll(Optional<?> emptyViewData, Optional<ValidatingResult> opValidatingResult)
    {
        ValidatingResult validatingResult = opValidatingResult.get();
        if(validatingResult.isSucceeded())
        {
            chainTranses.clear();
        }
        updateChainSelectedIndexForRemoveAll();
        updateAvailSelectedIndexForRemoveAll(validatingResult.getFailedReason());
    }

修改applyTransChain()方法,改为调用operTrans2()方法,并传入OperData2类的对象实例。将updatePresenterDataForApply()方法的第二个输入参数由“ValidatingResult validatingResult”改为“Optional opValidatingResult”。

Presenter.java

    public void applyTransChain()
    {
        operTrans2(operData2(Optional.of(this::collectViewDataForApply), 
                             Optional.of(this::buildParamValidatingRulesForApply), 
                             this::updatePresenterDataForApply, 
                             this::presentViewDataForApply));
    }

    private void updatePresenterDataForApply(Optional<String> viewData, Optional<ValidatingResult> opValidatingResult)
    {
        ValidatingResult validatingResult = opValidatingResult.get();
        resultStr = validatingResult.isSucceeded() ? businessLogic.transform(viewData.get(), chainTranses) : "";
        updateAvailSelectedIndexForApply(validatingResult.getFailedReason());
    }

删除不再有用的operTrans()方法和OperData类,并将operTrans2()重命名为operTrans(),将OperData2重命名为OperData。

9.2.2.2 ViewImpl

ViewImpl.java

    private void initUI()
    {
        ...

        pnCCenter.setLayout(new GridLayout(10, 1));

        ...

        pnCCenter.add(pnBtnAddAll);
        pnCCenter.add(new JPanel());

        ...

        pnBtnAddAll.setLayout(new BorderLayout());
        btnAddAll.setText("Add All");
        btnAddAll.setPreferredSize(new Dimension(120, 23));
        btnAddAll.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                btnAddAll_actionPerformed(e);
            }
        });
        pnBtnAddAll.add(btnAddAll, BorderLayout.NORTH);
    }

    private void btnAddAll_actionPerformed(ActionEvent e)
    {
        presenter.addAllTranses();
    }

    private JPanel pnBtnAddAll = new JPanel();
    private JButton btnAddAll = new JButton();

9.2.2.3 Running Result

运行起来,如下图所示:

viewimpl add all transformers

⚠️ **GitHub.com Fallback** ⚠️