201803 TDD Coding Practice String Transformer Reloaded - xiaoxianfaye/Courses GitHub Wiki
- 1 Review
- 2 Stop to Think
-
3 Implementation
- 3.1 Init
-
3.2 Build Transformer Chain
-
3.2.1 Add a Transformer
- 3.2.1.1 Normal Business Process 1: Add the transformer which is not the last
- 3.2.1.2 Normal Business Process 2: Add the transformer which is the last
- 3.2.1.3 Abnormal Business Process 1: Add a transformer which has been already existed in the chain
- 3.2.1.4 Abnormal Business Process 2: Add a transformer but none of the available transformers is specified
-
3.2.2 Remove a Transformer
- 3.2.2.1 Normal Business Process 1: Remove the transformer which is not the last when the chain has more than one transformers
- 3.2.2.2 Normal Business Process 2: Remove the transformer which is the last when the chain has more than one transformers
- 3.2.2.3 Normal Business Process 3: Remove the transformer when the chain has only one transformer
- 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
- 3.2.2.5 Abnormal Business Process 2: Remove a transformer when the chain is empty
- 3.2.3 Remove All Transformers
-
3.2.1 Add a Transformer
-
3.3 Apply Transformer Chain
- 3.3.1 Normal Business Process 1: Apply the transformer chain
- 3.3.2 Abnormal Business Process 1: Apply the transformer chain but the source string is empty
- 3.3.3 Abnormal Business Process 2: Apply the transformer chain but the source string is illegal
- 3.3.4 Abnormal Business Process 3: Apply the transformer chain but the transformer chain is empty
- 3.4 BusinessLogicImpl (In Addition to Upper, Other Transformers and Transformer Chain)
- 3.5 operTrans
- 4 Improvement
- 5 My Homework
实现一个基于GUI的应用——字符串转换器。
用户在“Source String(源字符串)”文本框中输入一个英文字符串,然后选择所需要的转换器,点击“Apply(应用)”按钮,系统会按照用户所选择的转换器链对输入字符串进行转换,并将结果显示在“Result String(结果字符串)”文本框中。
目前有3个转换器:Upper(转换为大写)、Lower(转换为小写)和TrimPrefixSpaces(去除前缀空格)。
举个例子,用户输入“ hello, world. ”,并依次添加了Upper和TrimPrefixSpaces转换器到转换器链中,点击Apply按钮后的界面可参考下图:
在上一次课程中,我们根据《测试总线势在必行——设计支持自动化验收测试的架构》这篇论文中介绍的“Bypassing the UI”的理论方法(下面第1张图)设计出一个分层系统(Layered System,下面第2张图)。
- View(视图层):Layout、Accept User Input / Output to User
- Presenter(表示层):Client-side Presentation、Validation、Calculation
- Business Logic(业务逻辑层):Business Logic
全局类图如下图所示:
我们停下来思考一下。
请学员思考目前的设计与实现是否存在问题?
Presenter和View的交互接口太过细致,关注的是交互实现细节,而不是真正的交互逻辑。
另外,每个参数的每种校验失败情况都对应了View接口中的一个通知方法,也偏向于“交互实现细节”,而不是真正的“交互逻辑”。
这会导致:
- View接口的频繁变更。
- Presenter测试代码与产品代码的频繁变更。
- Presenter实现与界面中使用的控件细节密切相关。
而View接口、Presenter测试代码与产品代码本应是系统中相对稳定的部分,不应也不宜频繁变更,更不应与界面使用的控件细节密切相关。
关于第3点,再详细说明一下。
在Java实现里,使用Java Swing库实现界面控件和布局。转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引的设置先后顺序无关紧要。并且使用Java Swing库可以使得可用转换器列表和转换器链列表在程序运行期间永远不会处于未选中任何转换器的状态。因此“添加转换器时未指定可用转换器”和“移除转换器时未指定转换器链列表中的转换器”等异常业务流程无需处理。
而在Python实现里,使用了Python自带的Tkinter库实现界面控件和布局。Tkinter的控件焦点控制机制导致只有最后设置选中的控件才会高亮显示出选中状态,因此转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引的设置先后顺序就有讲究了。而且可用转换器列表和转换器链列表可能处于未选中任何转换器的状态,因此必须要增加“添加转换器时未指定可用转换器”和“移除转换器时未指定转换器链列表中的转换器”等异常业务流程的处理。
有以下几个改进方向:
-
Presenter应关注交互逻辑而不是交互实现细节,才能使得View接口、Presenter测试代码及其产品代码都不会因为界面展现细节的变化而频繁变更,保持相对稳定。
-
既然之前的Presenter有实现细节与控件相关,那就把这些实现细节放到ViewImpl中,使得Presenter实现与界面中使用的控件细节无关。
-
定义统一的参数校验失败通知方法。
为此,Presenter与View的交互接口采用指令式,将计算描述与执行分离。Presenter计算好交互逻辑所需要的所有数据,封装为指令,传递给View,View根据指令控制界面控件展现细节。
这么实现的话,View不再是纯哑(Dummy)的,View中会有一些实现细节逻辑。需要注意,即便View中包含一些实现细节逻辑,也只能是和界面控件细节相关的逻辑,不能是业务逻辑,而且要尽量简单,简单到一眼就能看出有没有问题。
按照上一次课程中总结的业务流程,还是从交付角度,以一个业务流程为单位,将所有业务流程按端到端(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
上一次课程中提到了只包括界面控件和布局但不包括业务流程的ViewImpl类,这次依然在这个类的基础上实现完整的ViewImpl。
代码详见ViewImpl(Only Layout, Modify Package).java。注意可能需要修改包路径。
另外,上一次课程中介绍过的重构细节、运行结果均不再详细展示。
代码包路径:fayelab.tdd.stringtransformer.instruction.original
初始化完成后:
- 在可用转换器列表中依次呈现Upper、Lower和TrimPrefixSpaces三个条目,并选中第一个转换器。
- 转换器链列表、源字符串文本框、结果字符串文本框均为空。
Presenter与View的交互接口会有多个,这些交互接口的交互数据不尽相同,如果每个交互数据都定义自己的数据类,有点繁琐。考虑用映射(Map)作为统一的数据结构。但是Java语言的Map构造代码写起来有点繁琐,因此在Interaction.java中定义了一套方便构造交互数据的类和工具方法。
InteractionTest.java
import junit.framework.TestCase;
import java.util.Map;
import fayelab.tdd.stringtransformer.instruction.original.Entry.Key;
import static java.util.Arrays.asList;
import static fayelab.tdd.stringtransformer.instruction.original.Trans.*;
import static fayelab.tdd.stringtransformer.instruction.original.Entry.*;
import static fayelab.tdd.stringtransformer.instruction.original.Entry.Key.*;
import static fayelab.tdd.stringtransformer.instruction.original.Interaction.*;
public class InteractionTest extends TestCase
{
private Map<Key, Value<?>> data;
@Override
protected void setUp()
{
data = mockData();
}
public void test_equals()
{
assertEquals(mockData(), data);
}
public void test_toString()
{
assertEquals("{AVAIL_TRANSES=[Upper, Lower, TrimPrefixSpaces], AVAIL_SELECTED_INDEX=0}", data.toString());
}
public void test_toStrArray()
{
String[] actual = new Value<>(asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS)).toStrArray();
assertEquals(asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS), asList(actual));
}
public void test_toInt()
{
assertEquals(0, new Value<>(0).toInt());
}
private Map<Key, Value<?>> mockData()
{
return interactionData(
entry(AVAIL_TRANSES, asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS)),
entry(AVAIL_SELECTED_INDEX, 0));
}
}
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";
}
Interaction.java Interaction Class
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Stream;
import fayelab.tdd.stringtransformer.instruction.original.Entry.Key;
public class Interaction
{
static Map<Key, Value<?>> interactionData(Entry<?>...entries)
{
return Stream.of(entries)
.collect(LinkedHashMap::new,
(resultMap, entry) -> resultMap.put(entry.getKey(), new Value<>(entry.getValue())),
(map1, map2) -> map1.putAll(map2));
}
}
Interaction.java Entry Class
class Entry<V>
{
enum Key
{
AVAIL_TRANSES, AVAIL_SELECTED_INDEX
}
private Key key;
private V value;
static <V> Entry<V> entry(Key key, V value)
{
return new Entry<>(key, value);
}
private Entry(Key key, V value)
{
this.key = key;
this.value = value;
}
public Key getKey()
{
return key;
}
public V getValue()
{
return value;
}
}
Interaction.java Value Class
import java.util.List;
import java.util.Objects;
class Value<T>
{
private T value;
Value(T value)
{
this.value = value;
}
public T get()
{
return value;
}
@Override
public String toString()
{
return Objects.toString(value);
}
@Override
public boolean equals(Object obj)
{
if(this == obj)
{
return true;
}
if(obj == null)
{
return false;
}
if(getClass() != obj.getClass())
{
return false;
}
return Objects.equals(value, ((Value<?>)obj).value);
}
@Override
public int hashCode()
{
return 31 + Objects.hashCode(value);
}
@SuppressWarnings("unchecked")
String[] toStrArray()
{
return ((List<String>)value).toArray(new String[] {});
}
int toInt()
{
return ((Integer)value).intValue();
}
}
由BusinessLogic提供的所有转换器经由Presenter推送给View显示,可用转换器列表选中索引为0的转换器。
PresenterTest.java
import junit.framework.TestCase;
import static java.util.Arrays.asList;
import static fayelab.tdd.stringtransformer.instruction.original.Interaction.*;
import static fayelab.tdd.stringtransformer.instruction.original.Entry.*;
import static fayelab.tdd.stringtransformer.instruction.original.Entry.Key.*;
import static fayelab.tdd.stringtransformer.instruction.original.Trans.*;
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(interactionData(
entry(AVAIL_TRANSES, asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS)),
entry(AVAIL_SELECTED_INDEX, 0)), viewStub.getOnInitData());
}
}
在View这一层验证View拿到的OnInitData是否符合预期。OnInitData包括可用转换器列表和可用转换器列表选中转换器的索引。
1 新增ViewStub类。
ViewStub.java
import java.util.Map;
import fayelab.tdd.stringtransformer.instruction.original.Entry.Key;
public class ViewStub implements View
{
public Map<Key, Value<?>> getOnInitData()
{
return null;
}
}
2 ViewStub实现View接口,新增View接口类。
View.java
public interface View
{
}
3 新增BusinessLogicStub类。
BusinessLogicStub.java
public class BusinessLogicStub implements BusinessLogic
{
}
4 BusinessLogicStub实现BusinessLogic接口,新增BusinessLogic接口类。
BusinessLogic.java
public interface BusinessLogic
{
}
5 新增Presenter类。
Presenter.java
import static fayelab.tdd.stringtransformer.instruction.original.Interaction.*;
import static fayelab.tdd.stringtransformer.instruction.original.Entry.*;
import static fayelab.tdd.stringtransformer.instruction.original.Entry.Key.*;
public class Presenter
{
private View view;
private BusinessLogic businessLogic;
public Presenter(View view, BusinessLogic businessLogic)
{
this.view = view;
this.businessLogic = businessLogic;
}
public void init()
{
view.onInit(interactionData(
entry(AVAIL_TRANSES, businessLogic.getAllTranses()),
entry(AVAIL_SELECTED_INDEX, 0)));
}
}
在Presenter.init()方法中,将从businessLogic获得的所有转换器和可用转换器列表选中转换器的索引封装为一个映射类型的数据,通过调用View.onInit()方法传递给View。
6 View接口新增onInit()方法。
View.java
import java.util.Map;
import fayelab.tdd.stringtransformer.instruction.original.Entry.Key;
void onInit(Map<Key, Value<?>> data);
7 ViewStub类实现View新增方法。
ViewStub.java
private Map<Key, Value<?>> onInitData;
@Override
public void onInit(Map<Key, Value<?>> data)
{
onInitData = data;
}
public Map<Key, Value<?>> getOnInitData()
{
return onInitData;
}
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;
import static fayelab.tdd.stringtransformer.instruction.original.Trans.*;
@Override
public List<String> getAllTranses()
{
return asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS);
}
BusinessLogicImpl提供所有的转换器。
BusinessLogicImplTest.java
import junit.framework.TestCase;
import static java.util.Arrays.asList;
import static fayelab.tdd.stringtransformer.instruction.original.Trans.*;
public class BusinessLogicImplTest extends TestCase
{
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());
}
}
新增BusinessLogicImpl类,实现BusinessLogic接口,实现BusinessLogic新增方法。
BusinessLogicImpl.java
import java.util.List;
import static java.util.Arrays.asList;
import static fayelab.tdd.stringtransformer.instruction.original.Trans.*;
public class BusinessLogicImpl implements BusinessLogic
{
@Override
public List<String> getAllTranses()
{
return asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS);
}
}
AllTests.java
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(InteractionTest.class);
suite.addTestSuite(PresenterTest.class);
//$JUnit-END$
return suite;
}
}
ViewImpl实现View接口,实现View新增方法。
ViewImpl.java
import java.util.Map;
import fayelab.tdd.stringtransformer.instruction.original.Entry.Key;
import static fayelab.tdd.stringtransformer.instruction.original.Entry.Key.*;
public class ViewImpl extends JFrame implements View
{
...
@Override
public void onInit(Map<Key, Value<?>> data)
{
lstAvail.setListData(data.get(AVAIL_TRANSES).toStrArray());
lstAvail.setSelectedIndex(data.get(AVAIL_SELECTED_INDEX).toInt());
}
...
}
在ViewImpl.main()里构造产品环境中的presenter,并调用presenter.init()方法完成初始化。
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);
}
构建转换器链包括:添加转换器、移除转换器和移除所有转换器。
构建转换器链的代码主要集中在Presenter和View。
在可用转换器列表中选中不是最后的一个转换器,点击Add按钮,选中的转换器被添加到转换器链列表末尾,转换器链列表选中新添加的转换器。可用转换器列表中的转换器条目不变,选中之前选中的转换器的下一个转换器。
PresenterTest.java
public void test_add_not_the_last_trans()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, UPPER_TRANS)));
presenter.addTrans();
assertEquals(interactionData(
entry(CHAIN_TRANSES, asList(UPPER_TRANS)),
entry(CHAIN_SELECTED_INDEX, 0),
entry(AVAIL_SELECTED_INDEX, 1)), viewStub.getOnAddTransData());
}
在View这一层验证View拿到的OnAddTransData是否符合预期。OnAddTransData包括转换器链列表、转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引。
1 定义新的Entry.Key:AVAIL_SELECTED_TRANS、CHAIN_TRANSES和CHAIN_SELECTED_INDEX。
Interaction.java Entry Class
enum Key
{
AVAIL_TRANSES, AVAIL_SELECTED_INDEX, AVAIL_SELECTED_TRANS,
CHAIN_TRANSES, CHAIN_SELECTED_INDEX
}
2 ViewStub
ViewStub.java
public void setAddTransData(Map<Key, Value<?>> data)
{
}
public Map<Key, Value<?>> getOnAddTransData()
{
return null;
}
3 Presenter
Presenter.java
import java.util.ArrayList;
import java.util.List;
private int availSelectedIndex = 0;
private int chainSelectedIndex = NONE_SELECTED_INDEX;
private List<String> availTranses;
private List<String> chainTranses = new ArrayList<>();
public void init()
{
availTranses = businessLogic.getAllTranses();
view.onInit(interactionData(
entry(AVAIL_TRANSES, availTranses),
entry(AVAIL_SELECTED_INDEX, availSelectedIndex)));
}
public void addTrans()
{
String availSelectedTrans = view.collectAddTransData().get(AVAIL_SELECTED_TRANS).toStr();
chainTranses.add(availSelectedTrans);
updateChainSelectedIndexForAdd(availSelectedTrans);
updateAvailSelectedIndexForAdd(availSelectedTrans);
view.onAddTrans(interactionData(
entry(CHAIN_TRANSES, chainTranses),
entry(CHAIN_SELECTED_INDEX, chainSelectedIndex),
entry(AVAIL_SELECTED_INDEX, availSelectedIndex)));
}
private void updateChainSelectedIndexForAdd(String availSelectedTrans)
{
chainSelectedIndex = chainTranses.indexOf(availSelectedTrans);
}
private void updateAvailSelectedIndexForAdd(String availSelectedTrans)
{
availSelectedIndex = availTranses.indexOf(availSelectedTrans) + 1;
}
在Presenter.addTrans()方法中,调用View.collectAddTransData()方法从View获取addTrans所需数据,这个映射类型的数据包括选中的可用转换器。将该转换器添加到chainTranses末尾。在Presenter.updateChainSelectedIndexForAdd()方法中计算转换器链列表选中转换器的索引并更新chainSelectedIndex。在Presenter.updateAvailSelectedIndexForAdd()方法中计算可用转换器列表选中转换器的索引并更新availSelectedIndex。将转换器链列表、转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引封装为一个映射类型的数据,通过调用View.onAddTrans()方法传递给View。
4 在interaction类中定义NONE_SELECTED_INDEX常量,并在Value类中定义toStr()方法。
Interaction.java Interaction Class
static final int NONE_SELECTED_INDEX = -1;
-1表示无选中索引。
InteractionTest.java
public void test_toStr()
{
assertEquals(UPPER_TRANS, new Value<>(UPPER_TRANS).toStr());
}
Interaction.java Value Class
String toStr()
{
return (String)value;
}
5 View
View.java
Map<Key, Value<?>> collectAddTransData();
void onAddTrans(Map<Key, Value<?>> data);
6 ViewStub
ViewStub.java
private Map<Key, Value<?>> addTransData;
private Map<Key, Value<?>> onAddTransData;
@Override
public Map<Key, Value<?>> collectAddTransData()
{
return addTransData;
}
public void setAddTransData(Map<Key, Value<?>> data)
{
addTransData = data;
}
@Override
public void onAddTrans(Map<Key, Value<?>> data)
{
onAddTransData = data;
}
public Map<Key, Value<?>> getOnAddTransData()
{
return onAddTransData;
}
ViewImpl.java
import static fayelab.tdd.stringtransformer.instruction.original.Interaction.*;
import static fayelab.tdd.stringtransformer.instruction.original.Entry.*;
@Override
public Map<Key, Value<?>> collectAddTransData()
{
return interactionData(entry(AVAIL_SELECTED_TRANS, lstAvail.getSelectedValue()));
}
@Override
public void onAddTrans(Map<Key, Value<?>> data)
{
lstChain.setListData(data.get(CHAIN_TRANSES).toStrArray());
lstChain.setSelectedIndex(data.get(CHAIN_SELECTED_INDEX).toInt());
lstAvail.setSelectedIndex(data.get(AVAIL_SELECTED_INDEX).toInt());
}
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;
view.setPresenter(this);
}
在可用转换器列表中选中最后一个转换器,点击Add按钮,选中的转换器被添加到转换器链列表末尾,转换器链列表选中新添加的转换器。可用转换器列表中的转换器条目不变,选中第一个转换器。
PresenterTest.java
public void test_add_the_last_trans()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, TRIM_PREFIX_SPACES_TRANS)));
presenter.addTrans();
assertEquals(interactionData(
entry(CHAIN_TRANSES, asList(TRIM_PREFIX_SPACES_TRANS)),
entry(CHAIN_SELECTED_INDEX, 0),
entry(AVAIL_SELECTED_INDEX, 0)), viewStub.getOnAddTransData());
}
Presenter.java
private void updateAvailSelectedIndexForAdd(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;
}
在可用转换器列表中选中一个在转换器链列表中已存在的转换器,点击Add按钮,提示“待添加的转换器在转换器链中已存在”。转换器链列表中的转换器条目不变,选中已存在的转换器。可用转换器列表中的转换器条目不变,选中之前选中的转换器。
PresenterTest.java
import fayelab.tdd.stringtransformer.instruction.original.ValidatingResult.FailedReason;
public void test_add_already_existed_in_chain_trans()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, UPPER_TRANS)));
presenter.addTrans();
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, LOWER_TRANS)));
presenter.addTrans();
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, LOWER_TRANS)));
presenter.addTrans();
assertEquals(interactionData(
entry(VALIDATING_FAILED_REASON, FailedReason.ADD_ALREADY_EXISTED_IN_CHAIN_TRANS)),
viewStub.getOnValidatingFailedData());
assertEquals(interactionData(
entry(CHAIN_TRANSES, asList(UPPER_TRANS, LOWER_TRANS)),
entry(CHAIN_SELECTED_INDEX, 1),
entry(AVAIL_SELECTED_INDEX, 1)), viewStub.getOnAddTransData());
}
在验证View拿到的OnAddTransData是否符合预期之前,先验证View是否收到参数校验失败原因为ADD_ALREADY_EXISTED_IN_CHAIN_TRANS的通知。
关于参数校验,整体上依然采用之前的设计,但由于定义统一的参数校验失败通知方法,细节跟之前略有不同。
1 定义新的Entry.Key:VALIDATING_FAILED_REASON。
Interaction.java Entry Class
enum Key
{
AVAIL_TRANSES, AVAIL_SELECTED_INDEX, AVAIL_SELECTED_TRANS,
CHAIN_TRANSES, CHAIN_SELECTED_INDEX,
VALIDATING_FAILED_REASON
}
2 ValidatingResult
Validator.java ValidatingResult Class
class ValidatingResult
{
enum FailedReason
{
NONE,
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);
}
}
跟之前的ValidatingResult没有区别。
3 ParamValidatingRule
Validator.java ParamValidatingRule Class
import java.util.function.Predicate;
import fayelab.tdd.stringtransformer.instruction.original.ValidatingResult.FailedReason;
class ParamValidatingRule<T>
{
private T param;
private Predicate<T> failedPred;
private FailedReason failedReason;
static <T> ParamValidatingRule<T> paramValidatingRule(T param, Predicate<T> failedPred, FailedReason failedReason)
{
return new ParamValidatingRule<>(param, failedPred, failedReason);
}
private ParamValidatingRule(T param, Predicate<T> failedPred, FailedReason failedReason)
{
this.param = param;
this.failedPred = failedPred;
this.failedReason = failedReason;
}
public T getParam()
{
return param;
}
public Predicate<T> getFailedPred()
{
return failedPred;
}
public FailedReason getFailedReason()
{
return failedReason;
}
}
跟之前的ParamValidatingRule相比,少了failedAction。由于定义统一的参数校验失败通知方法,所以不再需要各自的failedAction。
4 Validator
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()))
{
return ValidatingResult.failedResult(rule.getFailedReason());
}
return ValidatingResult.succeededResult();
}
}
跟之前的Validator相比,在Validator.validateParam()方法中,如果failedPred为真,不再调用failedAction。也是因为定义统一的参数校验失败通知方法。
5 ViewStub
ViewStub.java
public Map<Key, Value<?>> getOnValidatingFailedData()
{
return null;
}
6 Presenter
Presenter.java
import fayelab.tdd.stringtransformer.instruction.original.ValidatingResult.FailedReason;
import static java.util.Arrays.asList;
import static fayelab.tdd.stringtransformer.instruction.original.ParamValidatingRule.*;
public void addTrans()
{
String availSelectedTrans = view.collectAddTransData().get(AVAIL_SELECTED_TRANS).toStr();
ValidatingResult validatingResult = validate(buildParamValidatingRulesForAdd(availSelectedTrans));
if(validatingResult.isSucceeded())
{
chainTranses.add(availSelectedTrans);
}
updateChainSelectedIndexForAdd(availSelectedTrans);
updateAvailSelectedIndexForAdd(availSelectedTrans, validatingResult.getFailedReason());
view.onAddTrans(interactionData(
entry(CHAIN_TRANSES, chainTranses),
entry(CHAIN_SELECTED_INDEX, chainSelectedIndex),
entry(AVAIL_SELECTED_INDEX, availSelectedIndex)));
}
private List<ParamValidatingRule<?>> buildParamValidatingRulesForAdd(String availSelectedTrans)
{
return asList(
paramValidatingRule(availSelectedTrans, this::alreadyExistedInChain,
FailedReason.ADD_ALREADY_EXISTED_IN_CHAIN_TRANS));
}
private void updateAvailSelectedIndexForAdd(String availSelectedTrans, FailedReason failedReason)
{
int selectedIndex = availTranses.indexOf(availSelectedTrans);
if(failedReason == FailedReason.ADD_ALREADY_EXISTED_IN_CHAIN_TRANS)
{
availSelectedIndex = selectedIndex;
}
else
{
availSelectedIndex = isLastIndex(selectedIndex, availTranses) ? 0: selectedIndex + 1;
}
}
private ValidatingResult validate(List<ParamValidatingRule<?>> rules)
{
ValidatingResult validatingResult = Validator.validate(rules);
if(!validatingResult.isSucceeded())
{
view.onValidatingFailed(interactionData(
entry(VALIDATING_FAILED_REASON, validatingResult.getFailedReason())));
}
return validatingResult;
}
private boolean alreadyExistedInChain(String trans)
{
return chainTranses.contains(trans);
}
- Presenter.validate()方法根据ParamValidatingRules进行参数校验。之所以在Presenter中再定义一个validate方法,而不是直接修改Validator.validate()方法,是因为不想让Validator和View打交道,增加不必要的耦合。
- View.onValidatingFailed()方法是统一的参数校验失败通知方法,当有参数校验失败时,通过调用这个方法将参数校验失败原因传递给View。
7 ViewStub
ViewStub.java
private Map<Key, Value<?>> onValidatingFailedData;
@Override
public void onValidatingFailed(Map<Key, Value<?>> data)
{
onValidatingFailedData = data;
}
public Map<Key, Value<?>> getOnValidatingFailedData()
{
return onValidatingFailedData;
}
ViewImpl.java
import javax.swing.JOptionPane;
import java.util.HashMap;
import fayelab.tdd.stringtransformer.instruction.original.ValidatingResult.FailedReason;
private static Map<FailedReason, String> VALIDATING_FAILED_REASON_AND_TIP_MAP = null;
static
{
VALIDATING_FAILED_REASON_AND_TIP_MAP = new HashMap<>();
VALIDATING_FAILED_REASON_AND_TIP_MAP.put(FailedReason.ADD_ALREADY_EXISTED_IN_CHAIN_TRANS,
"The transformer to be added has been already existed in the chain.");
}
@Override
public void onValidatingFailed(Map<Key, Value<?>> data)
{
JOptionPane.showMessageDialog(this,
VALIDATING_FAILED_REASON_AND_TIP_MAP.get(data.get(VALIDATING_FAILED_REASON).get()));
}
在ViewImpl类中定义一个参数校验失败原因与提示信息的映射。
3.2.1.4 Abnormal Business Process 2: Add a transformer but none of the available transformers is specified
在可用转换器列表中未选中任何转换器,点击Add按钮,提示“请在可用转换器中指定一个转换器”。转换器链列表中的转换器条目不变,选中之前已经选中的转换器。可用转换器列表中的转换器条目不变,选中第一个转换器。
PresenterTest.java
public void test_add_trans_but_avail_trans_not_specified()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, UPPER_TRANS)));
presenter.addTrans();
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, LOWER_TRANS)));
presenter.addTrans();
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, NONE_SELECTED_TRANS)));
presenter.addTrans();
assertEquals(interactionData(
entry(VALIDATING_FAILED_REASON, FailedReason.AVAIL_TRANS_NOT_SPECIFIED)),
viewStub.getOnValidatingFailedData());
assertEquals(interactionData(
entry(CHAIN_TRANSES, asList(UPPER_TRANS, LOWER_TRANS)),
entry(CHAIN_SELECTED_INDEX, 1),
entry(AVAIL_SELECTED_INDEX, 0)), viewStub.getOnAddTransData());
}
1 在Interaction类中定义NONE_SELECTED_TRANS常量。
Interaction.java Interaction Class
static final String NONE_SELECTED_TRANS = null;
2 ValidatingResult.FailedReason
Validator.java ValidatingResult Class
enum FailedReason
{
NONE,
AVAIL_TRANS_NOT_SPECIFIED,
ADD_ALREADY_EXISTED_IN_CHAIN_TRANS
}
3 Presenter
Presenter.java
public void addTrans()
{
String availSelectedTrans = view.collectAddTransData().get(AVAIL_SELECTED_TRANS).toStr();
ValidatingResult validatingResult = validate(buildParamValidatingRulesForAdd(availSelectedTrans));
if(validatingResult.isSucceeded())
{
chainTranses.add(availSelectedTrans);
}
updateChainSelectedIndexForAdd(availSelectedTrans, validatingResult.getFailedReason());
updateAvailSelectedIndexForAdd(availSelectedTrans, validatingResult.getFailedReason());
view.onAddTrans(interactionData(
entry(CHAIN_TRANSES, chainTranses),
entry(CHAIN_SELECTED_INDEX, chainSelectedIndex),
entry(AVAIL_SELECTED_INDEX, availSelectedIndex)));
}
private List<ParamValidatingRule<?>> buildParamValidatingRulesForAdd(String availSelectedTrans)
{
return asList(
paramValidatingRule(availSelectedTrans, Presenter::transNotSpecified,
FailedReason.AVAIL_TRANS_NOT_SPECIFIED),
paramValidatingRule(availSelectedTrans, this::alreadyExistedInChain,
FailedReason.ADD_ALREADY_EXISTED_IN_CHAIN_TRANS));
}
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;
}
}
private static boolean transNotSpecified(String trans)
{
return trans == NONE_SELECTED_TRANS;
}
ViewImpl.java
static
{
VALIDATING_FAILED_REASON_AND_TIP_MAP = new HashMap<>();
VALIDATING_FAILED_REASON_AND_TIP_MAP.put(FailedReason.AVAIL_TRANS_NOT_SPECIFIED,
"Specify an available transformer, please.");
VALIDATING_FAILED_REASON_AND_TIP_MAP.put(FailedReason.ADD_ALREADY_EXISTED_IN_CHAIN_TRANS,
"The transformer to be added has been already existed in the chain.");
}
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按钮,选中的转换器被移除出转换器链。可用转换器列表中的转换器条目不变,选中被移除的转换器。转换器链列表选中之前选中的转换器的下一个转换器。
PresenterTest.java
public void test_remove_not_the_last_trans_when_chain_has_more_than_one_transes()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, UPPER_TRANS)));
presenter.addTrans();
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, LOWER_TRANS)));
presenter.addTrans();
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, TRIM_PREFIX_SPACES_TRANS)));
presenter.addTrans();
viewStub.setRemoveTransData(interactionData(entry(CHAIN_SELECTED_TRANS, LOWER_TRANS)));
presenter.removeTrans();
assertEquals(interactionData(
entry(CHAIN_TRANSES, asList(UPPER_TRANS, TRIM_PREFIX_SPACES_TRANS)),
entry(AVAIL_SELECTED_INDEX, 1),
entry(CHAIN_SELECTED_INDEX, 1)), viewStub.getOnRemoveTransData());
}
1 定义新的Entry.Key:CHAIN_SELECTED_TRANS。
Interaction.java Entry Class
enum Key
{
AVAIL_TRANSES, AVAIL_SELECTED_INDEX, AVAIL_SELECTED_TRANS,
CHAIN_TRANSES, CHAIN_SELECTED_INDEX, CHAIN_SELECTED_TRANS,
VALIDATING_FAILED_REASON
}
2 ViewStub
ViewStub.java
public void setRemoveTransData(Map<Key, Value<?>> data)
{
}
public Map<Key, Value<?>> getOnRemoveTransData()
{
return null;
}
3 Presenter
Presenter.java
public void removeTrans()
{
String chainSelectedTrans = view.collectRemoveTransData().get(CHAIN_SELECTED_TRANS).toStr();
updateChainSelectedIndexForRemove(chainSelectedTrans);
chainTranses.remove(chainSelectedTrans);
updateAvailSelectedIndexForRemove(chainSelectedTrans);
view.onRemoveTrans(interactionData(
entry(CHAIN_TRANSES, chainTranses),
entry(AVAIL_SELECTED_INDEX, availSelectedIndex),
entry(CHAIN_SELECTED_INDEX, chainSelectedIndex)));
}
private void updateChainSelectedIndexForRemove(String chainSelectedTrans)
{
chainSelectedIndex = chainTranses.indexOf(chainSelectedTrans);
}
private void updateAvailSelectedIndexForRemove(String chainSelectedTrans)
{
availSelectedIndex = availTranses.indexOf(chainSelectedTrans);
}
4 View
View.java
Map<Key, Value<?>> collectRemoveTransData();
void onRemoveTrans(Map<Key, Value<?>> data);
5 ViewStub
ViewStub.java
private Map<Key, Value<?>> removeTransData;
private Map<Key, Value<?>> onRemoveTransData;
@Override
public Map<Key, Value<?>> collectRemoveTransData()
{
return removeTransData;
}
public void setRemoveTransData(Map<Key, Value<?>> data)
{
removeTransData = data;
}
@Override
public void onRemoveTrans(Map<Key, Value<?>> data)
{
onRemoveTransData = data;
}
public Map<Key, Value<?>> getOnRemoveTransData()
{
return onRemoveTransData;
}
ViewImpl.java
@Override
public Map<Key, Value<?>> collectRemoveTransData()
{
return interactionData(entry(CHAIN_SELECTED_TRANS, lstChain.getSelectedValue()));
}
@Override
public void onRemoveTrans(Map<Key, Value<?>> data)
{
lstChain.setListData(data.get(CHAIN_TRANSES).toStrArray());
lstAvail.setSelectedIndex(data.get(AVAIL_SELECTED_INDEX).toInt());
lstChain.setSelectedIndex(data.get(CHAIN_SELECTED_INDEX).toInt());
}
private void btnRemove_actionPerformed(ActionEvent e)
{
presenter.removeTrans();
}
3.2.2.2 Normal Business Process 2: Remove the transformer which is the last when the chain has more than one transformers
当转换器链列表包含多于一个的转换器时,在转换器链列表中选中最后一个转换器,点击Remove按钮,选中的转换器被移除出转换器链。可用转换器列表中的转换器条目不变,选中被移除的转换器。转换器链列表选中第一个转换器。
PresenterTest.java
public void test_remove_the_last_trans_when_chain_has_more_than_one_transes()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, UPPER_TRANS)));
presenter.addTrans();
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, LOWER_TRANS)));
presenter.addTrans();
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, TRIM_PREFIX_SPACES_TRANS)));
presenter.addTrans();
viewStub.setRemoveTransData(interactionData(entry(CHAIN_SELECTED_TRANS, TRIM_PREFIX_SPACES_TRANS)));
presenter.removeTrans();
assertEquals(interactionData(
entry(CHAIN_TRANSES, asList(UPPER_TRANS, LOWER_TRANS)),
entry(AVAIL_SELECTED_INDEX, 2),
entry(CHAIN_SELECTED_INDEX, 0)), viewStub.getOnRemoveTransData());
}
Presenter.java
private void updateChainSelectedIndexForRemove(String chainSelectedTrans)
{
int selectedIndex = chainTranses.indexOf(chainSelectedTrans);
chainSelectedIndex = isLastIndex(selectedIndex, chainTranses) ? 0 : selectedIndex;
}
当转换器链列表仅包含一个转换器时,在转换器链列表中选中这个转换器,点击Remove按钮,选中的转换器被移除出转换器链,转换器链列表为空,无选中项。可用转换器列表中的转换器条目不变,选中被移除的转换器。
PresenterTest.java
public void test_remove_a_trans_when_chain_has_only_one_transes()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, UPPER_TRANS)));
presenter.addTrans();
viewStub.setRemoveTransData(interactionData(entry(CHAIN_SELECTED_TRANS, UPPER_TRANS)));
presenter.removeTrans();
assertEquals(interactionData(
entry(CHAIN_TRANSES, asList()),
entry(AVAIL_SELECTED_INDEX, 0),
entry(CHAIN_SELECTED_INDEX, NONE_SELECTED_INDEX)), viewStub.getOnRemoveTransData());
}
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;
}
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按钮,提示“请在转换器链中指定一个转换器”。可用转换器列表中的转换器条目不变,选中之前已经选中的转换器。转换器链列表中的转换器条目不变,选中第一个转换器。
PresenterTest.java
public void test_remove_trans_when_chain_is_not_empty_but_chain_trans_not_specified()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, UPPER_TRANS)));
presenter.addTrans();
viewStub.setRemoveTransData(interactionData(entry(CHAIN_SELECTED_TRANS, NONE_SELECTED_TRANS)));
presenter.removeTrans();
assertEquals(interactionData(
entry(VALIDATING_FAILED_REASON, FailedReason.CHAIN_TRANS_NOT_SPECIFIED)),
viewStub.getOnValidatingFailedData());
assertEquals(interactionData(
entry(CHAIN_TRANSES, asList(UPPER_TRANS)),
entry(AVAIL_SELECTED_INDEX, 1),
entry(CHAIN_SELECTED_INDEX, 0)), viewStub.getOnRemoveTransData());
}
1 ValidatingResult.FailedReason
Validator.java ValidatingResult Class
enum FailedReason
{
...
CHAIN_TRANS_NOT_SPECIFIED
}
2 Presenter
Presenter.java
public void removeTrans()
{
String chainSelectedTrans = view.collectRemoveTransData().get(CHAIN_SELECTED_TRANS).toStr();
ValidatingResult validatingResult = validate(buildParamValidatingRulesForRemove(chainSelectedTrans));
updateChainSelectedIndexForRemove(chainSelectedTrans, validatingResult.getFailedReason());
if(validatingResult.isSucceeded())
{
chainTranses.remove(chainSelectedTrans);
}
updateAvailSelectedIndexForRemove(chainSelectedTrans, validatingResult.getFailedReason());
view.onRemoveTrans(interactionData(
entry(CHAIN_TRANSES, chainTranses),
entry(AVAIL_SELECTED_INDEX, availSelectedIndex),
entry(CHAIN_SELECTED_INDEX, chainSelectedIndex)));
}
private List<ParamValidatingRule<?>> buildParamValidatingRulesForRemove(String chainSelectedTrans)
{
return asList(
paramValidatingRule(chainSelectedTrans, Presenter::transNotSpecified,
FailedReason.CHAIN_TRANS_NOT_SPECIFIED));
}
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);
}
ViewImpl.java
static
{
...
VALIDATING_FAILED_REASON_AND_TIP_MAP.put(FailedReason.CHAIN_TRANS_NOT_SPECIFIED,
"Specify a transformer from the chain, please.");
}
转换器链列表为空,无选中项,点击Remove按钮,提示“转换器链为空”。可用转换器列表中的转换器条目不变,选中第一个转换器。
PresenterTest.java
public void test_remove_trans_when_chain_is_empty()
{
presenter.removeTrans();
assertEquals(interactionData(
entry(VALIDATING_FAILED_REASON, FailedReason.CHAIN_EMPTY)),
viewStub.getOnValidatingFailedData());
assertEquals(interactionData(
entry(CHAIN_TRANSES, asList()),
entry(AVAIL_SELECTED_INDEX, 0),
entry(CHAIN_SELECTED_INDEX, NONE_SELECTED_INDEX)), viewStub.getOnRemoveTransData());
}
1 ValidatingResult.FailedReason
Validator.java ValidatingResult Class
enum FailedReason
{
...
CHAIN_EMPTY
}
2 Presenter
Presenter.java
import java.util.Map;
import fayelab.tdd.stringtransformer.instruction.original.Entry.Key;
public void removeTrans()
{
Map<Key, Value<?>> removeTransData = view.collectRemoveTransData();
String chainSelectedTrans = removeTransData != null ? removeTransData.get(CHAIN_SELECTED_TRANS).toStr() : null;
ValidatingResult validatingResult = validate(buildParamValidatingRulesForRemove(chainSelectedTrans));
updateChainSelectedIndexForRemove(chainSelectedTrans, validatingResult.getFailedReason());
if(validatingResult.isSucceeded())
{
chainTranses.remove(chainSelectedTrans);
}
updateAvailSelectedIndexForRemove(chainSelectedTrans, validatingResult.getFailedReason());
view.onRemoveTrans(interactionData(
entry(CHAIN_TRANSES, chainTranses),
entry(AVAIL_SELECTED_INDEX, availSelectedIndex),
entry(CHAIN_SELECTED_INDEX, chainSelectedIndex)));
}
private List<ParamValidatingRule<?>> buildParamValidatingRulesForRemove(String chainSelectedTrans)
{
return asList(
paramValidatingRule(chainTranses, Presenter::emptyList,
FailedReason.CHAIN_EMPTY),
paramValidatingRule(chainSelectedTrans, Presenter::transNotSpecified,
FailedReason.CHAIN_TRANS_NOT_SPECIFIED));
}
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();
}
ViewImpl.java
static
{
...
VALIDATING_FAILED_REASON_AND_TIP_MAP.put(FailedReason.CHAIN_EMPTY,
"Specify the transformer chain, please.");
}
转换器链列表不为空,无论转换器链列表是否选中转换器,点击Remove All按钮,所有转换器均被移除出转换器链,转换器链列表为空,无选中项。可用转换器列表中的转换器条目不变,选中第一个转换器。
PresenterTest.java
public void test_remove_all_transes_when_chain_is_not_empty()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, UPPER_TRANS)));
presenter.addTrans();
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, LOWER_TRANS)));
presenter.addTrans();
presenter.removeAllTranses();
assertEquals(interactionData(
entry(CHAIN_TRANSES, asList()),
entry(AVAIL_SELECTED_INDEX, 0),
entry(CHAIN_SELECTED_INDEX, NONE_SELECTED_INDEX)), viewStub.getOnRemoveAllTransesData());
}
1 ViewStub
ViewStub.java
public Map<Key, Value<?>> getOnRemoveAllTransesData()
{
return null;
}
2 Presenter
Presenter.java
public void removeAllTranses()
{
chainTranses.clear();
updateChainSelectedIndexForRemoveAll();
updateAvailSelectedIndexForRemoveAll();
view.onRemoveAllTranses(interactionData(
entry(CHAIN_TRANSES, chainTranses),
entry(CHAIN_SELECTED_INDEX, chainSelectedIndex),
entry(AVAIL_SELECTED_INDEX, availSelectedIndex)));
}
private void updateChainSelectedIndexForRemoveAll()
{
chainSelectedIndex = NONE_SELECTED_INDEX;
}
private void updateAvailSelectedIndexForRemoveAll()
{
availSelectedIndex = 0;
}
3 View
View.java
void onRemoveAllTranses(Map<Key, Value<?>> data);
4 ViewStub
ViewStub.java
private Map<Key, Value<?>> onRemoveAllTransesData;
@Override
public void onRemoveAllTranses(Map<Key, Value<?>> data)
{
onRemoveAllTransesData = data;
}
public Map<Key, Value<?>> getOnRemoveAllTransesData()
{
return onRemoveAllTransesData;
}
ViewImpl.java
@Override
public void onRemoveAllTranses(Map<Key, Value<?>> data)
{
lstChain.setListData(data.get(CHAIN_TRANSES).toStrArray());
lstChain.setSelectedIndex(data.get(CHAIN_SELECTED_INDEX).toInt());
lstAvail.setSelectedIndex(data.get(AVAIL_SELECTED_INDEX).toInt());
}
private void btnRemoveAll_actionPerformed(ActionEvent e)
{
presenter.removeAllTranses();
}
转换器链列表为空,无选中项,点击Remove All按钮,提示“请指定转换器链”。可用转换器列表中的转换器条目不变,选中之前选中的转换器。
PresenterTest.java
public void test_remove_all_transes_when_chain_is_empty()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, LOWER_TRANS)));
presenter.addTrans();
viewStub.setRemoveTransData(interactionData(entry(CHAIN_SELECTED_TRANS, LOWER_TRANS)));
presenter.removeTrans();
presenter.removeAllTranses();
assertEquals(interactionData(
entry(VALIDATING_FAILED_REASON, FailedReason.CHAIN_EMPTY)),
viewStub.getOnValidatingFailedData());
assertEquals(interactionData(
entry(CHAIN_TRANSES, asList()),
entry(AVAIL_SELECTED_INDEX, 1),
entry(CHAIN_SELECTED_INDEX, NONE_SELECTED_INDEX)), viewStub.getOnRemoveAllTransesData());
}
Presenter.java
public void removeAllTranses()
{
ValidatingResult validatingResult = validate(buildParamValidatingRulesForRemoveAll());
if(validatingResult.isSucceeded())
{
chainTranses.clear();
}
updateChainSelectedIndexForRemoveAll();
updateAvailSelectedIndexForRemoveAll(validatingResult.getFailedReason());
view.onRemoveAllTranses(interactionData(
entry(CHAIN_TRANSES, chainTranses),
entry(CHAIN_SELECTED_INDEX, chainSelectedIndex),
entry(AVAIL_SELECTED_INDEX, availSelectedIndex)));
}
private List<ParamValidatingRule<?>> buildParamValidatingRulesForRemoveAll()
{
return asList(paramValidatingRule(chainTranses, Presenter::emptyList, FailedReason.CHAIN_EMPTY));
}
private void updateAvailSelectedIndexForRemoveAll(FailedReason failedReason)
{
if(failedReason == FailedReason.CHAIN_EMPTY)
{
return;
}
availSelectedIndex = 0;
}
输入合法的源字符串,构建好非空的转换器链,点击Apply按钮,将转换器链中的转换器从上到下依次应用到源字符串上,得到最终的结果字符串。
PresenterTest.java
public void test_apply_trans_chain()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, UPPER_TRANS)));
presenter.addTrans();
viewStub.setApplyTransChainData(interactionData(entry(SOURCE_STR, "Hello, world.")));
presenter.applyTransChain();
assertEquals(interactionData(entry(RESULT_STR, "HELLO, WORLD.")), viewStub.getOnApplyTransChainData());
}
1 定义新的Entry.Key:SOURCE_STR和RESULT_STR。
Interaction.java Entry Class
enum Key
{
AVAIL_TRANSES, AVAIL_SELECTED_INDEX, AVAIL_SELECTED_TRANS,
CHAIN_TRANSES, CHAIN_SELECTED_INDEX, CHAIN_SELECTED_TRANS,
VALIDATING_FAILED_REASON,
SOURCE_STR, RESULT_STR
}
2 ViewStub
ViewStub.java
public void setApplyTransChainData(Map<Key, Value<?>> data)
{
}
public Map<Key, Value<?>> getOnApplyTransChainData()
{
return null;
}
3 Presenter
Presenter.java
private String resultStr;
public void applyTransChain()
{
String sourceStr = view.collectApplyTransChainData().get(SOURCE_STR).toStr();
resultStr = businessLogic.transform(sourceStr, chainTranses);
view.onApplyTransChain(interactionData(entry(RESULT_STR, resultStr)));
}
4 View
View.java
Map<Key, Value<?>> collectApplyTransChainData();
void onApplyTransChain(Map<Key, Value<?>> data);
5 ViewStub
ViewStub.java
private Map<Key, Value<?>> applyTransChainData;
private Map<Key, Value<?>> onApplyTransChainData;
@Override
public Map<Key, Value<?>> collectApplyTransChainData()
{
return applyTransChainData;
}
public void setApplyTransChainData(Map<Key, Value<?>> data)
{
applyTransChainData = data;
}
@Override
public void onApplyTransChain(Map<Key, Value<?>> data)
{
onApplyTransChainData = data;
}
public Map<Key, Value<?>> getOnApplyTransChainData()
{
return onApplyTransChainData;
}
6 BusinessLogic
BusinessLogic.java
String transform(String sourceStr, List<String> transes);
7 BusinessLogicStub
BusinessLogicStub.java
@Override
public String transform(String sourceStr, List<String> transes)
{
return "HELLO, WORLD.";
}
BusinessLogicImplTest.java
public void test_transform_upper()
{
assertEquals("HELLO, WORLD.", impl.transform("Hello, world.", asList(UPPER_TRANS)));
}
BusinessLogicImpl.java
import java.util.Map;
import java.util.function.Function;
import java.util.LinkedHashMap;
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);
}
@Override
public String transform(String sourceStr, List<String> transes)
{
return TRANS_FUNC_MAP.get(transes.get(0)).apply(sourceStr);
}
private static String upper(String str)
{
return str.toUpperCase();
}
ViewImpl.java
@Override
public Map<Key, Value<?>> collectApplyTransChainData()
{
return interactionData(entry(SOURCE_STR, txtSourceStr.getText()));
}
@Override
public void onApplyTransChain(Map<Key, Value<?>> data)
{
txtResultStr.setText(data.get(RESULT_STR).toStr());
}
private void btnApply_actionPerformed(ActionEvent e)
{
presenter.applyTransChain();
}
构建好非空的转换器链,但未输入源字符串或源字符串为空,点击Apply按钮,提示“请输入源字符串”,焦点定位到源字符串文本框。
PresenterTest.java
public void test_apply_trans_chain_when_source_str_is_empty()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, UPPER_TRANS)));
presenter.addTrans();
viewStub.setApplyTransChainData(interactionData(entry(SOURCE_STR, "")));
presenter.applyTransChain();
assertEquals(interactionData(
entry(VALIDATING_FAILED_REASON, FailedReason.SOURCE_STR_EMPTY)),
viewStub.getOnValidatingFailedData());
assertEquals(interactionData(entry(RESULT_STR, "")), viewStub.getOnApplyTransChainData());
}
1 ValidatingResult.FailedReason
Validator.java ValidatingResult Class
enum FailedReason
{
...
SOURCE_STR_EMPTY
}
2 Presenter
Presenter.java
public void applyTransChain()
{
String sourceStr = view.collectApplyTransChainData().get(SOURCE_STR).toStr();
ValidatingResult validatingResult = validate(buildParamValidatingRulesForApply(sourceStr));
resultStr = validatingResult.isSucceeded() ? businessLogic.transform(sourceStr, chainTranses) : "";
view.onApplyTransChain(interactionData(entry(RESULT_STR, resultStr)));
}
private List<ParamValidatingRule<?>> buildParamValidatingRulesForApply(String sourceStr)
{
return asList(
paramValidatingRule(sourceStr, Presenter::emptyStr,
FailedReason.SOURCE_STR_EMPTY));
}
private static boolean emptyStr(String str)
{
return str.isEmpty();
}
1 ViewImpl.java
static
{
...
VALIDATING_FAILED_REASON_AND_TIP_MAP.put(FailedReason.SOURCE_STR_EMPTY,
"Specify the source string, please.");
}
@Override
public void onValidatingFailed(Map<Key, Value<?>> data)
{
JOptionPane.showMessageDialog(this,
VALIDATING_FAILED_REASON_AND_TIP_MAP.get(data.get(VALIDATING_FAILED_REASON).get()));
if(data.get(VALIDATING_FAILED_REASON).toFailedReason() == FailedReason.SOURCE_STR_EMPTY)
{
txtSourceStr.requestFocus();
}
}
2 在Value类中定义toFailedReason()方法。
InteractionTest.java
import fayelab.tdd.stringtransformer.instruction.original.ValidatingResult.FailedReason;
public void test_toFailedReason()
{
assertEquals(FailedReason.SOURCE_STR_EMPTY, new Value<>(FailedReason.SOURCE_STR_EMPTY).toFailedReason());
}
Interaction.java Value Class
import fayelab.tdd.stringtransformer.instruction.original.ValidatingResult.FailedReason;
FailedReason toFailedReason()
{
return (FailedReason)value;
}
构建好非空的转换器链,但输入了非法的源字符串,例如包含中文字符等,点击Apply按钮,提示“请输入合法的源字符串”,焦点定位到源字符串文本框,并全选高亮当前文本。
PresenterTest.java
public void test_apply_trans_chain_when_source_str_is_illegal()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, UPPER_TRANS)));
presenter.addTrans();
viewStub.setApplyTransChainData(interactionData(entry(SOURCE_STR, "a中文b")));
presenter.applyTransChain();
assertEquals(interactionData(
entry(VALIDATING_FAILED_REASON, FailedReason.SOURCE_STR_ILLEGAL)),
viewStub.getOnValidatingFailedData());
assertEquals(interactionData(entry(RESULT_STR, "")), viewStub.getOnApplyTransChainData());
}
1 ValidatingResult.FailedReason
Validator.java ValidatingResult Class
enum FailedReason
{
...
SOURCE_STR_ILLEGAL
}
2 Presenter
Presenter.java
private List<ParamValidatingRule<?>> buildParamValidatingRulesForApply(String sourceStr)
{
return asList(
paramValidatingRule(sourceStr, Presenter::emptyStr,
FailedReason.SOURCE_STR_EMPTY),
paramValidatingRule(sourceStr, Presenter::illegalSourceStr,
FailedReason.SOURCE_STR_ILLEGAL));
}
private static boolean illegalSourceStr(String str)
{
return str.matches(".*[\u4e00-\u9fa5]+.*");
}
ViewImpl.java
static
{
...
VALIDATING_FAILED_REASON_AND_TIP_MAP.put(FailedReason.SOURCE_STR_ILLEGAL,
"Specify the legal source string, please.");
}
@Override
public void onValidatingFailed(Map<Key, Value<?>> data)
{
JOptionPane.showMessageDialog(this,
VALIDATING_FAILED_REASON_AND_TIP_MAP.get(data.get(VALIDATING_FAILED_REASON).get()));
if(data.get(VALIDATING_FAILED_REASON).toFailedReason() == FailedReason.SOURCE_STR_EMPTY)
{
txtSourceStr.requestFocus();
}
else if(data.get(VALIDATING_FAILED_REASON).toFailedReason() == FailedReason.SOURCE_STR_ILLEGAL)
{
txtSourceStr.requestFocus();
txtSourceStr.selectAll();
}
}
用解释器的方式重写ViewImpl.onValidatingFailed()方法。
ViewImpl.java
import java.util.List;
import static java.util.Arrays.asList;
private enum ValidatingFailedActionType
{
SHOW_TIP,
FOCUS_AND_SELECT_ALL_SOURCE_STR
}
private static Map<FailedReason, List<List<Object>>> VALIDATING_FAILED_REASON_AND_ACTIONS_MAP = null;
static
{
VALIDATING_FAILED_REASON_AND_ACTIONS_MAP = new HashMap<>();
VALIDATING_FAILED_REASON_AND_ACTIONS_MAP.put(FailedReason.AVAIL_TRANS_NOT_SPECIFIED,
asList(asList(ValidatingFailedActionType.SHOW_TIP,
"Specify an available transformer, please.")));
VALIDATING_FAILED_REASON_AND_ACTIONS_MAP.put(FailedReason.ADD_ALREADY_EXISTED_IN_CHAIN_TRANS,
asList(asList(ValidatingFailedActionType.SHOW_TIP,
"The transformer to be added has been already existed in the chain.")));
VALIDATING_FAILED_REASON_AND_ACTIONS_MAP.put(FailedReason.CHAIN_TRANS_NOT_SPECIFIED,
asList(asList(ValidatingFailedActionType.SHOW_TIP,
"Specify a transformer from the chain, please.")));
VALIDATING_FAILED_REASON_AND_ACTIONS_MAP.put(FailedReason.CHAIN_EMPTY,
asList(asList(ValidatingFailedActionType.SHOW_TIP,
"Specify the transformer chain, please.")));
VALIDATING_FAILED_REASON_AND_ACTIONS_MAP.put(FailedReason.SOURCE_STR_EMPTY,
asList(asList(ValidatingFailedActionType.SHOW_TIP,
"Specify the source string, please."),
asList(ValidatingFailedActionType.FOCUS_AND_SELECT_ALL_SOURCE_STR)));
VALIDATING_FAILED_REASON_AND_ACTIONS_MAP.put(FailedReason.SOURCE_STR_ILLEGAL,
asList(asList(ValidatingFailedActionType.SHOW_TIP,
"Specify the legal source string, please."),
asList(ValidatingFailedActionType.FOCUS_AND_SELECT_ALL_SOURCE_STR)));
}
@Override
public void onValidatingFailed(Map<Key, Value<?>> data)
{
List<List<Object>> actions = VALIDATING_FAILED_REASON_AND_ACTIONS_MAP.get(data.get(VALIDATING_FAILED_REASON).get());
for(List<Object> action : actions)
{
if(action.get(0) == ValidatingFailedActionType.SHOW_TIP)
{
JOptionPane.showMessageDialog(this, action.get(1));
}
else
{
txtSourceStr.requestFocus();
txtSourceStr.selectAll();
}
}
}
原来的VALIDATING_FAILED_REASON_AND_TIP_MAP删掉了。
仔细观察,onAddTrans()、onRemoveTrans()和onRemoveAllTranses()方法都是将chainTranses、chainSelectedIndex和availSelectedIndex推送给View显示。因为使用Java Swing库实现界面控件和布局,转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引的设置先后顺序无关紧要。因此可以抽取onBuildTransChain()方法。
ViewImpl.java
@Override
public void onAddTrans(Map<Key, Value<?>> data)
{
onBuildTransChain(data);
}
@Override
public void onRemoveTrans(Map<Key, Value<?>> data)
{
onBuildTransChain(data);
}
@Override
public void onRemoveAllTranses(Map<Key, Value<?>> data)
{
onBuildTransChain(data);
}
private void onBuildTransChain(Map<Key, Value<?>> data)
{
lstChain.setListData(data.get(CHAIN_TRANSES).toStrArray());
lstChain.setSelectedIndex(data.get(CHAIN_SELECTED_INDEX).toInt());
lstAvail.setSelectedIndex(data.get(AVAIL_SELECTED_INDEX).toInt());
}
输入合法的源字符串,但转换器链为空,点击Apply按钮,提示“请指定转换器链”。可用转换器列表中的转换器条目不变,选中第一个转换器。
PresenterTest.java
public void test_apply_trans_chain_when_chain_is_empty()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, LOWER_TRANS)));
presenter.addTrans();
viewStub.setRemoveTransData(interactionData(entry(CHAIN_SELECTED_TRANS, LOWER_TRANS)));
presenter.removeTrans();
viewStub.setApplyTransChainData(interactionData(entry(SOURCE_STR, "Hello, world.")));
presenter.applyTransChain();
assertEquals(interactionData(
entry(VALIDATING_FAILED_REASON, FailedReason.CHAIN_EMPTY)),
viewStub.getOnValidatingFailedData());
assertEquals(interactionData(
entry(RESULT_STR, ""), entry(AVAIL_SELECTED_INDEX, 0)),
viewStub.getOnApplyTransChainData());
}
由于OnApplyTransChainData的数据结构变了,所以applyTransChain相关的测试用例都需要修改。
PresenterTest.java
public void test_apply_trans_chain()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, UPPER_TRANS)));
presenter.addTrans();
viewStub.setApplyTransChainData(interactionData(entry(SOURCE_STR, "Hello, world.")));
presenter.applyTransChain();
assertEquals(interactionData(
entry(RESULT_STR, "HELLO, WORLD."), entry(AVAIL_SELECTED_INDEX, 1)),
viewStub.getOnApplyTransChainData());
}
public void test_apply_trans_chain_when_source_str_is_empty()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, UPPER_TRANS)));
presenter.addTrans();
viewStub.setApplyTransChainData(interactionData(entry(SOURCE_STR, "")));
presenter.applyTransChain();
assertEquals(interactionData(
entry(VALIDATING_FAILED_REASON, FailedReason.SOURCE_STR_EMPTY)),
viewStub.getOnValidatingFailedData());
assertEquals(interactionData(
entry(RESULT_STR, ""), entry(AVAIL_SELECTED_INDEX, 1)),
viewStub.getOnApplyTransChainData());
}
public void test_apply_trans_chain_when_source_str_is_illegal()
{
viewStub.setAddTransData(interactionData(entry(AVAIL_SELECTED_TRANS, UPPER_TRANS)));
presenter.addTrans();
viewStub.setApplyTransChainData(interactionData(entry(SOURCE_STR, "a中文b")));
presenter.applyTransChain();
assertEquals(interactionData(
entry(VALIDATING_FAILED_REASON, FailedReason.SOURCE_STR_ILLEGAL)),
viewStub.getOnValidatingFailedData());
assertEquals(interactionData(
entry(RESULT_STR, ""), entry(AVAIL_SELECTED_INDEX, 1)),
viewStub.getOnApplyTransChainData());
}
Presenter.java
public void applyTransChain()
{
String sourceStr = view.collectApplyTransChainData().get(SOURCE_STR).toStr();
ValidatingResult validatingResult = validate(buildParamValidatingRulesForApply(sourceStr));
resultStr = validatingResult.isSucceeded() ? businessLogic.transform(sourceStr, chainTranses) : "";
updateAvailSelectedIndexForApply(validatingResult.getFailedReason());
view.onApplyTransChain(interactionData(
entry(RESULT_STR, resultStr),
entry(AVAIL_SELECTED_INDEX, availSelectedIndex)));
}
private List<ParamValidatingRule<?>> buildParamValidatingRulesForApply(String sourceStr)
{
return asList(
paramValidatingRule(sourceStr, Presenter::emptyStr,
FailedReason.SOURCE_STR_EMPTY),
paramValidatingRule(sourceStr, Presenter::illegalSourceStr,
FailedReason.SOURCE_STR_ILLEGAL),
paramValidatingRule(chainTranses, Presenter::emptyList,
FailedReason.CHAIN_EMPTY));
}
private void updateAvailSelectedIndexForApply(FailedReason failedReason)
{
if(failedReason == FailedReason.CHAIN_EMPTY)
{
availSelectedIndex = 0;
}
}
ViewImpl.java
@Override
public void onApplyTransChain(Map<Key, Value<?>> data)
{
txtResultStr.setText(data.get(RESULT_STR).toStr());
lstAvail.setSelectedIndex(data.get(AVAIL_SELECTED_INDEX).toInt());
}
1. test_get_all_transes (Done)
2. test_transform_upper (Done)
3. test_transform_lower
4. test_transform_trimprefixspaces
5. test_transform
BusinessLogicImplTest.java
import java.util.List;
public void test_transform_lower()
{
assertEquals("hello, world.", impl.transform("Hello, world.", asList(LOWER_TRANS)));
}
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));
}
public void test_transform()
{
assertEquals("hello, world. ",
impl.transform(" Hello, world. ", asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS)));
}
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);
}
@Override
public List<String> getAllTranses()
{
return asList(TRANS_FUNC_MAP.keySet().toArray(new String[] {}));
}
@Override
public String transform(String sourceStr, List<String> transes)
{
return transes.stream()
.reduce(sourceStr, (resultStr, trans) -> TRANS_FUNC_MAP.get(trans).apply(resultStr));
}
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)
{
return str.chars()
.mapToObj(c -> asList(str.indexOf(c), c))
.filter(indexAndChar -> indexAndChar.get(1) != ' ')
.mapToInt(indexAndChar -> indexAndChar.get(0))
.findFirst()
.orElse(-1);
}
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;
}
}
Presenter.java
private <T> void operTrans(OperData<T> operData)
{
Optional<T> viewData = operData.getCollectViewDataFunc().orElse(() -> Optional.empty()).get();
ValidatingResult validatingResult = validate(operData.getBuildParamValidatingRulesFunc().apply(viewData));
operData.getUpdatePresenterDataFunc().accept(viewData, validatingResult);
operData.getPresentViewDataFunc().run();
}
Presenter.java
import static fayelab.tdd.stringtransformer.instruction.original.OperData.*;
public void addTrans()
{
operTrans(operData(Optional.of(this::collectViewDataForAdd),
this::buildParamValidatingRulesForAdd,
this::updatePresenterDataForAdd,
this::presentViewDataForAdd));
}
private Optional<String> collectViewDataForAdd()
{
return Optional.ofNullable(view.collectAddTransData().get(AVAIL_SELECTED_TRANS).toStr());
}
private List<ParamValidatingRule<?>> buildParamValidatingRulesForAdd(Optional<String> viewData)
{
String availSelectedTrans = viewData.orElse(NONE_SELECTED_TRANS);
return asList(
paramValidatingRule(availSelectedTrans, Presenter::transNotSpecified,
FailedReason.AVAIL_TRANS_NOT_SPECIFIED),
paramValidatingRule(availSelectedTrans, this::alreadyExistedInChain,
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.onAddTrans(interactionData(
entry(CHAIN_TRANSES, chainTranses),
entry(CHAIN_SELECTED_INDEX, chainSelectedIndex),
entry(AVAIL_SELECTED_INDEX, availSelectedIndex)));
}
Presenter.java
public void removeTrans()
{
operTrans(operData(Optional.of(this::collectViewDataForRemove),
this::buildParamValidatingRulesForRemove,
this::updatePresenterDataForRemove,
this::presentViewDataForRemove));
}
private Optional<String> collectViewDataForRemove()
{
Map<Key, Value<?>> removeTransData = view.collectRemoveTransData();
String chainSelectedTrans = removeTransData != null ? removeTransData.get(CHAIN_SELECTED_TRANS).toStr() : null;
return Optional.ofNullable(chainSelectedTrans);
}
private List<ParamValidatingRule<?>> buildParamValidatingRulesForRemove(Optional<String> viewData)
{
String chainSelectedTrans = viewData.orElse(NONE_SELECTED_TRANS);
return asList(
paramValidatingRule(chainTranses, Presenter::emptyList,
FailedReason.CHAIN_EMPTY),
paramValidatingRule(chainSelectedTrans, Presenter::transNotSpecified,
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.onRemoveTrans(interactionData(
entry(CHAIN_TRANSES, chainTranses),
entry(AVAIL_SELECTED_INDEX, availSelectedIndex),
entry(CHAIN_SELECTED_INDEX, chainSelectedIndex)));
}
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, FailedReason.CHAIN_EMPTY));
}
private void updatePresenterDataForRemoveAll(Optional<?> emptyViewData, ValidatingResult validatingResult)
{
if(validatingResult.isSucceeded())
{
chainTranses.clear();
}
updateChainSelectedIndexForRemoveAll();
updateAvailSelectedIndexForRemoveAll(validatingResult.getFailedReason());
}
private void presentViewDataForRemoveAll()
{
view.onRemoveAllTranses(interactionData(
entry(CHAIN_TRANSES, chainTranses),
entry(CHAIN_SELECTED_INDEX, chainSelectedIndex),
entry(AVAIL_SELECTED_INDEX, availSelectedIndex)));
}
Presenter.java
public void applyTransChain()
{
operTrans(operData(Optional.of(this::collectViewDataForApply),
this::buildParamValidatingRulesForApply,
this::updatePresenterDataForApply,
this::presentViewDataForApply));
}
private Optional<String> collectViewDataForApply()
{
return Optional.ofNullable(view.collectApplyTransChainData().get(SOURCE_STR).toStr());
}
private List<ParamValidatingRule<?>> buildParamValidatingRulesForApply(Optional<String> viewData)
{
String sourceStr = viewData.get();
return asList(
paramValidatingRule(sourceStr, Presenter::emptyStr,
FailedReason.SOURCE_STR_EMPTY),
paramValidatingRule(sourceStr, Presenter::illegalSourceStr,
FailedReason.SOURCE_STR_ILLEGAL),
paramValidatingRule(chainTranses, Presenter::emptyList,
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.onApplyTransChain(interactionData(
entry(RESULT_STR, resultStr),
entry(AVAIL_SELECTED_INDEX, availSelectedIndex)));
}
仔细观察,presentViewDataForAdd()、presentViewDataForRemove()和presentViewDataForRemoveAll()方法都是将chainTranses、chainSelectedIndex和availSelectedIndex推送给View显示。目前的实现方案中的展示操作都是顺序无关的,因此可以抽取buildPresentViewDataForBuildingTransChain()方法。
Presenter.java
private void presentViewDataForAdd()
{
view.onAddTrans(buildPresentViewDataForBuildingTransChain());
}
private void presentViewDataForRemove()
{
view.onRemoveTrans(buildPresentViewDataForBuildingTransChain());
}
private void presentViewDataForRemoveAll()
{
view.onRemoveAllTranses(buildPresentViewDataForBuildingTransChain());
}
private Map<Key, Value<?>> buildPresentViewDataForBuildingTransChain()
{
return interactionData(
entry(CHAIN_TRANSES, chainTranses),
entry(CHAIN_SELECTED_INDEX, chainSelectedIndex),
entry(AVAIL_SELECTED_INDEX, availSelectedIndex));
}
对比一下之前的View接口类和现在的View接口类,很明显,现在的View接口关注的是交互逻辑,之前的View接口关注的是交互实现细节。统一的参数校验失败通知方法,也体现出了这一点。
这样做的好处在于:
- View接口、Presenter测试代码与产品代码都不会因为界面展现细节的变化而频繁变更,保持相对稳定。
- Presenter实现与界面中使用的控件细节无关,因为那些细节被放到了ViewImpl中。
Presenter与View的交互接口采用指令式,将计算描述与执行分离。Presenter计算好交互逻辑所需要的所有数据,封装为指令,传递给View,View根据指令控制界面控件展现细节。
这么实现的话,View不再是纯哑(Dummy)的,View中会有一些实现细节逻辑。需要注意,即便View中包含一些实现细节逻辑,也只能是和界面控件细节相关的逻辑,不能是业务逻辑,而且要尽量简单,简单到一眼就能看出有没有问题。
字符串转换器支持“Reverse”转换。
主要修改BusinessLogicImpl。
代码包路径:fayelab.tdd.stringtransformer.instruction.reverse
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)));
}
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();
}
新增“Add All”(添加所有转换器)功能。
代码包路径:fayelab.tdd.stringtransformer.instruction.addall
无论转换器链列表是否为空,无论可用转换器列表是否选中转换器,点击Add All按钮,所有转换器均被添加到转换器链列表。转换器链列表和可用转换器列表完全一样。可用转换器列表中的转换器条目不变,选中第一个转换器。转换器链列表选中最后一个转换器。
PresenterTest.java
public void test_add_all_transes()
{
presenter.addAllTranses();
assertEquals(interactionData(
entry(CHAIN_TRANSES, asList(UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS)),
entry(AVAIL_SELECTED_INDEX, 0),
entry(CHAIN_SELECTED_INDEX, 2)), viewStub.getOnAddAllTransesData());
}
ViewStub.java
private Map<Key, Value<?>> onAddAllTransesData;
@Override
public void onAddAllTranses(Map<Key, Value<?>> data)
{
onAddAllTransesData = data;
}
public Map<Key, Value<?>> getOnAddAllTransesData()
{
return onAddAllTransesData;
}
1 View
View.java
void onAddAllTranses(Map<Key, Value<?>> data);
2 Presenter
需要注意的是,对于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(validate(func.apply(viewData))));
operData.getUpdatePresenterDataFunc().accept(viewData, validatingResult);
operData.getPresentViewDataFunc().run();
}
新增addAllTranses()、updatePresenterDataForAddAll()、updateChainSelectedIndexForAddAll()、updateAvailSelectedIndexForAddAll()和presentViewDataForAddAll()方法。
Presenter.java
public void addAllTranses()
{
operTrans2(operData2(Optional.empty(),
Optional.empty(),
this::updatePresenterDataForAddAll,
this::presentViewDataForAddAll));
}
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;
}
private void presentViewDataForAddAll()
{
view.onAddAllTranses(buildPresentViewDataForBuildingTransChain());
}
修改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::presentViewDataForAdd));
}
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::presentViewDataForRemove));
}
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::presentViewDataForRemoveAll));
}
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类以及相关import,并将operTrans2()重命名为operTrans(),将OperData2重命名为OperData。
ViewImpl.java
@Override
public void onAddAllTranses(Map<Key, Value<?>> data)
{
onBuildTransChain(data);
}
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();