201802 TDD Coding Practice String Transformer in Python - xiaoxianfaye/Courses GitHub Wiki
- 1 Problem
- 2 Showcase & Discuss
- 3 Guide
-
4 Analysis
- 4.1 Problem Domain
- 4.2 UI Elements
-
4.3 Business Processes
- 4.3.1 Init
-
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
- 4.3.2.1.2 Normal Business Process 2: Add the transformer which is the last
- 4.3.2.1.3 Abnormal Business Process 1: Add a transformer which has been already existed in the chain
- 4.3.2.1.4 Abnormal Business Process 2: Add a transformer but none of the available transformers is specified
-
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
- 4.3.2.2.2 Normal Business Process 2: Remove the transformer which is the last when the chain has more than one transformers
- 4.3.2.2.3 Normal Business Process 3: Remove the transformer when the chain has only one transformer
- 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
- 4.3.2.2.5 Abnormal Business Process 2: Remove a transformer when the chain is empty
- 4.3.2.3 Remove All Transformers
-
4.3.2.1 Add a Transformer
- 4.3.3 Apply Transformer Chain - 4.3.3.1 Normal Business Process 1: Apply the transformer chain - 4.3.3.2 Abnormal Business Process 1: Apply the transformer chain but the source string is empty - 4.3.3.3 Abnormal Business Process 2: Apply the transformer chain but the source string is illegal - 4.3.3.4 Abnormal Business Process 3: Apply the transformer chain but the transformer chain is empty
- 5 Design
-
6 Implementation
- 6.1 Instructions
- 6.2 Business Processes Summary
- 6.3 ViewImpl in Which Business Processes not Included
-
6.4 Business Processes
- 6.4.1 Init
-
6.4.2 Build Transformer Chain
-
6.4.2.1 Add a Transformer
- 6.4.2.1.1 Normal Business Process 1: Add the transformer which is not the last
- 6.4.2.1.2 Normal Business Process 2: Add the transformer which is the last
- 6.4.2.1.3 Abnormal Business Process 1: Add a transformer which has been already existed in the chain
- 6.4.2.1.4 Abnormal Business Process 2: Add a transformer but none of the available transformers is specified
-
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
- 6.4.2.2.2 Normal Business Process 2: Remove the transformer which is the last when the chain has more than one transformers
- 6.4.2.2.3 Normal Business Process 3: Remove the transformer when the chain has only one transformer
- 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
- 6.4.2.2.5 Abnormal Business Process 2: Remove a transformer when the chain is empty
- 6.4.2.3 Remove All Transformers
-
6.4.2.1 Add a Transformer
-
6.4.3 Apply Transformer Chain
- 6.4.3.1 Normal Business Process 1: Apply the transformer chain
- 6.4.3.2 Abnormal Business Process 1: Apply the transformer chain but the source string is empty
- 6.4.3.3 Abnormal Business Process 2: Apply the transformer chain but the source string is illegal
- 6.4.3.4 Abnormal Business Process 3: Apply the transformer chain but the transformer chain is empty
- 6.5 BusinessLogicImpl (In Addition to Upper, Other Transformers and Transformer Chain)
- 6.6 Review and Refactor the Code
- 7 Summary
- 8 Homework
- 9 My Homework
实现一个基于GUI的应用——字符串转换器。
用户在“Source String(源字符串)”文本框中输入一个英文字符串,然后选择所需要的转换器,点击“Apply(应用)”按钮,系统会按照用户所选择的转换器链对输入字符串进行转换,并将结果显示在“Result String(结果字符串)”文本框中。
目前有3个转换器:Upper(转换为大写)、Lower(转换为小写)和TrimPrefixSpaces(去除前缀空格)。
举个例子,用户输入“ hello, world. ”,并依次添加了Upper和TrimPrefixSpaces转换器到转换器链中,点击Apply按钮后的界面可参考下图:

请学员展示自己之前的设计思路和实现方式,大家可以互相点评、讨论。以学员为主,讲师为辅。
《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趣学指南》
敏捷方法,特别是测试驱动开发实践,已经唤醒了软件业对于自动化验收测试的认识。人们现在已经知道,在版本发布冲刺阶段的进度压力下,让大量的测试人员疯狂地手工运行那些枯燥乏味的测试脚本是根本不能保证软件质量的。
市面上可以买到很多工具,这些工具可以帮助测试者编写通过用户界面来进行系统测试的自动化脚本。这样的工具就像一个机器人,它们被编程用来模拟测试人员——它们揿下按钮,选择菜单项,勾上复选框,在文本框里输入数据,然后观察屏幕。
这种自动化测试方法的问题在于:
- 测试运行缓慢,无法频繁运行。
- 测试是用一种和码字类似的语言写出来的,使得测试难以理解。
- 测试非常脆弱,界面上每个微小的改变都会造成多个测试的失败或者无法运行。
大概一个世纪以前,电话公司设计了包含内置测试装置的电话交换机,解决了类似的问题。这样的装置,又称作测试总线,能够让电话公司在晚间自动测试每条电话线,这样远在用户发现问题前,他们就把故障和对服务质量的影响给消除了。
软件开发商也开始采用类似的策略,用内置的测试总线来构架软件系统。他们使用类似Fit以及FitNesse (www.fitnesse.org)这样的工具,用一种业务人员易于理解的说明性语言来描述测试。
不过,要想构建出这样的自动化验收测试,在架构设计层面,至少要做到三个主要的隔离:
- 绕过UI
- 隔离测试总线
- 隔离数据库
在软件系统中,测试总线指的是一组API,通过这组API能够方便地编写单元测试和验收测试。这些测试描述了系统行为。单元测试描述了模块行为,而验收测试描述了功能特性行为。
测试总线的存在意味着系统中存在一些相应的结构,能够让测试去访问一些承载它们所描述行为的模块和子系统。例如,想要编写绕过UI去操作底层业务规则的测试,就得有这样一个API,UI 和测试都可以使用这个API调用业务规则。此外,这个API必须还要独立于UI。

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

然而,有些UI富含逻辑,很容易将业务规则包含进去。例如,有些系统会在UI中校验数据或者执行一些重要计算。在客户-服务器以及基于WEB的系统中,这种情况尤其普遍。此时,采用一般的设计策略往往会使得业务规则难以测试。不过,如果开发团队把可测试性作为一种强制的架构要求,那么他们就必须得提供能够让测试访问业务规则的机制。一种常见的解决方案就是在UI中创建另一个API来把校验、计算规则与UI的底层细节隔离。
显然,这种API会约束UI的设计方式以及设计者使用客户端工具(例如JavaScript)的方式。设计者必须找到一种使得测试可以方便地访问客户端业务规则的方法,这种方法就是:把客户端业务规则和UI进行隔离,使得测试总线API可以调用这些业务规则。分离UI和业务规则一直以来都是一个好的设计目标:“模型—视图隔离原则告诉我们,模型(领域)对象不应当和视图对象有直接的依赖关系”。强制的测试总线要求会把好的设计目标变成一个必须要实现的需求。
基于每个API写的测试应当仅仅测试该API所涵盖的东西。针对Presentation API编写的测试不应该直接测试业务规则API涵盖的内容。原因和不要通过UI去测试业务逻辑一样。如果你通过Presentation API去测试业务规则,那么Presentation 层就会变得难以改变,因为这些改变会破坏测试。
这里,我们再一次看到,把测试总线当成一种强制要求会迫使我们达成好的设计目标。在子系统和层级间进行解耦一直以来都是良好设计的特征,不过当我们用自动化测试来明确系统的行为时,这种解耦就变成了一个需求,而不仅仅只是一个设计目标。
通过被隔离的API(而不是UI)运行这些测试极大地加快了测试的运行速度,使得测试可以在每次构建后都运行,一天能运行很多次。
数据库是测试运行慢的另外一个原因。在大型数据库上的操作会比较慢,而同样的操作在小型数据库上就可能快很多。这里有一个简单的加速测试的策略,就是在一系列小型的、预先准备好的数据库上运行测试。测试系统只在每次测试前创建这些数据库,测试运行完毕即删除它。
即便是小型数据库,也需要通过磁头的旋转从磁盘上读写数据。这些磁盘操作和RAM上的同样操作相比,慢得不是一个数量级。所以,另外一个加速测试运行的策略就是从应用中将数据库分离出去,在测试中用一个只在RAM中存在的数据库替换它。

上图展示了我们可以创建另外一个API来达到这个目标,业务规则可以使用这个API,无论是真实的数据库还是RAM中的数据库都来实现这个API。
速度不是这种隔离带来的唯一好处。在RAM中运行测试用例能够让测试完全控制数据库中的内容。测试首先使用一个空白的数据库,调用特定的启动函数将RAM 数据库设置到一个已知的状态。而且,如果测试在一个RAM 数据库上运行,那么它们对数据的修改是不会持久化的,这使得测试既是独立的又是可重复运行的。对测试速度,测试可重复性以及测试独立性的要求使得这个分离成为了一个至关重要的架构需求。
在一个真实的测试驱动环境中,对隔离的要求会延伸到更低的层次,从主要的子系统到模块,到类,甚至到方法。作为编写验收测试的业务分析师,质量保证专家,和测试人员,以及编写单元测试的开发人员,将软件隔离成测试可以独立访问和操作的单元,已经成为一个急迫的需求。而这又进一步要求架构师,设计人员以及开发人员具有更高、更好的面向对象设计技能。面向对象的设计为我们提供了工具,原则以及模式,使得以上提到的自动化测试需要的那些隔离能够达成。
当我们把可测试性作为一个强制的架构设计要求时,它会迫使我们去遵循好的设计原则并降低我们系统的耦合。
分析:定义清楚问题是什么。
String Transformer based on UI (基于UI的字符串转换器)
界面要素包括:
- Source String:源字符串,单行文本框、可编辑
- Result String:结果字符串,单行文本框、不可编辑、可拷贝选中文本
- Available Transformers:可用转换器,Upper|Lower|TrimPrefixSpaces,列表、单选
- Transformer Chain:转换器链,列表、单选
- Add >>:添加转换器到转换器链,按钮
- Remove <<:从转换器链移除转换器,按钮
- Remove All:从转换器链移除所有转换器,按钮
- Apply:将转换器链应用到源字符串,按钮
- Exit:退出,按钮
业务流程包括:初始化、构建转换器链、将转换器链应用到源字符串。
初始化完成后:
- 在可用转换器列表中依次呈现Upper、Lower和TrimPrefixSpaces三个条目,并选中第一个转换器。
- 转换器链列表、源字符串文本框、结果字符串文本框均为空。
构建转换器链包括:添加转换器、移除转换器和移除所有转换器。
在可用转换器列表中选中不是最后的一个转换器,点击Add按钮,选中的转换器被添加到转换器链列表末尾,转换器链列表选中新添加的转换器。可用转换器列表中的转换器条目不变,选中之前选中的转换器的下一个转换器。
在可用转换器列表中选中最后一个转换器,点击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.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按钮,选中的转换器被移除出转换器链。可用转换器列表中的转换器条目不变,选中被移除的转换器。转换器链列表选中第一个转换器。
当转换器链列表仅包含一个转换器时,在转换器链列表中选中这个转换器,点击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按钮,提示“请在转换器链中指定一个转换器”。可用转换器列表中的转换器条目不变,选中之前已经选中的转换器。转换器链列表中的转换器条目不变,选中第一个转换器。
转换器链列表为空,无选中项,点击Remove按钮,提示“转换器链为空”。可用转换器列表中的转换器条目不变,选中第一个转换器。
转换器链列表不为空,无论转换器链列表是否选中转换器,点击Remove All按钮,所有转换器均被移除出转换器链,转换器链列表为空,无选中项。可用转换器列表中的转换器条目不变,选中第一个转换器。
转换器链列表为空,无选中项,点击Remove All按钮,提示“请指定转换器链”。可用转换器列表中的转换器条目不变,选中之前选中的转换器。
输入合法的源字符串,构建好非空的转换器链,点击Apply按钮,将转换器链中的转换器从上到下依次应用到源字符串上,得到最终的结果字符串。
构建好非空的转换器链,但未输入源字符串或源字符串为空,点击Apply按钮,提示“请输入源字符串”,焦点定位到源字符串文本框。
构建好非空的转换器链,但输入了非法的源字符串,例如包含中文字符等,点击Apply按钮,提示“请输入合法的源字符串”,焦点定位到源字符串文本框,并全选高亮当前文本。
输入合法的源字符串,但转换器链为空,点击Apply按钮,提示“请指定转换器链”。可用转换器列表中的转换器条目不变,选中第一个转换器。
设计:问题分析清楚以后,提出解决问题的逻辑框架。
根据《测试总线势在必行》“Bypassing the UI”一节中的Figure 2,设计如下:

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




- TDD三步军规、小步。
- 测试:
- 始终选择当前最有价值的测试用例;
- 测试三段式:Arrange、Act、Assert;
- 不要忘了测试代码也需要重构。
- 每次修改代码,都运行一下测试用例,注意红绿绿的节奏。
- 写代码应做到:
- 简洁明了:概念定义清晰、准确;简明清晰地表达语义;
- 表达到位:层次分明、表现意图;跟问题领域的概念、规则相吻合;
- 安全可靠:状态、时序管理得当;具有必要的测试安全网络。
- 开始写一个测试用例前,先让学员思考如何写测试、如何验证。
- 规范(可探讨):
- 目录结构:如下所示;
- 模块名:全小写、多个单词用下划线分隔;
- 类名:CamelCase、首字母大写、测试类名以Test开头
- 方法/函数名:全小写、多个单词用下划线分隔,内部方法/函数名以下划线开头。
目录结构
func
|- tests
| |- __init__.py
| |- test_module_a.py
| |- test_module_b.py
|- module_a.py
|- module_b.py
|- test_all.py
test_all.py
import unittest
from tests.test_module_a import TestClassA
from tests.test_module_b import TestClassB
if __name__ == '__main__':
unittest.main()以module_a为例。
tests.test_module_a.py
import unittest
from module_a import ClassA
class TestClassA(unittest.TestCase):
...module_a.py
class ClassA(object):
...从交付角度,以一个业务流程为单位,将所有业务流程按端到端(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类。使用Python自带的Tkinter库实现界面控件和布局,并提供了一些方法封装了Tkinter控件操作细节。
代码详见view(Only Layout).py。
view.py ViewImpl Class
from Tkinter import *
class ViewImpl(object):
def __init__(self):
self.root = Tk()
self.root.title('String Transformer')
self.initui()
def centershow(self, wndwidth, wndheight):
scnwidth, scnheight = self.root.maxsize()
geometrystr = '%dx%d+%d+%d' % (wndwidth, wndheight,
(scnwidth - wndwidth) / 2,
(scnheight - wndheight) / 2)
self.root.geometry(geometrystr)
def initui(self):
self.init_topframe()
self.init_centerframe()
self.init_bottomframe()
def init_topframe(self):
topframe = Frame(self.root)
topframe.pack(side=TOP, fill=BOTH, padx=10, pady=10)
labelsframe = Frame(topframe)
labelsframe.pack(side=LEFT)
Label(labelsframe, text="Source String").grid(row=0, sticky=W, pady=5)
Label(labelsframe, text="Result String").grid(row=1, sticky=W, pady=5)
txtsframe = Frame(topframe)
txtsframe.pack(fill=BOTH, padx=5)
self.txtsourcestr = Entry(txtsframe)
self.txtsourcestr.pack(side=TOP, fill=BOTH, pady=6)
self.resultstr = StringVar()
self.txtresultstr = Entry(txtsframe, textvariable=self.resultstr, state='readonly')
self.txtresultstr.pack(side=BOTTOM, fill=BOTH, pady=6)
def init_centerframe(self):
centerframe = Frame(self.root)
centerframe.pack(fill=BOTH, padx=10, pady=10)
self.init_availframe(centerframe)
self.init_chainframe(centerframe)
self.init_operbtnsframe(centerframe)
def init_availframe(self, parent):
availframe = LabelFrame(parent, text='Available Transformers')
availframe.pack(side=LEFT, padx=5)
scrolly = Scrollbar(availframe)
scrolly.pack(side=RIGHT, fill=Y)
self.lstavail = Listbox(availframe, width=25, yscrollcommand=scrolly.set)
self.lstavail.pack(fill=BOTH)
scrolly.config(command=self.lstavail.yview)
def init_chainframe(self, parent):
chainframe = LabelFrame(parent, text='Transformer Chain')
chainframe.pack(side=RIGHT, padx=5)
scrolly = Scrollbar(chainframe)
scrolly.pack(side=RIGHT, fill=Y)
self.lstchain = Listbox(chainframe, width=25, yscrollcommand=scrolly.set)
self.lstchain.pack(fill=BOTH)
scrolly.config(command=self.lstchain.yview)
def init_operbtnsframe(self, parent):
operbtnsframe = Frame(parent)
operbtnsframe.pack(fill=BOTH)
topemptyframe = Frame(operbtnsframe)
topemptyframe.pack(side=TOP)
Label(topemptyframe, text="").pack()
bottomemptyframe = Frame(operbtnsframe)
bottomemptyframe.pack(side=BOTTOM)
Label(bottomemptyframe, text="").pack()
Button(operbtnsframe, text='Add >>', width=10, command=self.add_transformer).pack(pady=10)
Button(operbtnsframe, text='Remove <<', width=10, command=self.remove_transformer).pack(pady=10)
Button(operbtnsframe, text='Remove All', width=10, command=self.remove_all_transformers).pack(pady=10)
def init_bottomframe(self):
bottomframe = Frame(self.root)
bottomframe.pack(side=BOTTOM, fill=X, padx=10, pady=10)
btnsframe = Frame(bottomframe)
btnsframe.pack(side=RIGHT)
Button(btnsframe, text='Apply', width=10, command=self.apply_transformer_chain).pack(side=LEFT, padx=5)
Button(btnsframe, text='Exit', width=10, command=self.exit).pack(side=LEFT, padx=5)
@staticmethod
def set_list_data(lstbox, items):
lstbox.delete(0, END)
for item in items:
lstbox.insert(END, item)
@staticmethod
def set_list_selected_index(lstbox, index):
lstbox.selection_clear(0, END)
lstbox.selection_set(index)
@staticmethod
def get_list_selected_item(lstbox):
selected_index = lstbox.curselection()
return lstbox.get(selected_index) if selected_index else None
@staticmethod
def get_entry_txt(self, entry):
return entry.get()
@staticmethod
def set_entry_txt(self, strvar, s):
strvar.set(s)
def add_transformer(self): pass
def remove_transformer(self): pass
def remove_all_transformers(self): pass
def apply_transformer_chain(self): pass
def exit(self):
self.root.destroy()
if __name__ == '__main__':
viewimpl = ViewImpl()
viewimpl.centershow(560, 400)
viewimpl.root.mainloop()运行起来,如下图所示:

代码路径:dummy.original
初始化完成后:
- 在可用转换器列表中依次呈现Upper、Lower和TrimPrefixSpaces三个条目,并选中第一个转换器。
- 转换器链列表、源字符串文本框、结果字符串文本框均为空。
由BusinessLogic提供的所有转换器经由Presenter推送给View显示,可用转换器列表选中索引为0的转换器。
test_all.py
import unittest
from tests.test_presenter import TestPresenter
if __name__ == '__main__':
unittest.main()tests.test_presenter.py TestPresenter Class
import unittest
from presenter import Presenter
class TestPresenter(unittest.TestCase):
def test_init(self):
viewstub = ViewStub()
businesslogicstub = BusinessLogicStub()
presenter = Presenter(viewstub, businesslogicstub)
presenter.init()
self.assertEquals(['Upper', 'Lower', 'TrimPrefixSpaces'], viewstub.get_avail_transes())
self.assertEquals(0, viewstub.get_avail_selected_index())在View这一层验证View拿到的可用转换器列表和可用转换器列表选中转换器的索引。强调一下,必须在View这一层验证。如果只在Presenter这一层验证,无法验证Presenter和View是否正确地进行了交互。
1 新增ViewStub类。
tests.test_presenter.py ViewStub Class
from view import View
class ViewStub(View):
def get_avail_transes(self): pass
def get_avail_selected_index(self): pass2 ViewStub继承自View,新增View(接口)类。
view.py View (Interface) Class
class View(object): passQ:ViewStub继承自View,那么viewstub.get_avail_transes()是View的方法,还是ViewStub的方法?
viewstub.get_avail_transes()是ViewStub的测试辅助方法,不是View的方法。假设viewstub.get_avail_transes()是View的方法,View的方法都是给Presenter使用的,那么站在Presenter的角度是否需要使用View的这个方法?换个问法,Presenter需要从View获取可用转换器列表吗?不需要。同理,viewstub.get_avail_selected_index()也是ViewStub的测试辅助方法。
3 新增BusinessLogicStub类。
tests.test_presenter.py BusinessLogicStub Class
from businesslogic import BusinessLogic
class BusinessLogicStub(BusinessLogic): pass4 BusinessLogicStub继承自BusinessLogic,新增BusinessLogic(接口)类。
businesslogic.py BusinessLogic (Interface) Class
class BusinessLogic(object): pass5 新增Presenter类。
presenter.py Presenter Class
class Presenter(object):
def __init__(self, view, businesslogic):
self.view = view
self.businesslogic = businesslogic
self.avail_selected_index = 0
def init(self):
self.view.present_avail_transes(self.businesslogic.get_all_transes())
self.view.set_avail_selected_index(self.avail_selected_index)- Presenter类的构造参数应该是“View view”和“BusinessLogic businesslogic”,而不是“ViewStub viewstub”和“BusinessLogicStub businesslogicstub”。Presenter是产品代码,不能引用测试代码,它只会看到View和BusinessLogic,看不到ViewStub和BusinessLogicStub。
- 根据之前的业务流程分析可知,在某些流程中,新选中的转换器的索引依赖于之前选中的转换器的索引,因此Presenter中需要维护选中转换器的索引。这里先新增avail_selected_index成员变量,用于保存可用转换器列表选中的转换器的索引,并初始化为0。
- 在Presenter.init()方法中,从businesslogic获得所有转换器,推送给View显示为可用转换器列表,并告诉View可用转换器列表选中转换器的索引。
6 View类新增present_avail_transes()和set_avail_selected_index()方法。
view.py View (Interface) Class
def present_avail_transes(self, transes): pass
def set_avail_selected_index(self, index): pass7 ViewStub类实现View新增方法。
tests.test_presenter.py ViewStub Class
# Override
def present_avail_transes(self, transes):
self.avail_transes = transes
def get_avail_transes(self):
return self.avail_transes
# Override
def set_avail_selected_index(self, index):
self.avail_selected_index = index
def get_avail_selected_index(self):
return self.avail_selected_index在ViewStub.present_avail_transes()方法中,把传入的transes保存到avail_transes,并在get_avail_transes()方法中返回avail_transes。这是一种打桩的方法,证明View.present_avail_transes()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。
同理,在ViewStub.set_avail_selected_index()方法中,把传入的index保存到avail_selected_index,并在get_avail_selected_index()方法中返回avail_selected_index,证明View.set_avail_selected_index()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。
8 BusinessLogic类新增get_all_transes()方法。
businesslogic.py BusinessLogic Class
def get_all_transes(self): pass9 BusinessLogicStub类实现BusinessLogic新增方法。
tests.test_presenter.py BusinessLogicStub Class
def get_all_transes(self):
return ['Upper', 'Lower', 'TrimPrefixSpaces']看上去BusinessLogicStub.get_all_transes()似乎做了产品代码要做的事情,但这里其实还是打桩。因为在产品场景中,all_transes有可能来自于分布式的服务端、数据库或者文件。在TDD开发时,这些耦合都要解除,否则无法测试。
将'Upper'、'Lower'和'TrimPrefixSpaces'字符串单独定义到一个“常量”文件中。这里的“常量”打上引号,是因为并不是真正意义上的不可变常量,但这属于非常细小的实现细节,这里就先用全局变量简单实现一下。
trans.py
UPPER_TRANS = 'Upper'
LOWER_TRANS = 'Lower'
TRIM_PREFIX_SPACES_TRANS = 'TrimPrefixSpaces'tests.test_presenter.py TestPresenter Class
from trans import *
def test_init(self):
viewstub = ViewStub()
businesslogicstub = BusinessLogicStub()
presenter = Presenter(viewstub, businesslogicstub)
presenter.init()
self.assertEquals([UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS], viewstub.get_avail_transes())
self.assertEquals(0, viewstub.get_avail_selected_index())tests.test_presenter.py BusinessLogicStub Class
def get_all_transes(self):
return [UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS]BusinessLogicImpl提供所有的转换器。
test_all.py
from tests.test_businesslogicimpl import TestBusinessLogicImpltests.test_businesslogicimpl.py TestBusinessLogicImpl Class
import unittest
from businesslogic import BusinessLogicImpl
from trans import *
class TestBusinessLogicImpl(unittest.TestCase):
def test_get_all_transes(self):
impl = BusinessLogicImpl()
self.assertEquals([UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS], impl.get_all_transes())新增BusinessLogicImpl类,继承自BusinessLogic,实现BusinessLogic新增方法。
businesslogic.py BusinessLogicImpl Class
from trans import *
class BusinessLogicImpl(BusinessLogic):
# Override
def get_all_transes(self):
return [UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS]无。
ViewImpl继承自View,实现View新增方法。
view.py ViewImpl Class
class ViewImpl(View):
...
# Override
def present_avail_transes(self, transes):
ViewImpl.set_list_data(self.lstavail, transes)
# Override
def set_avail_selected_index(self, index):
ViewImpl.set_list_selected_index(self.lstavail, index)在TestPresenter的test_init()方法中,viewstub、businesslogicstub作为构造参数构造出测试环境中的presenter,并调用presenter.init()方法完成初始化。那么,如何构造产品环境中的presenter,并调用presenter.init()方法完成初始化呢?
view.py的“main”是整个程序的运行总入口,在这里完成上述过程是合适的。
view.py
from businesslogic import BusinessLogicImpl
from presenter import Presenter
if __name__ == '__main__':
viewimpl = ViewImpl()
businesslogicimpl = BusinessLogicImpl()
presenter = Presenter(viewimpl, businesslogicimpl)
presenter.init()
viewimpl.centershow(560, 400)
viewimpl.root.mainloop()运行起来,如下图所示:

构建转换器链包括:添加转换器、移除转换器和移除所有转换器。
构建转换器链的代码主要集中在Presenter和View。
在可用转换器列表中选中不是最后的一个转换器,点击Add按钮,选中的转换器被添加到转换器链列表末尾,转换器链列表选中新添加的转换器。可用转换器列表中的转换器条目不变,选中之前选中的转换器的下一个转换器。
####### 6.4.2.1.1.1.1 Add a Test tests.test_presenter.py TestPresenter Class
def test_add_not_the_last_trans(self):
viewstub = ViewStub()
businesslogicstub = BusinessLogicStub()
presenter = Presenter(viewstub, businesslogicstub)
presenter.init()
viewstub.set_avail_selected_trans(UPPER_TRANS)
presenter.add_trans()
self.assertEquals([UPPER_TRANS], viewstub.get_chain_transes())
self.assertEquals(0, viewstub.get_chain_selected_index())
self.assertEquals(1, viewstub.get_avail_selected_index())在View这一层验证View拿到的转换器链列表、转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引。
####### 6.4.2.1.1.1.2 Write the Code 1 ViewStub
tests.test_presenter.py ViewStub Class
def set_avail_selected_trans(self, trans): pass
def get_chain_transes(self): pass
def get_chain_selected_index(self): passset_avail_selected_trans()、get_chain_transes()和get_chain_selected_index()都是ViewStub的测试辅助方法,不是View的方法。
2 Presenter
presenter.py Presenter Class
from trans import NONE_SELECTED_INDEX
def __init__(self, view, businesslogic):
self.view = view
self.businesslogic = businesslogic
self.avail_selected_index = 0
self.chain_selected_index = NONE_SELECTED_INDEX
self.avail_transes = None
self.chain_transes = []
def init(self):
self.avail_transes = self.businesslogic.get_all_transes()
self.view.present_avail_transes(self.avail_transes)
self.view.set_avail_selected_index(self.avail_selected_index)
def add_trans(self):
avail_selected_trans = self.view.get_avail_selected_trans()
self.chain_transes.append(avail_selected_trans)
self.chain_selected_index = self.chain_transes.index(avail_selected_trans)
self.avail_selected_index = self.avail_transes.index(avail_selected_trans) + 1
self.view.present_chain_transes(self.chain_transes)
self.view.set_chain_selected_index(self.chain_selected_index)
self.view.set_avail_selected_index(self.avail_selected_index)其中,NONE_SELECTED_INDEX定义在trans.py中,值为-1,表示无选中索引。
trans.py
NONE_SELECTED_INDEX = -1- 除了已有的avail_selected_index,Presenter类新增chain_selected_index、avail_transes和chain_transes成员变量,它们全部都是Presenter需要维护的状态,含义如下:
- avail_selected_index: 可用转换器列表选中的转换器的索引,初始化为0。
- chain_selected_index: 转换器链列表选中的转换器的索引,初始化为NONE_SELECTED_INDEX (-1)。
- avail_transes: 可用转换器列表,初始化为None。
- chain_transes: 转换器链列表,初始化为[]。
- 在init()方法中,从businesslogic获得的所有转换器保存到avail_transes中,并将avail_transes推送给View显示为可用转换器列表,并告诉View可用转换器列表选中转换器的索引。
- 在add_trans()方法中,从View获取选中的可用转换器,添加到chain_transes末尾。计算转换器链列表选中转换器的索引并更新chain_selected_index。计算可用转换器列表选中转换器的索引并更新avail_selected_index。将chain_transes推送给View显示为转换器链列表,并告诉View转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引。
3 View
view.py View (Interface) Class
def get_avail_selected_trans(self): pass
def present_chain_transes(self, transes): pass
def set_chain_selected_index(self, index): pass4 ViewStub
tests.test_presenter.py ViewStub Class
# Override
def get_avail_selected_trans(self):
return self.avail_selected_trans
def set_avail_selected_trans(self, trans):
self.avail_selected_trans = trans
# Override
def present_chain_transes(self, transes):
self.chain_transes = transes
def get_chain_transes(self):
return self.chain_transes
# Override
def set_chain_selected_index(self, index):
self.chain_selected_index = index
def get_chain_selected_index(self):
return self.chain_selected_index- 在get_avail_selected_trans()方法中返回在set_avail_selected_trans()方法中保存下来的avail_selected_trans。
- 在get_chain_transes()方法中返回在present_chain_transes()方法中保存下来的chain_transes。
- 在get_chain_selected_index()方法中返回在set_chain_selected_index()方法中保存下来的chain_selected_index。
这些都是打桩的方法,证明View.get_avail_selected_trans()、View.present_chain_transes()、View.set_chain_selected_index()以及View.set_avail_selected_index()这些方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。
####### 6.4.2.1.1.1.3 Refactor the Code 1 重构测试代码。TestPresenter类抽取setUp()方法。
tests.test_presenter.py TestPresenter Class
def setUp(self):
self.viewstub = ViewStub()
self.businesslogicstub = BusinessLogicStub()
self.presenter = Presenter(self.viewstub, self.businesslogicstub)
self.presenter.init()
def test_init(self):
self.assertEquals([UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS], self.viewstub.get_avail_transes())
self.assertEquals(0, self.viewstub.get_avail_selected_index())
def test_add_not_the_last_trans(self):
self.viewstub.set_avail_selected_trans(UPPER_TRANS)
self.presenter.add_trans()
self.assertEquals([UPPER_TRANS], self.viewstub.get_chain_transes())
self.assertEquals(0, self.viewstub.get_chain_selected_index())
self.assertEquals(1, self.viewstub.get_avail_selected_index())2 重构产品代码。Presenter类抽取update_chain_selected_index()和update_avail_selected_index()方法。
presenter.py Presenter Class
def add_trans(self):
avail_selected_trans = self.view.get_avail_selected_trans()
self.chain_transes.append(avail_selected_trans)
self.update_chain_selected_index(avail_selected_trans)
self.update_avail_selected_index(avail_selected_trans)
self.view.present_chain_transes(self.chain_transes)
self.view.set_chain_selected_index(self.chain_selected_index)
self.view.set_avail_selected_index(self.avail_selected_index)
def update_chain_selected_index(self, avail_selected_trans):
self.chain_selected_index = self.chain_transes.index(avail_selected_trans)
def update_avail_selected_index(self, avail_selected_trans):
self.avail_selected_index = self.avail_transes.index(avail_selected_trans) + 1view.py ViewImpl Class
# Override
def get_avail_selected_trans(self):
return ViewImpl.get_list_selected_item(self.lstavail)
# Override
def present_chain_transes(self, transes):
ViewImpl.set_list_data(self.lstchain, transes)
# Override
def set_chain_selected_index(self, index):
ViewImpl.set_list_selected_index(self.lstchain, index)ViewImpl的各个事件响应方法的实现都是简单委托Presenter来做,不包含任何其他逻辑。在Add按钮的事件响应方法中调用Presenter.add_trans()方法。但是,ViewImpl中还没有Presenter的对象实例。
可以通过set方式将Presenter对象实例注入到ViewImpl中。之所以不直接在ViewImpl的构造方法中创建Presenter对象实例,是因为创建Presenter对象实例需要有View和BusinessLogic接口类对象的实例。如果直接在ViewImpl类的构造方法中创建Presenter对象实例,View就要知道BusinessLogic,而View本来是不需要知道BusinessLogic的。
在Presenter类的构造方法中调用View.set_presenter()方法,真正建立Presenter和View之间的双向联系。而Presenter只知道View接口类,因此set_presenter()是View接口类的方法。ViewImpl实现View.set_presenter()方法,将注入的Presenter对象实例保存成ViewImpl的成员变量。ViewStub也需要实现View.set_presenter()方法,只不过是空实现而已。
view.py View (Interface) Class
def set_presenter(self, presenter): passview.py ViewImpl Class
def add_transformer(self):
self.presenter.add_trans()
# Override
def set_presenter(self, presenter):
self.presenter = presentertests.test_presenter.py ViewStub Class
# Override
def set_presenter(self, presenter): passpresenter.py Presenter Class
def __init__(self, view, businesslogic):
self.view = view
self.businesslogic = businesslogic
self.view.set_presenter(self)
...运行起来,如下图所示:

应该会注意到,转换器链列表并没有高亮显示选中的转换器。这是Tkinter的控件焦点控制机制导致的,只有最后设置选中的控件才会高亮显示出选中状态。在Presenter.add_trans()方法中先设置了转换器链列表选中转换器的索引,后设置了可用转换器列表选中转换器的索引,因此最终显示了可用转换器列表的高亮选中状态。之所以设计为这样的顺序,是为了让用户能方便地连续添加可用转换器。
在可用转换器列表中选中最后一个转换器,点击Add按钮,选中的转换器被添加到转换器链列表末尾,转换器链列表选中新添加的转换器。可用转换器列表中的转换器条目不变,选中第一个转换器。
####### 6.4.2.1.2.1.1 Add a Test tests.test_presenter.py TestPresenter Class
def test_add_the_last_trans(self):
self.viewstub.set_avail_selected_trans(TRIM_PREFIX_SPACES_TRANS)
self.presenter.add_trans()
self.assertEquals([TRIM_PREFIX_SPACES_TRANS], self.viewstub.get_chain_transes())
self.assertEquals(0, self.viewstub.get_chain_selected_index())
self.assertEquals(0, self.viewstub.get_avail_selected_index())在View这一层验证View拿到的转换器链列表、转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引。
####### 6.4.2.1.2.1.2 Write the Code presenter.py Presenter Class
def update_avail_selected_index(self, avail_selected_trans):
selected_index = self.avail_transes.index(avail_selected_trans)
self.avail_selected_index = 0 if selected_index == len(self.avail_transes) - 1 else selected_index + 1####### 6.4.2.1.2.1.3 Refactor the Code Presenter类抽取is_last_index()静态方法。
presenter.py Presenter Class
def update_avail_selected_index(self, avail_selected_trans):
selected_index = self.avail_transes.index(avail_selected_trans)
self.avail_selected_index = \
0 if Presenter.is_last_index(selected_index, self.avail_transes) else selected_index + 1
@staticmethod
def is_last_index(index, lst):
return index == len(lst) - 16.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.1 Add a Test tests.test_presenter.py TestPresenter Class
def test_add_already_existed_in_chain_trans(self):
self.viewstub.set_avail_selected_trans(UPPER_TRANS)
self.presenter.add_trans()
self.viewstub.set_avail_selected_trans(LOWER_TRANS)
self.presenter.add_trans()
self.viewstub.set_avail_selected_trans(LOWER_TRANS)
self.presenter.add_trans()
self.assertTrue(self.viewstub.is_add_already_existed_in_chain_trans_notified())
self.assertEquals([UPPER_TRANS, LOWER_TRANS], self.viewstub.get_chain_transes())
self.assertEquals(1, self.viewstub.get_chain_selected_index())
self.assertEquals(1, self.viewstub.get_avail_selected_index())在验证View拿到的转换器链列表、转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引之前,先验证View是否收到add_already_existed_in_chain_trans的通知。
####### 6.4.2.1.3.1.2 Write the Code 1 ViewStub
tests.test_presenter.py ViewStub Class
def is_add_already_existed_in_chain_trans_notified(self): passis_add_already_existed_in_chain_trans_notified()是ViewStub的测试辅助方法,不是View的方法。
2 Presenter
presenter.py Presenter Class
def add_trans(self):
avail_selected_trans = self.view.get_avail_selected_trans()
is_add_already_existed_in_chain_trans = False
if avail_selected_trans in self.chain_transes:
is_add_already_existed_in_chain_trans = True
self.view.notify_add_already_existed_in_chain_trans()
else:
self.chain_transes.append(avail_selected_trans)
self.update_chain_selected_index(avail_selected_trans)
self.update_avail_selected_index(avail_selected_trans, is_add_already_existed_in_chain_trans)
self.view.present_chain_transes(self.chain_transes)
self.view.set_chain_selected_index(self.chain_selected_index)
self.view.set_avail_selected_index(self.avail_selected_index)
def update_avail_selected_index(self, avail_selected_trans, is_add_already_existed_in_chain_trans):
selected_index = self.avail_transes.index(avail_selected_trans)
if is_add_already_existed_in_chain_trans:
self.avail_selected_index = selected_index
else:
self.avail_selected_index = \
0 if Presenter.is_last_index(selected_index, self.avail_transes) else selected_index + 13 View
view.py View (Interface) Class
def notify_add_already_existed_in_chain_trans(self): pass4 ViewStub
tests.test_presenter.py ViewStub Class
# Override
def notify_add_already_existed_in_chain_trans(self):
self.add_already_existed_in_chain_trans_notified = True
def is_add_already_existed_in_chain_trans_notified(self):
return self.add_already_existed_in_chain_trans_notified在is_add_already_existed_in_chain_trans_notified()方法中返回在notify_add_already_existed_in_chain_trans()方法中保存下来的add_already_existed_in_chain_trans_notified。
这还是打桩的方法,证明View.notify_add_already_existed_in_chain_trans()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。
####### 6.4.2.1.3.1.3 Refactor the Code Presenter类抽取already_existed_in_chain()方法。
presenter.py Presenter Class
def add_trans(self):
avail_selected_trans = self.view.get_avail_selected_trans()
is_add_already_existed_in_chain_trans = False
if self.already_existed_in_chain(avail_selected_trans):
is_add_already_existed_in_chain_trans = True
self.view.notify_add_already_existed_in_chain_trans()
else:
self.chain_transes.append(avail_selected_trans)
self.update_chain_selected_index(avail_selected_trans)
self.update_avail_selected_index(avail_selected_trans, is_add_already_existed_in_chain_trans)
self.view.present_chain_transes(self.chain_transes)
self.view.set_chain_selected_index(self.chain_selected_index)
self.view.set_avail_selected_index(self.avail_selected_index)
def already_existed_in_chain(self, trans):
return trans in self.chain_transesview.py ViewImpl Class
import tkMessageBox
# Override
def notify_add_already_existed_in_chain_trans(self):
tkMessageBox.showinfo('Information', 'The transformer to be added has been already existed in the chain.')运行起来,如下图所示:

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.1 Add a Test tests.test_presenter.py TestPresenter Class
def test_add_trans_but_avail_trans_not_specified(self):
self.viewstub.set_avail_selected_trans(UPPER_TRANS)
self.presenter.add_trans()
self.viewstub.set_avail_selected_trans(LOWER_TRANS)
self.presenter.add_trans()
self.viewstub.set_avail_selected_trans(None)
self.presenter.add_trans()
self.assertTrue(self.viewstub.is_avail_trans_not_specified_notified())
self.assertEquals([UPPER_TRANS, LOWER_TRANS], self.viewstub.get_chain_transes())
self.assertEquals(1, self.viewstub.get_chain_selected_index())
self.assertEquals(0, self.viewstub.get_avail_selected_index())在验证View拿到的转换器链列表、转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引之前,先验证View是否收到avail_trans_not_specified的通知。
####### 6.4.2.1.4.1.2 Write the Code 1 ViewStub
tests.test_presenter.py ViewStub Class
def is_avail_trans_not_specified_notified(self): passis_avail_trans_not_specified_notified()是ViewStub的测试辅助方法,不是View的方法。
2 Presenter
presenter.py Presenter Class
def add_trans(self):
avail_selected_trans = self.view.get_avail_selected_trans()
is_avail_trans_not_specified = False
is_add_already_existed_in_chain_trans = False
if avail_selected_trans == None:
is_avail_trans_not_specified = True
self.view.notify_avail_trans_not_specified()
elif self.already_existed_in_chain(avail_selected_trans):
is_add_already_existed_in_chain_trans = True
self.view.notify_add_already_existed_in_chain_trans()
else:
self.chain_transes.append(avail_selected_trans)
self.update_chain_selected_index(avail_selected_trans, is_avail_trans_not_specified)
self.update_avail_selected_index(avail_selected_trans,
is_avail_trans_not_specified, is_add_already_existed_in_chain_trans)
self.view.present_chain_transes(self.chain_transes)
self.view.set_chain_selected_index(self.chain_selected_index)
self.view.set_avail_selected_index(self.avail_selected_index)
def update_chain_selected_index(self, avail_selected_trans, is_avail_trans_not_specified):
if is_avail_trans_not_specified:
return
self.chain_selected_index = self.chain_transes.index(avail_selected_trans)
def update_avail_selected_index(self, avail_selected_trans,
is_avail_trans_not_specified, is_add_already_existed_in_chain_trans):
if is_avail_trans_not_specified:
self.avail_selected_index = 0
return
selected_index = self.avail_transes.index(avail_selected_trans)
if is_add_already_existed_in_chain_trans:
self.avail_selected_index = selected_index
else:
self.avail_selected_index = \
0 if Presenter.is_last_index(selected_index, self.avail_transes) else selected_index + 13 View
view.py View (Interface) Class
def notify_avail_trans_not_specified(self): pass4 ViewStub
tests.test_presenter.py ViewStub Class
# Override
def notify_avail_trans_not_specified(self):
self.avail_trans_not_specified_notified = True
def is_avail_trans_not_specified_notified(self):
return self.avail_trans_not_specified_notified在is_avail_trans_not_specified_notified()方法中返回在notify_avail_trans_not_specified()方法中保存下来的avail_trans_not_specified_notified。
这还是打桩的方法,证明View.notify_avail_trans_not_specified()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。
####### 6.4.2.1.4.1.3 Refactor the Code Presenter类抽取trans_not_specified()静态方法。
presenter.py Presenter Class
def add_trans(self):
avail_selected_trans = self.view.get_avail_selected_trans()
is_avail_trans_not_specified = False
is_add_already_existed_in_chain_trans = False
if Presenter.trans_not_specified(avail_selected_trans):
is_avail_trans_not_specified = True
self.view.notify_avail_trans_not_specified()
elif self.already_existed_in_chain(avail_selected_trans):
is_add_already_existed_in_chain_trans = True
self.view.notify_add_already_existed_in_chain_trans()
else:
self.chain_transes.append(avail_selected_trans)
self.update_chain_selected_index(avail_selected_trans, is_avail_trans_not_specified)
self.update_avail_selected_index(avail_selected_trans,
is_avail_trans_not_specified, is_add_already_existed_in_chain_trans)
self.view.present_chain_transes(self.chain_transes)
self.view.set_chain_selected_index(self.chain_selected_index)
self.view.set_avail_selected_index(self.avail_selected_index)
@staticmethod
def trans_not_specified(trans):
return trans == None仔细观察add_trans()方法的代码,中间的一大段代码其实是在进行参数校验。围绕avail_selected_trans校验是否指定了可用转换器以及选中的可用转换器是否已存在于转换器链列表。
Wishful Thinking,希望这段代码长成下面的样子:
validating_result = self.validate_add_trans(avail_selected_trans)其中,validating_result包括校验是否成功以及校验失败原因这两部分内容。校验失败原因需要传递给update_chain_selected_index()和update_avail_selected_index()这两个更新索引的方法。
在validator.py中定义ValidatingResult类。
validator.py ValidatingResult Class
class ValidatingResult(object):
VRFR_AVAIL_TRANS_NOT_SPECIFIED = 'validating_result_failed_reason_avail_trans_not_specified'
VRFR_ADD_ALREADY_EXISTED_IN_CHAIN_TRANS = 'validating_result_failed_reason_add_already_existed_in_chain_trans'
def __init__(self, is_succeeded, failed_reason=None):
self.is_succeeded = is_succeeded
self.failed_reason = failed_reason
@staticmethod
def succeeded_result():
return ValidatingResult(True)
@staticmethod
def failed_result(failed_reason):
return ValidatingResult(False, failed_reason)presenter.py Presenter Class
from validator import ValidatingResult
def add_trans(self):
avail_selected_trans = self.view.get_avail_selected_trans()
validating_result = self.validate_add_trans(avail_selected_trans)
if validating_result.is_succeeded:
self.chain_transes.append(avail_selected_trans)
self.update_chain_selected_index(avail_selected_trans, validating_result.failed_reason)
self.update_avail_selected_index(avail_selected_trans, validating_result.failed_reason)
self.view.present_chain_transes(self.chain_transes)
self.view.set_chain_selected_index(self.chain_selected_index)
self.view.set_avail_selected_index(self.avail_selected_index)
def validate_add_trans(self, avail_selected_trans):
if Presenter.trans_not_specified(avail_selected_trans):
self.view.notify_avail_trans_not_specified()
return ValidatingResult.failed_result(ValidatingResult.VRFR_AVAIL_TRANS_NOT_SPECIFIED)
if self.already_existed_in_chain(avail_selected_trans):
self.view.notify_add_already_existed_in_chain_trans()
return ValidatingResult.failed_result(ValidatingResult.VRFR_ADD_ALREADY_EXISTED_IN_CHAIN_TRANS)
return ValidatingResult.succeeded_result()
def update_chain_selected_index(self, avail_selected_trans, validating_result_failed_reason):
if validating_result_failed_reason == ValidatingResult.VRFR_AVAIL_TRANS_NOT_SPECIFIED:
return
self.chain_selected_index = self.chain_transes.index(avail_selected_trans)
def update_avail_selected_index(self, avail_selected_trans, validating_result_failed_reason):
if validating_result_failed_reason == ValidatingResult.VRFR_AVAIL_TRANS_NOT_SPECIFIED:
self.avail_selected_index = 0
return
selected_index = self.avail_transes.index(avail_selected_trans)
if validating_result_failed_reason == ValidatingResult.VRFR_ADD_ALREADY_EXISTED_IN_CHAIN_TRANS:
self.avail_selected_index = selected_index
else:
self.avail_selected_index = \
0 if Presenter.is_last_index(selected_index, self.avail_transes) else selected_index + 1继续观察validate_add_trans()方法,围绕avail_selected_trans,校验是否指定了可用转换器跟校验选中的可用转换器是否已存在于转换器链列表的代码存在重复。这两段代码都遵循同一种模式,模式用伪码描述如下:
if failed_predication(param):
failed_action
return validating_failed_result(failed_reason)
return validating_succeeded_result用函数式编程重构这部分代码。在validator.py中新增Validator类,并新增validate_param()静态方法。
validator.py Validator Class
class Validator(object):
@staticmethod
def validate_param(param, param_failed_pred, param_failed_action, param_failed_reason):
if param_failed_pred(param):
param_failed_action()
return ValidatingResult.failed_result(param_failed_reason)
return ValidatingResult.succeeded_result()presenter.py Presenter Class
from validator import ValidatingResult, Validator
def validate_add_trans(self, avail_selected_trans):
validating_result = Validator.validate_param(avail_selected_trans,
Presenter.trans_not_specified,
self.view.notify_avail_trans_not_specified,
ValidatingResult.VRFR_AVAIL_TRANS_NOT_SPECIFIED)
if not validating_result.is_succeeded:
return validating_result
validating_result = Validator.validate_param(avail_selected_trans,
self.already_existed_in_chain,
self.view.notify_add_already_existed_in_chain_trans,
ValidatingResult.VRFR_ADD_ALREADY_EXISTED_IN_CHAIN_TRANS)
if not validating_result.is_succeeded:
return validating_result
return ValidatingResult.succeeded_result()看上去代码量似乎并没有减少多少,但层次已经提升了。但两段validate_param代码依然存在重复,继续重构,让代码更简单。
- 将param、param_failed_pred、param_failed_action和param_failed_reason封装为参数校验规则(ParamValidatingRule)。
- 多个参数的校验规则可以定义为一个列表,列表的每个元素都是一个ParamValidatingRule。
- 为了不影响已有代码,先将已有的Validator.validate_param()静态方法重命名为validate_param2。新增validate_param()静态方法,输入参数为param_validating_rule。
- 在Validator类中再提供一个validate(param_validating_rules)方法,顺序按照列表中的每一条规则校验参数。在这个过程中,有校验失败的就返回这个校验失败结果,整个过程都没有校验失败的就返回校验成功的结果。
- 在Presenter.validate_add_trans()方法中,只需要定义规则列表,直接返回Validator.validate(param_validating_rules)方法的执行结果即可。
- 删除不再有引用的Validator.validate_param2()方法。
validator.py Validator Class
class ParamValidatingRule(object):
def __init__(self, param, failed_pred, failed_action, failed_reason):
self.param = param
self.failed_pred = failed_pred
self.failed_action = failed_action
self.failed_reason = failed_reason
class Validator(object):
@staticmethod
def validate_param(param_validating_rule):
if param_validating_rule.failed_pred(param_validating_rule.param):
param_validating_rule.failed_action()
return ValidatingResult.failed_result(param_validating_rule.failed_reason)
return ValidatingResult.succeeded_result()
@staticmethod
def validate(param_validating_rules):
for rule in param_validating_rules:
validating_result = Validator.validate_param(rule)
if not validating_result.is_succeeded:
return validating_result
return ValidatingResult.succeeded_result()presenter.py Presenter Class
from validator import ValidatingResult, ParamValidatingRule, Validator
def validate_add_trans(self, avail_selected_trans):
param_validating_rules = [ParamValidatingRule(avail_selected_trans,
Presenter.trans_not_specified,
self.view.notify_avail_trans_not_specified,
ValidatingResult.VRFR_AVAIL_TRANS_NOT_SPECIFIED),
ParamValidatingRule(avail_selected_trans,
self.already_existed_in_chain,
self.view.notify_add_already_existed_in_chain_trans,
ValidatingResult.VRFR_ADD_ALREADY_EXISTED_IN_CHAIN_TRANS)]
return Validator.validate(param_validating_rules)现在Presenter.validate_add_trans()方法中的代码真的很简单了。
这里运用了“计算描述与执行分离”的设计思想。定义param_validating_rules就是在描述计算,而Validator.validate()就是在执行计算,是一个解释器。这种设计思想非常强大。它带来的好处是:一方面在描述层面可以很自由,可以任意组装,不受限于执行层面;另一方面在执行层面可以做很多优化,比如并发,或者解释成其他的结果,比如把param_validating_rules中的所有参数校验规则打印出来,只要再写一个解释器即可。需求变化的复杂性和代码修改的复杂性是线性关系,重要的是还不影响描述层面。
定义param_validating_rules的过程就是在用自定义的领域特定语言(DSL: Domain-Specific Language)定义一份规格说明(Specification)。这份规格说明是一个纯数据,是声明性的、抽象的。只要把这份规格说明传给解释器Validator解释执行,结果就出来了。这个领域特定语言最主要的语素是ParamValidatingRule(param, failed_pred, failed_action, failed_reason),当然还有List。它们是针对“字符串转换器的参数校验”这个特定问题领域抽象出来的计算模型。这是最本质的抽象,跟OO或函数式编程没有关系,因为它们都是实现层面的,不是设计层面的。事实上,我们现在使用函数式编程实现的,也可以改为用OO实现,但param_validating_rules描述不变。
我们可以使用实现语言(Python/Java/C等)内置的语言特性实现DSL,例如这里的ParamValidatingRule类和List,也可以自定义一套跟实现语言无关的小语言实现。
用DSL编写规格说明(Specification)也是在“编程”,只不过是在我们自己抽象出的针对“字符串转换器的参数校验”这个特定问题领域的计算模型上用DSL表达计算,即“编程”,跟我们用Python编程并没有本质区别。用Python编程不过是在Python语言提供的计算模型上用Python语言提供的DSL表达计算,即“编程”。
事实上马上就会看到并感受到,编写其他操作(例如Remove、Apply)的参数校验代码就是在这里抽象出来的计算模型上用DSL(ParamValidatingRule Class + List)在编程,编写Remove或Apply操作参数校验的规格说明(Specification)。
view.py ViewImpl Class
# Override
def notify_avail_trans_not_specified(self):
tkMessageBox.showinfo('Information', 'Specify an available transformer, please.')“tkMessageBox.showinfo('Information', ...)”存在重复,抽取show_info()静态方法。
view.py ViewImpl Class
@staticmethod
def show_info(info):
tkMessageBox.showinfo('Information', info)
# Override
def notify_add_already_existed_in_chain_trans(self):
ViewImpl.show_info('The transformer to be added has been already existed in the chain.')
# Override
def notify_avail_trans_not_specified(self):
ViewImpl.show_info('Specify an available transformer, please.')运行起来,如下图所示:

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.1 Add a Test tests.test_presenter.py TestPresenter Class
def test_remove_not_the_last_trans_when_chain_has_more_than_one_transes(self):
self.viewstub.set_avail_selected_trans(UPPER_TRANS)
self.presenter.add_trans()
self.viewstub.set_avail_selected_trans(LOWER_TRANS)
self.presenter.add_trans()
self.viewstub.set_avail_selected_trans(TRIM_PREFIX_SPACES_TRANS)
self.presenter.add_trans()
self.viewstub.set_chain_selected_trans(LOWER_TRANS)
self.presenter.remove_trans()
self.assertEquals([UPPER_TRANS, TRIM_PREFIX_SPACES_TRANS], self.viewstub.get_chain_transes())
self.assertEquals(1, self.viewstub.get_avail_selected_index())
self.assertEquals(1, self.viewstub.get_chain_selected_index())在View这一层验证View拿到的转换器链列表、可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引。
####### 6.4.2.2.1.1.2 Write the Code 1 ViewStub
tests.test_presenter.py ViewStub Class
def set_chain_selected_trans(self, trans): passset_chain_selected_trans()是ViewStub的测试辅助方法,不是View的方法。
2 Presenter
presenter.py Presenter Class
def remove_trans(self):
chain_selected_trans = self.view.get_chain_selected_trans()
chain_selected_index = self.chain_transes.index(chain_selected_trans)
self.chain_transes.remove(chain_selected_trans)
self.avail_selected_index = self.avail_transes.index(chain_selected_trans)
self.chain_selected_index = chain_selected_index
self.view.present_chain_transes(self.chain_transes)
self.view.set_avail_selected_index(self.avail_selected_index)
self.view.set_chain_selected_index(self.chain_selected_index)从View获取转换器链列表中选中的转换器。因为chain_transes是可变的,所以先计算这个转换器在转换器链列表中对应的索引,再从chain_transes中移除这个转换器。计算可用转换器列表选中转换器的索引并更新avail_selected_index。计算转换器链列表选中转换器的索引并更新chain_selected_index。将chain_transes推送给View显示为转换器链列表。告诉View可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引。
3 View
view.py View (Interface) Class
def get_chain_selected_trans(self): pass4 ViewStub
tests.test_presenter.py ViewStub Class
# Override
def get_chain_selected_trans(self):
return self.chain_selected_trans
def set_chain_selected_trans(self, trans):
self.chain_selected_trans = trans在get_chain_selected_trans()方法中返回在set_chain_selected_trans()方法中保存下来的chain_selected_trans。
这还是打桩的方法,证明View.get_chain_selected_trans()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。
####### 6.4.2.2.1.1.3 Refactor the Code Presenter类抽取update_chain_selected_index_for_remove()和update_avail_selected_index_for_remove()方法。
presenter.py Presenter Class
def remove_trans(self):
chain_selected_trans = self.view.get_chain_selected_trans()
self.update_chain_selected_index_for_remove(chain_selected_trans)
self.chain_transes.remove(chain_selected_trans)
self.update_avail_selected_index_for_remove(chain_selected_trans)
self.view.present_chain_transes(self.chain_transes)
self.view.set_avail_selected_index(self.avail_selected_index)
self.view.set_chain_selected_index(self.chain_selected_index)
def update_chain_selected_index_for_remove(self, chain_selected_trans):
self.chain_selected_index = self.chain_transes.index(chain_selected_trans)
def update_avail_selected_index_for_remove(self, chain_selected_trans):
self.avail_selected_index = self.avail_transes.index(chain_selected_trans)原来为Add操作服务的update_chain_selected_index()和update_avail_selected_index()方法的方法名都增加“_for_add”的后缀。
presenter.py Presenter Class
def add_trans(self):
avail_selected_trans = self.view.get_avail_selected_trans()
validating_result = self.validate_add_trans(avail_selected_trans)
if validating_result.is_succeeded:
self.chain_transes.append(avail_selected_trans)
self.update_chain_selected_index_for_add(avail_selected_trans, validating_result.failed_reason)
self.update_avail_selected_index_for_add(avail_selected_trans, validating_result.failed_reason)
self.view.present_chain_transes(self.chain_transes)
self.view.set_chain_selected_index(self.chain_selected_index)
self.view.set_avail_selected_index(self.avail_selected_index)
def update_chain_selected_index_for_add(self, avail_selected_trans, validating_result_failed_reason):
if validating_result_failed_reason == ValidatingResult.VRFR_AVAIL_TRANS_NOT_SPECIFIED:
return
self.chain_selected_index = self.chain_transes.index(avail_selected_trans)
def update_avail_selected_index_for_add(self, avail_selected_trans, validating_result_failed_reason):
if validating_result_failed_reason == ValidatingResult.VRFR_AVAIL_TRANS_NOT_SPECIFIED:
self.avail_selected_index = 0
return
selected_index = self.avail_transes.index(avail_selected_trans)
if validating_result_failed_reason == ValidatingResult.VRFR_ADD_ALREADY_EXISTED_IN_CHAIN_TRANS:
self.avail_selected_index = selected_index
else:
self.avail_selected_index = \
0 if Presenter.is_last_index(selected_index, self.avail_transes) else selected_index + 1view.py ViewImpl Class
def remove_transformer(self):
self.presenter.remove_trans()
# Override
def get_chain_selected_trans(self):
return ViewImpl.get_list_selected_item(self.lstchain)运行起来,如下图所示:

应该会注意到,可用转换器列表并没有高亮显示选中的转换器。同理,这还是Tkinter的控件焦点控制机制导致的,只有最后设置选中的控件才会高亮显示出选中状态。在Presenter.remove_trans()方法中先设置了可用转换器列表的选中,后设置了转换器链列表的选中,因此最终显示了转换器链列表的高亮选中状态。之所以设计为这样的顺序,是为了让用户能方便地连续移除转换器链列表中的转换器。
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.1 Add a Test tests.test_presenter.py TestPresenter Class
def test_remove_the_last_trans_when_chain_has_more_than_one_transes(self):
self.viewstub.set_avail_selected_trans(UPPER_TRANS)
self.presenter.add_trans()
self.viewstub.set_avail_selected_trans(LOWER_TRANS)
self.presenter.add_trans()
self.viewstub.set_avail_selected_trans(TRIM_PREFIX_SPACES_TRANS)
self.presenter.add_trans()
self.viewstub.set_chain_selected_trans(TRIM_PREFIX_SPACES_TRANS)
self.presenter.remove_trans()
self.assertEquals([UPPER_TRANS, LOWER_TRANS], self.viewstub.get_chain_transes())
self.assertEquals(2, self.viewstub.get_avail_selected_index())
self.assertEquals(0, self.viewstub.get_chain_selected_index())在View这一层验证View拿到的转换器链列表、可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引。
####### 6.4.2.2.2.1.2 Write the Code presenter.py Presenter Class
def update_chain_selected_index_for_remove(self, chain_selected_trans):
selected_index = self.chain_transes.index(chain_selected_trans)
self.chain_selected_index = \
0 if Presenter.is_last_index(selected_index, self.chain_transes) else selected_index####### 6.4.2.2.2.1.3 Refactor the Code 无。
当转换器链列表仅包含一个转换器时,在转换器链列表中选中这个转换器,点击Remove按钮,选中的转换器被移除出转换器链,转换器链列表为空,无选中项。可用转换器列表中的转换器条目不变,选中被移除的转换器。
####### 6.4.2.2.3.1.1 Add a Test tests.test_presenter.py TestPresenter Class
def test_remove_a_trans_when_chain_has_only_one_transes(self):
self.viewstub.set_avail_selected_trans(UPPER_TRANS)
self.presenter.add_trans()
self.viewstub.set_chain_selected_trans(UPPER_TRANS)
self.presenter.remove_trans()
self.assertEquals([], self.viewstub.get_chain_transes())
self.assertEquals(0, self.viewstub.get_avail_selected_index())
self.assertEquals(NONE_SELECTED_INDEX, self.viewstub.get_chain_selected_index())在View这一层验证View拿到的转换器链列表、可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引。
####### 6.4.2.2.3.1.2 Write the Code presenter.py Presenter Class
def update_chain_selected_index_for_remove(self, chain_selected_trans):
if len(self.chain_transes) == 1:
self.chain_selected_index = NONE_SELECTED_INDEX
return
selected_index = self.chain_transes.index(chain_selected_trans)
self.chain_selected_index = \
0 if Presenter.is_last_index(selected_index, self.chain_transes) else selected_index####### 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.1 Add a Test tests.test_presenter.py TestPresenter Class
def test_remove_trans_when_chain_is_not_empty_but_chain_trans_not_specified(self):
self.viewstub.set_avail_selected_trans(UPPER_TRANS)
self.presenter.add_trans()
self.viewstub.set_chain_selected_trans(None)
self.presenter.remove_trans()
self.assertTrue(self.viewstub.is_chain_trans_not_specified_notified())
self.assertEquals([UPPER_TRANS], self.viewstub.get_chain_transes())
self.assertEquals(1, self.viewstub.get_avail_selected_index())
self.assertEquals(0, self.viewstub.get_chain_selected_index())在验证View拿到的转换器链列表、可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引之前,先验证View是否收到chain_trans_not_specified的通知。
####### 6.4.2.2.4.1.2 Write the Code 1 ViewStub
tests.test_presenter.py ViewStub Class
def is_chain_trans_not_specified_notified(self): passis_chain_trans_not_specified_notified()是ViewStub的测试辅助方法,不是View的方法。
2 Validator
validator.py Validator Class
VRFR_CHAIN_TRANS_NOT_SPECIFIED = 'validating_result_failed_reason_chain_trans_not_specified'3 Presenter
presenter.py Presenter Class
def remove_trans(self):
chain_selected_trans = self.view.get_chain_selected_trans()
validating_result = self.validate_remove_trans(chain_selected_trans)
self.update_chain_selected_index_for_remove(chain_selected_trans, validating_result.failed_reason)
if validating_result.is_succeeded:
self.chain_transes.remove(chain_selected_trans)
self.update_avail_selected_index_for_remove(chain_selected_trans, validating_result.failed_reason)
self.view.present_chain_transes(self.chain_transes)
self.view.set_avail_selected_index(self.avail_selected_index)
self.view.set_chain_selected_index(self.chain_selected_index)
def validate_remove_trans(self, chain_selected_trans):
param_validating_rules = [ParamValidatingRule(chain_selected_trans,
Presenter.trans_not_specified,
self.view.notify_chain_trans_not_specified,
ValidatingResult.VRFR_CHAIN_TRANS_NOT_SPECIFIED)]
return Validator.validate(param_validating_rules)
def update_chain_selected_index_for_remove(self, chain_selected_trans, validating_result_failed_reason):
if validating_result_failed_reason == ValidatingResult.VRFR_CHAIN_TRANS_NOT_SPECIFIED:
self.chain_selected_index = 0
return
if len(self.chain_transes) == 1:
self.chain_selected_index = NONE_SELECTED_INDEX
return
selected_index = self.chain_transes.index(chain_selected_trans)
self.chain_selected_index = \
0 if Presenter.is_last_index(selected_index, self.chain_transes) else selected_index
def update_avail_selected_index_for_remove(self, chain_selected_trans, validating_result_failed_reason):
if validating_result_failed_reason == ValidatingResult.VRFR_CHAIN_TRANS_NOT_SPECIFIED:
return
self.avail_selected_index = self.avail_transes.index(chain_selected_trans)4 View
view.py View (Interface) Class
def notify_chain_trans_not_specified(self): pass5 ViewStub
tests.test_presenter.py ViewStub Class
# Override
def notify_chain_trans_not_specified(self):
self.chain_trans_not_specified_notified = True
def is_chain_trans_not_specified_notified(self):
return self.chain_trans_not_specified_notified在is_chain_trans_not_specified_notified()方法中返回在notify_chain_trans_not_specified()方法中保存下来的chain_trans_not_specified_notified。
这还是打桩的方法,证明View.notify_chain_trans_not_specified()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。
####### 6.4.2.2.4.1.3 Refactor the Code 无。
view.py ViewImpl Class
# Override
def notify_chain_trans_not_specified(self):
ViewImpl.show_info('Specify a transformer from the chain, please.')运行起来,如下图所示:

转换器链列表为空,无选中项,点击Remove按钮,提示“转换器链为空”。可用转换器列表中的转换器条目不变,选中第一个转换器。
####### 6.4.2.2.5.1.1 Add a Test tests.test_presenter.py TestPresenter Class
def test_remove_trans_when_chain_is_empty(self):
self.presenter.remove_trans()
self.assertTrue(self.viewstub.is_chain_empty_notified())
self.assertEquals([], self.viewstub.get_chain_transes())
self.assertEquals(0, self.viewstub.get_avail_selected_index())
self.assertEquals(NONE_SELECTED_INDEX, self.viewstub.get_chain_selected_index())在验证View拿到的转换器链列表、可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引之前,先验证View是否收到chain_empty的通知。
####### 6.4.2.2.5.1.2 Write the Code 1 ViewStub
tests.test_presenter.py ViewStub Class
def __init__(self):
self.chain_selected_trans = None
def is_chain_empty_notified(self): pass- 如果chain_selected_trans从来没有被赋过值,就直接返回,Python会报如下错误信息。所以在构造函数中先赋一个初值。
AttributeError: 'ViewStub' object has no attribute 'chain_selected_trans'
- is_chain_empty_notified()是ViewStub的测试辅助方法,不是View的方法。
2 Validator
validator.py Validator Class
VRFR_CHAIN_EMPTY = 'validating_result_failed_reason_chain_empty'3 Presenter
presenter.py Presenter Class
def validate_remove_trans(self, chain_selected_trans):
param_validating_rules = [ParamValidatingRule(self.chain_transes,
Presenter.empty_list,
self.view.notify_chain_empty,
ValidatingResult.VRFR_CHAIN_EMPTY),
ParamValidatingRule(chain_selected_trans,
Presenter.trans_not_specified,
self.view.notify_chain_trans_not_specified,
ValidatingResult.VRFR_CHAIN_TRANS_NOT_SPECIFIED)]
return Validator.validate(param_validating_rules)
def update_chain_selected_index_for_remove(self, chain_selected_trans, validating_result_failed_reason):
if validating_result_failed_reason == ValidatingResult.VRFR_CHAIN_EMPTY:
self.chain_selected_index = NONE_SELECTED_INDEX
return
if validating_result_failed_reason == ValidatingResult.VRFR_CHAIN_TRANS_NOT_SPECIFIED:
self.chain_selected_index = 0
return
if len(self.chain_transes) == 1:
self.chain_selected_index = NONE_SELECTED_INDEX
return
selected_index = self.chain_transes.index(chain_selected_trans)
self.chain_selected_index = \
0 if Presenter.is_last_index(selected_index, self.chain_transes) else selected_index
def update_avail_selected_index_for_remove(self, chain_selected_trans, validating_result_failed_reason):
if validating_result_failed_reason == ValidatingResult.VRFR_CHAIN_EMPTY:
self.avail_selected_index = 0
return
if validating_result_failed_reason == ValidatingResult.VRFR_CHAIN_TRANS_NOT_SPECIFIED:
return
self.avail_selected_index = self.avail_transes.index(chain_selected_trans)
@staticmethod
def empty_list(lst):
return lst == []可以看到,remove_trans()方法不用修改,只需要修改validate_remove_trans()方法中的param_validating_rules即可,但是要注意规则的顺序。在这里,校验转换器链是否为空要放在校验转换器链列表是否选中了转换器之前。update_chain_selected_index_for_remove()和update_avail_selected_index_for_remove()增加VRFR_CHAIN_EMPTY校验失败原因的处理分支即可。另外,新增empty_list()静态方法。
4 View
view.py View (Interface) Class
def notify_chain_empty(self): pass5 ViewStub
tests.test_presenter.py ViewStub Class
# Override
def notify_chain_empty(self):
self.chain_empty_notified = True
def is_chain_empty_notified(self):
return self.chain_empty_notified在is_chain_empty_notified()方法中返回在notify_chain_empty()方法中保存下来的chain_empty_notified。
这还是打桩的方法,证明View.notify_chain_empty()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。
####### 6.4.2.2.5.1.3 Refactor the Code 无。
view.py ViewImpl Class
# Override
def notify_chain_empty(self):
ViewImpl.show_info('Specify the transformer chain, please.')运行起来,如下图所示:

应该会注意到,虽然代码先设置了可用转换器列表的选中,后设置了转换器链列表的选中,但这次是可用转换器列表高亮显示选中的转换器。这是因为设置转换器链列表选中的转换器的索引是-1,也就是不选中,Tkinter没有设置转换器链列表的焦点,因此最终显示了可用转换器列表的高亮选中状态。
转换器链列表不为空,无论转换器链列表是否选中转换器,点击Remove All按钮,所有转换器均被移除出转换器链,转换器链列表为空,无选中项。可用转换器列表中的转换器条目不变,选中第一个转换器。
####### 6.4.2.3.1.1.1 Add a Test tests.test_presenter.py TestPresenter Class
def test_remove_all_transes_when_chain_is_not_empty(self):
self.viewstub.set_avail_selected_trans(UPPER_TRANS)
self.presenter.add_trans()
self.viewstub.set_avail_selected_trans(LOWER_TRANS)
self.presenter.add_trans()
self.presenter.remove_all_transes()
self.assertEquals([], self.viewstub.get_chain_transes())
self.assertEquals(0, self.viewstub.get_avail_selected_index())
self.assertEquals(NONE_SELECTED_INDEX, self.viewstub.get_chain_selected_index())在View这一层验证View拿到的转换器链列表、转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引。
####### 6.4.2.3.1.1.2 Write the Code presenter.py Presenter Class
def remove_all_transes(self):
del self.chain_transes[:]
self.chain_selected_index = NONE_SELECTED_INDEX
self.avail_selected_index = 0
self.view.present_chain_transes(self.chain_transes)
self.view.set_chain_selected_index(self.chain_selected_index)
self.view.set_avail_selected_index(self.avail_selected_index)清空chain_transes。更新转换器链列表选中转换器的索引chain_selected_index为NONE_SELECTED_INDEX。更新可用转换器列表选中转换器的索引avail_selected_index为0。将chain_transes推送给View显示为转换器链列表。告诉View转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引。
####### 6.4.2.3.1.1.3 Refactor the Code Presenter类抽取update_chain_selected_index_for_remove_all()和update_avail_selected_index_for_remove_all()方法。
presenter.py Presenter Class
def remove_all_transes(self):
del self.chain_transes[:]
self.update_chain_selected_index_for_remove_all()
self.update_avail_selected_index_for_remove_all()
self.view.present_chain_transes(self.chain_transes)
self.view.set_chain_selected_index(self.chain_selected_index)
self.view.set_avail_selected_index(self.avail_selected_index)
def update_chain_selected_index_for_remove_all(self):
self.chain_selected_index = NONE_SELECTED_INDEX
def update_avail_selected_index_for_remove_all(self):
self.avail_selected_index = 0view.py ViewImpl Class
def remove_all_transformers(self):
self.presenter.remove_all_transes()运行起来,如下图所示:

转换器链列表为空,无选中项,点击Remove All按钮,提示“请指定转换器链”。可用转换器列表中的转换器条目不变,选中之前选中的转换器。
####### 6.4.2.3.2.1.1 Add a Test tests.test_presenter.py TestPresenter Class
def test_remove_all_transes_when_chain_is_empty(self):
self.viewstub.set_avail_selected_trans(LOWER_TRANS)
self.presenter.add_trans()
self.viewstub.set_chain_selected_trans(LOWER_TRANS)
self.presenter.remove_trans()
self.presenter.remove_all_transes()
self.assertTrue(self.viewstub.is_chain_empty_notified())
self.assertEquals([], self.viewstub.get_chain_transes())
self.assertEquals(1, self.viewstub.get_avail_selected_index())
self.assertEquals(NONE_SELECTED_INDEX, self.viewstub.get_chain_selected_index())- 在验证View拿到的转换器链列表、可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引之前,先验证View是否收到chain_empty的通知。
- 之所以先add LOWER_TRANS、再remove LOWER_TRANS这样构建空转换器链,是为了让avail_selected_index不为0。上一个测试用例驱动出来的avail_selected_index为0。这样验证更充分。
####### 6.4.2.3.2.1.2 Write the Code presenter.py Presenter Class
def remove_all_transes(self):
validating_result = self.validate_remove_all_transes()
if validating_result.is_succeeded:
del self.chain_transes[:]
self.update_chain_selected_index_for_remove_all()
self.update_avail_selected_index_for_remove_all(validating_result.failed_reason)
self.view.present_chain_transes(self.chain_transes)
self.view.set_chain_selected_index(self.chain_selected_index)
self.view.set_avail_selected_index(self.avail_selected_index)
def validate_remove_all_transes(self):
param_validating_rules = [ParamValidatingRule(self.chain_transes,
Presenter.empty_list,
self.view.notify_chain_empty,
ValidatingResult.VRFR_CHAIN_EMPTY)]
return Validator.validate(param_validating_rules)
def update_avail_selected_index_for_remove_all(self, validating_result_failed_reason):
if validating_result_failed_reason == ValidatingResult.VRFR_CHAIN_EMPTY:
return
self.avail_selected_index = 0####### 6.4.2.3.2.1.3 Refactor the Code 无。
运行起来,如下图所示:

输入合法的源字符串,构建好非空的转换器链,点击Apply按钮,将转换器链中的转换器从上到下依次应用到源字符串上,得到最终的结果字符串。
tests.test_presenter.py TestPresenter Class
def test_apply_trans_chain(self):
self.viewstub.set_avail_selected_trans(UPPER_TRANS)
self.presenter.add_trans()
self.viewstub.set_source_str('Hello, world.')
self.presenter.apply_trans_chain()
self.assertEquals('HELLO, WORLD.', self.viewstub.get_result_str())在View这一层验证View拿到的转换结果字符串。
1 ViewStub
tests.test_presenter.py ViewStub Class
def set_source_str(self, s): pass
def get_result_str(self): passset_source_str()和get_result_str()都是ViewStub的测试辅助方法,不是View的方法。
2 Presenter
presenter.py Presenter Class
def apply_trans_chain(self):
self.view.present_result_str(self.businesslogic.transform(self.view.get_source_str(), self.chain_transes))在apply_trans_chain()方法中,从View获取源字符串,跟chain_transes一起传给BusinessLogic进行转换,并将转换后的结果字符串推送给View显示。
3 View
view.py View (Interface) Class
def get_source_str(self): pass
def present_result_str(self, s): pass4 ViewStub
tests.test_presenter.py ViewStub Class
# Override
def get_source_str(self):
return self.source_str
def set_source_str(self, s):
self.source_str = s
# Override
def present_result_str(self, s):
self.result_str = s
def get_result_str(self):
return self.result_str- 在get_source_str()方法中返回在set_source_str()方法中保存下来的source_str。
- 在get_result_str()方法中返回在present_result_str()方法中保存下来的result_str。
这些都是打桩的方法,证明View.get_source_str()和View.present_result_str()这些方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。
5 BusinessLogic
businesslogic.py BusinessLogic (Interface) Class
def transform(self, source_str, transes): pass6 BusinessLogicStub
tests.test_presenter.py BusinessLogicStub Class
def transform(self, source_str, transes):
return 'HELLO, WORLD.'这里容易出现两类问题:
- 把转换逻辑写在了Presenter中。这是不对的。因为根据前面的设计,真正的转换逻辑应该在BusinessLogicImpl中实现。但此时也不应该在BusinessLogicImpl中写转换逻辑,因为现在是在测试驱动开发Presenter,所以只需要在BusinessLogicStub中打桩就可以了,不需要实现真正的转换逻辑。而且转换逻辑有可能在分布式的服务端实现。在TDD开发时,这些耦合都要解除,否则无法测试。
- 把转换逻辑写在了BusinessLogicStub中。这也是不对的。因为真正的转换逻辑是产品代码,不应该写在测试代码中。
无。
BusinessLogicImpl提供UPPER_TRANS转换器的转换逻辑。
tests.test_businesslogicimpl.py TestBusinessLogicImpl Class
def test_transform_upper(self):
impl = BusinessLogicImpl()
self.assertEquals('HELLO, WORLD.', impl.transform('Hello, world.', [UPPER_TRANS]))businesslogic.py BusinessLogicImpl Class
# Override
def transform(self, source_str, transes):
return source_str.upper()无。
view.py ViewImpl Class
def apply_transformer_chain(self):
self.presenter.apply_trans_chain()
# Override
def get_source_str(self):
return ViewImpl.get_entry_txt(self.txtsourcestr)
# Override
def present_result_str(self, s):
ViewImpl.set_entry_txt(self.resultstr, s)运行起来,如下图所示:

构建好非空的转换器链,但未输入源字符串或源字符串为空,点击Apply按钮,提示“请输入源字符串”,焦点定位到源字符串文本框。
tests.test_presenter.py TestPresenter Class
def test_apply_trans_chain_when_source_str_is_empty(self):
self.viewstub.set_avail_selected_trans(UPPER_TRANS)
self.presenter.add_trans()
self.viewstub.set_source_str('')
self.presenter.apply_trans_chain()
self.assertTrue(self.viewstub.is_source_str_empty_notified())
self.assertEquals('', self.viewstub.get_result_str())在View这一层先验证View是否收到source_str_empty的通知,再验证View拿到的转换结果字符串是否为''。
1 ViewStub
tests.test_presenter.py ViewStub Class
def is_source_str_empty_notified(self): passis_source_str_empty_notified()是ViewStub的测试辅助方法,不是View的方法。
2 Validator
validator.py Validator Class
VRFR_SOURCE_STR_EMPTY = 'validating_result_failed_reason_source_str_empty'3 Presenter
presenter.py Presenter Class
def apply_trans_chain(self):
source_str = self.view.get_source_str()
validating_result = self.validate_apply_trans_chain(source_str)
result_str = self.businesslogic.transform(source_str, self.chain_transes) \
if validating_result.is_succeeded else ''
self.view.present_result_str(result_str)
def validate_apply_trans_chain(self, source_str):
param_validating_rules = [ParamValidatingRule(source_str,
Presenter.empty_str,
self.view.notify_source_str_empty,
ValidatingResult.VRFR_SOURCE_STR_EMPTY)]
return Validator.validate(param_validating_rules)
@staticmethod
def empty_str(s):
return s == ''4 View
view.py View (Interface) Class
def notify_source_str_empty(self): pass5 ViewStub
tests.test_presenter.py ViewStub Class
# Override
def notify_source_str_empty(self):
self.source_str_empty_notified = True
def is_source_str_empty_notified(self):
return self.source_str_empty_notified在is_source_str_empty_notified()方法中返回在notify_source_str_empty()方法中保存下来的source_str_empty_notified。
这还是打桩的方法,证明View.notify_source_str_empty()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。
无。
view.py ViewImpl Class
# Override
def notify_source_str_empty(self):
ViewImpl.show_info('Specify the source string, please.')
self.txtsourcestr.focus_set()运行起来,如下图所示:

构建好非空的转换器链,但输入了非法的源字符串,例如包含中文字符等,点击Apply按钮,提示“请输入合法的源字符串”,焦点定位到源字符串文本框,并全选高亮当前文本。
tests.test_presenter.py TestPresenter Class
# -*- coding: UTF-8 -*-
def test_apply_trans_chain_when_source_str_is_illegal(self):
self.viewstub.set_avail_selected_trans(UPPER_TRANS)
self.presenter.add_trans()
self.viewstub.set_source_str('a中文b')
self.presenter.apply_trans_chain()
self.assertTrue(self.viewstub.is_source_str_illegal_notified())
self.assertEquals('', self.viewstub.get_result_str())因为测试代码中有中文,需要在tests.test_presenter.py文件的开头加上编码格式的声明,否则Python会报如下错误信息:
SyntaxError: Non-ASCII character '\xe4' in file ...
这样测试运行环境的编码格式也是UTF-8。
在View这一层先验证View是否收到source_str_illegal的通知,再验证View拿到的转换结果字符串是否为''。
1 ViewStub
tests.test_presenter.py ViewStub Class
def is_source_str_illegal_notified(self): passis_source_str_illegal_notified()是ViewStub的测试辅助方法,不是View的方法。
2 Validator
validator.py Validator Class
VRFR_SOURCE_STR_ILLEGAL = 'validating_result_failed_reason_source_str_illegal'3 Presenter
presenter.py Presenter Class
import re
def validate_apply_trans_chain(self, source_str):
param_validating_rules = [ParamValidatingRule(source_str,
Presenter.empty_str,
self.view.notify_source_str_empty,
ValidatingResult.VRFR_SOURCE_STR_EMPTY),
ParamValidatingRule(source_str,
Presenter.illegal_source_str,
self.view.notify_source_str_illegal,
ValidatingResult.VRFR_SOURCE_STR_ILLEGAL)]
return Validator.validate(param_validating_rules)
@staticmethod
def illegal_source_str(s):
pattern = re.compile(u'[\u4e00-\u9fa5]+')
return pattern.search(s.decode('utf-8')) is not NonePython中字符串的表示是用Unicode编码。所以在做编码转换时,通常要以Unicode作为中间编码。decode的作用是将其他编码的字符串转换成Unicode编码,比如s.decode('utf-8'),表示将UTF-8编码的字符串转换成Unicode编码。
另外,encode的作用是将Unicode编码的字符串转换成其他编码格式的字符串,比如s.encode('utf-8'),表示将Unicode编码的字符串转换成UTF-8编码的字符串。
这里的正则表达式的模式中使用了Unicode,那么要匹配的字符串也必须调用decode()转换为Unicode编码,否则肯定会不匹配。
4 View
view.py View (Interface) Class
def notify_source_str_illegal(self): pass5 ViewStub
tests.test_presenter.py ViewStub Class
# Override
def notify_source_str_illegal(self):
self.source_str_illegal_notified = True
def is_source_str_illegal_notified(self):
return self.source_str_illegal_notified在is_source_str_illegal_notified()方法中返回在notify_source_str_illegal()方法中保存下来的source_str_illegal_notified。
这还是打桩的方法,证明View.notify_source_str_illegal()方法确实被Presenter调用过,以此验证Presenter和View之间的交互是否正确。
无。
view.py ViewImpl Class
# Override
def notify_source_str_illegal(self):
ViewImpl.show_info('Specify the legal source string, please.')
self.txtsourcestr.focus_set()
self.txtsourcestr.select_range(0, END)运行ViewImpl,Python会报如下错误信息:
UnicodeEncodeError: 'ascii' codec can't encode characters in position 1-2: ordinal not in range(128)
产品运行环境中,Python2的默认编码格式是ASCII,会用ASCII编码格式处理字符,当处理的字符不在ASCII范围之内,例如中文字符,就会抛出异常“ordinal not in range(128)”。解决方案是修改默认的编码格式,代码如下:
import sys
reload(sys)
sys.setdefaultencoding('utf-8')另外,Python3的默认编码格式是UTF-8,就不需要这三行代码了。
view.py ViewImpl Class
if __name__ == '__main__':
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
viewimpl = ViewImpl()
businesslogicimpl = BusinessLogicImpl()
presenter = Presenter(viewimpl, businesslogicimpl)
presenter.init()
viewimpl.centershow(560, 400)
viewimpl.root.mainloop()运行起来,如下图所示:

输入合法的源字符串,但转换器链为空,点击Apply按钮,提示“请指定转换器链”。可用转换器列表中的转换器条目不变,选中第一个转换器。
tests.test_presenter.py TestPresenter Class
def test_apply_trans_chain_when_chain_is_empty(self):
self.viewstub.set_avail_selected_trans(LOWER_TRANS)
self.presenter.add_trans()
self.viewstub.set_chain_selected_trans(LOWER_TRANS)
self.presenter.remove_trans()
self.viewstub.set_source_str('Hello, world.')
self.presenter.apply_trans_chain()
self.assertTrue(self.viewstub.is_chain_empty_notified())
self.assertEquals('', self.viewstub.get_result_str())
self.assertEquals(0, self.viewstub.get_avail_selected_index())- 在View这一层先验证View是否收到chain_empty的通知,再验证View拿到的转换结果字符串是否为'',以及可用转换器列表选中转换器的索引。
- 之所以先add LOWER_TRANS、再remove LOWER_TRANS这样构建空转换器链,是为了让apply_trans_chain()之前的avail_selected_index不为0。这样验证更充分。
presenter.py Presenter Class
def apply_trans_chain(self):
source_str = self.view.get_source_str()
validating_result = self.validate_apply_trans_chain(source_str)
result_str = self.businesslogic.transform(source_str, self.chain_transes) \
if validating_result.is_succeeded else ''
self.update_avail_selected_index_for_apply(validating_result.failed_reason)
self.view.present_result_str(result_str)
self.view.set_avail_selected_index(self.avail_selected_index)
def validate_apply_trans_chain(self, source_str):
param_validating_rules = [ParamValidatingRule(source_str,
Presenter.empty_str,
self.view.notify_source_str_empty,
ValidatingResult.VRFR_SOURCE_STR_EMPTY),
ParamValidatingRule(source_str,
Presenter.illegal_source_str,
self.view.notify_source_str_illegal,
ValidatingResult.VRFR_SOURCE_STR_ILLEGAL),
ParamValidatingRule(self.chain_transes,
Presenter.empty_list,
self.view.notify_chain_empty,
ValidatingResult.VRFR_CHAIN_EMPTY)]
return Validator.validate(param_validating_rules)
def update_avail_selected_index_for_apply(self, validating_result_failed_reason):
if validating_result_failed_reason == ValidatingResult.VRFR_CHAIN_EMPTY:
self.avail_selected_index = 0无。
运行起来,如下图所示:

1. test_get_all_transes (Done)
2. test_transform_upper (Done)
3. test_transform_lower
4. test_transform_trimprefixspaces
5. test_transform
字符串转小写。
tests.test_businesslogicimpl.py TestBusinessLogicImpl Class
def test_transform_lower(self):
impl = BusinessLogicImpl()
self.assertEquals('hello, world.', impl.transform('Hello, world.', [LOWER_TRANS]))businesslogic.py BusinessLogicImpl Class
# Override
def transform(self, source_str, transes):
result_str = source_str
trans = transes[0]
if trans == UPPER_TRANS:
result_str = source_str.upper()
elif trans == LOWER_TRANS:
result_str = source_str.lower()
return result_str重构测试代码。TestBusinessLogicImpl类抽取setUp()方法。
tests.test_businesslogicimpl.py TestBusinessLogicImpl Class
def setUp(self):
self.impl = BusinessLogicImpl()
def test_get_all_transes(self):
self.assertEquals([UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS], self.impl.get_all_transes())
def test_transform_upper(self):
self.assertEquals('HELLO, WORLD.', self.impl.transform('Hello, world.', [UPPER_TRANS]))
def test_transform_lower(self):
self.assertEquals('hello, world.', self.impl.transform('Hello, world.', [LOWER_TRANS]))字符串去除前缀空格。
tests.test_businesslogicimpl.py TestBusinessLogicImpl Class
def test_transform_trimprefixspaces(self):
self.assertEquals('Hello, world. ', self.impl.transform(' Hello, world. ', [TRIM_PREFIX_SPACES_TRANS]))
self.assertEquals('', self.impl.transform(' ', [TRIM_PREFIX_SPACES_TRANS]))
self.assertEquals('Hello, world. ', self.impl.transform('Hello, world. ', [TRIM_PREFIX_SPACES_TRANS]))businesslogic.py BusinessLogicImpl Class
# Override
def transform(self, source_str, transes):
result_str = source_str
trans = transes[0]
if trans == UPPER_TRANS:
result_str = source_str.upper()
elif trans == LOWER_TRANS:
result_str = source_str.lower()
elif trans == TRIM_PREFIX_SPACES_TRANS:
result_str = source_str.lstrip()
return result_str1 定义upper()、lower()和trim_prefix_spaces()三个静态转换方法。
businesslogic.py BusinessLogicImpl Class
@staticmethod
def upper(s):
return s.upper()
@staticmethod
def lower(s):
return s.lower()
@staticmethod
def trim_prefix_spaces(s):
return s.lstrip()2 定义一个trans和转换方法的映射。
businesslogic.py BusinessLogicImpl Class
TRANS_FUNC_MAP = {
UPPER_TRANS:lambda s: BusinessLogicImpl.upper(s),
LOWER_TRANS:lambda s: BusinessLogicImpl.lower(s),
TRIM_PREFIX_SPACES_TRANS:lambda s: BusinessLogicImpl.trim_prefix_spaces(s)
}
# Override
def transform(self, source_str, transes):
return BusinessLogicImpl.TRANS_FUNC_MAP[transes[0]](source_str) 3 get_all_transes()方法中返回映射的keys。
businesslogic.py BusinessLogicImpl Class
# Override
def get_all_transes(self):
return BusinessLogicImpl.TRANS_FUNC_MAP.keys()注意Map.keys()方法返回的顺序是Map的内建顺序,不是put的顺序。
根据转换器链进行转换。
tests.test_businesslogicimpl.py TestBusinessLogicImpl Class
def test_transform(self):
self.assertEquals("hello, world. ",
self.impl.transform(' Hello, world. ',
[UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS]))businesslogic.py BusinessLogicImpl Class
# Override
def transform(self, source_str, transes):
result_str = source_str
for trans in transes:
result_str = BusinessLogicImpl.TRANS_FUNC_MAP[trans](result_str)
return result_str用reduce()重构transform()方法。
businesslogic.py BusinessLogicImpl Class
# Override
def transform(self, source_str, transes):
def _acc_transform(acc, trans):
return BusinessLogicImpl.TRANS_FUNC_MAP[trans](acc)
return reduce(_acc_transform, transes, source_str)从全局、整体上审视并重构代码。这一点很重要,站在全局整体的视角,而不再陷入局部细节。
重构主要集中在Presenter。
仔细观察,validate_add_trans()、validate_remove_trans()、validate_remove_all_transes()和validate_apply_trans_chain()方法中的最后一行代码都是相同的,不同的是构建param_validating_rules的代码。
以validate_add_trans()为例。新增validate()静态方法。新增build_param_validating_rules_for_add()方法,删除validate_add_trans()方法,add_trans改为调用validate()静态方法。
presenter.py Presenter Class
def add_trans(self):
avail_selected_trans = self.view.get_avail_selected_trans()
validating_result = Presenter.validate(self.build_param_validating_rules_for_add(avail_selected_trans))
if validating_result.is_succeeded:
self.chain_transes.append(avail_selected_trans)
self.update_chain_selected_index_for_add(avail_selected_trans, validating_result.failed_reason)
self.update_avail_selected_index_for_add(avail_selected_trans, validating_result.failed_reason)
self.view.present_chain_transes(self.chain_transes)
self.view.set_chain_selected_index(self.chain_selected_index)
self.view.set_avail_selected_index(self.avail_selected_index)
def build_param_validating_rules_for_add(self, avail_selected_trans):
return [ParamValidatingRule(avail_selected_trans,
Presenter.trans_not_specified,
self.view.notify_avail_trans_not_specified,
ValidatingResult.VRFR_AVAIL_TRANS_NOT_SPECIFIED),
ParamValidatingRule(avail_selected_trans,
self.already_existed_in_chain,
self.view.notify_add_already_existed_in_chain_trans,
ValidatingResult.VRFR_ADD_ALREADY_EXISTED_IN_CHAIN_TRANS)]
@staticmethod
def validate(param_validating_rules):
return Validator.validate(param_validating_rules)同理,validate_remove_trans()、validate_remove_all_transes()和validate_apply_trans_chain()做类似重构。
presenter.py Presenter Class
def remove_trans(self):
chain_selected_trans = self.view.get_chain_selected_trans()
validating_result = Presenter.validate(self.build_param_validating_rules_for_remove(chain_selected_trans))
self.update_chain_selected_index_for_remove(chain_selected_trans, validating_result.failed_reason)
if validating_result.is_succeeded:
self.chain_transes.remove(chain_selected_trans)
self.update_avail_selected_index_for_remove(chain_selected_trans, validating_result.failed_reason)
self.view.present_chain_transes(self.chain_transes)
self.view.set_avail_selected_index(self.avail_selected_index)
self.view.set_chain_selected_index(self.chain_selected_index)
def build_param_validating_rules_for_remove(self, chain_selected_trans):
return [ParamValidatingRule(self.chain_transes,
Presenter.empty_list,
self.view.notify_chain_empty,
ValidatingResult.VRFR_CHAIN_EMPTY),
ParamValidatingRule(chain_selected_trans,
Presenter.trans_not_specified,
self.view.notify_chain_trans_not_specified,
ValidatingResult.VRFR_CHAIN_TRANS_NOT_SPECIFIED)]
def remove_all_transes(self):
validating_result = Presenter.validate(self.build_param_validating_rules_for_remove_all())
if validating_result.is_succeeded:
del self.chain_transes[:]
self.update_chain_selected_index_for_remove_all()
self.update_avail_selected_index_for_remove_all(validating_result.failed_reason)
self.view.present_chain_transes(self.chain_transes)
self.view.set_chain_selected_index(self.chain_selected_index)
self.view.set_avail_selected_index(self.avail_selected_index)
def build_param_validating_rules_for_remove_all(self):
return [ParamValidatingRule(self.chain_transes,
Presenter.empty_list,
self.view.notify_chain_empty,
ValidatingResult.VRFR_CHAIN_EMPTY)]
def apply_trans_chain(self):
source_str = self.view.get_source_str()
validating_result = Presenter.validate(self.build_param_validating_rules_for_apply(source_str))
result_str = self.businesslogic.transform(source_str, self.chain_transes) \
if validating_result.is_succeeded else ''
self.update_avail_selected_index_for_apply(validating_result.failed_reason)
self.view.present_result_str(result_str)
self.view.set_avail_selected_index(self.avail_selected_index)
def build_param_validating_rules_for_apply(self, source_str):
return [ParamValidatingRule(source_str,
Presenter.empty_str,
self.view.notify_source_str_empty,
ValidatingResult.VRFR_SOURCE_STR_EMPTY),
ParamValidatingRule(source_str,
Presenter.illegal_source_str,
self.view.notify_source_str_illegal,
ValidatingResult.VRFR_SOURCE_STR_ILLEGAL),
ParamValidatingRule(self.chain_transes,
Presenter.empty_list,
self.view.notify_chain_empty,
ValidatingResult.VRFR_CHAIN_EMPTY)]仔细观察,add_trans()、remove_trans()、remove_all_transes()和apply_trans_chain()方法中的代码都遵循同一种模式,模式用伪码描述如下:
view_data = collect_view_data()
validating_result = validate(build_param_validating_rules(view_data))
update_presenter_data(view_data, validating_result)
present_view_data()用函数式编程重构这部分代码。在presenter.py中新增OperData类,并新增_oper_trans()方法。
presenter.py OperData Class
def _oper_trans(self, oper_data):
view_data = oper_data.collect_view_data()
validating_result = Presenter.validate(oper_data.build_param_validating_rules(view_data))
oper_data.update_presenter_data(view_data, validating_result)
oper_data.present_view_data()
class OperData(object):
def __init__(self, collect_view_data, build_param_validating_rules,
update_presenter_data, present_view_data):
self.collect_view_data = collect_view_data
self.build_param_validating_rules = build_param_validating_rules
self.update_presenter_data = update_presenter_data
self.present_view_data = present_view_data修改add_trans()方法,新增collect_view_data_for_add()、update_presenter_data_for_add()和present_view_data_for_add()方法。build_param_validating_rules_for_add()已有。
presenter.py Presenter Class
def add_trans(self):
self._oper_trans(OperData(self.collect_view_data_for_add,
self.build_param_validating_rules_for_add,
self.update_presenter_data_for_add,
self.present_view_data_for_add))
def collect_view_data_for_add(self):
return self.view.get_avail_selected_trans()
def build_param_validating_rules_for_add(self, avail_selected_trans):
return [ParamValidatingRule(avail_selected_trans,
Presenter.trans_not_specified,
self.view.notify_avail_trans_not_specified,
ValidatingResult.VRFR_AVAIL_TRANS_NOT_SPECIFIED),
ParamValidatingRule(avail_selected_trans,
self.already_existed_in_chain,
self.view.notify_add_already_existed_in_chain_trans,
ValidatingResult.VRFR_ADD_ALREADY_EXISTED_IN_CHAIN_TRANS)]
def update_presenter_data_for_add(self, avail_selected_trans, validating_result):
if validating_result.is_succeeded:
self.chain_transes.append(avail_selected_trans)
self.update_chain_selected_index_for_add(avail_selected_trans, validating_result.failed_reason)
self.update_avail_selected_index_for_add(avail_selected_trans, validating_result.failed_reason)
def present_view_data_for_add(self):
self.view.present_chain_transes(self.chain_transes)
self.view.set_chain_selected_index(self.chain_selected_index)
self.view.set_avail_selected_index(self.avail_selected_index)修改remove_trans()方法,新增collect_view_data_for_remove()、update_presenter_data_for_remove()和present_view_data_for_remove()方法。build_param_validating_rules_for_remove()已有。
presenter.py Presenter Class
def remove_trans(self):
self._oper_trans(OperData(self.collect_view_data_for_remove,
self.build_param_validating_rules_for_remove,
self.update_presenter_data_for_remove,
self.present_view_data_for_remove))
def collect_view_data_for_remove(self):
return self.view.get_chain_selected_trans()
def build_param_validating_rules_for_remove(self, chain_selected_trans):
return [ParamValidatingRule(self.chain_transes,
Presenter.empty_list,
self.view.notify_chain_empty,
ValidatingResult.VRFR_CHAIN_EMPTY),
ParamValidatingRule(chain_selected_trans,
Presenter.trans_not_specified,
self.view.notify_chain_trans_not_specified,
ValidatingResult.VRFR_CHAIN_TRANS_NOT_SPECIFIED)]
def update_presenter_data_for_remove(self, chain_selected_trans, validating_result):
self.update_chain_selected_index_for_remove(chain_selected_trans, validating_result.failed_reason)
if validating_result.is_succeeded:
self.chain_transes.remove(chain_selected_trans)
self.update_avail_selected_index_for_remove(chain_selected_trans, validating_result.failed_reason)
def present_view_data_for_remove(self):
self.view.present_chain_transes(self.chain_transes)
self.view.set_avail_selected_index(self.avail_selected_index)
self.view.set_chain_selected_index(self.chain_selected_index)对于remove_all_transes()方法,由于不需要collect_view_data,所以_oper_trans()方法要先改一下。再修改remove_all_transes()方法,新增update_presenter_data_for_remove_all()和present_view_data_for_remove_all()方法。build_param_validating_rules_for_remove_all()已有。
presenter.py Presenter Class
def _oper_trans(self, oper_data):
if oper_data.collect_view_data:
view_data = oper_data.collect_view_data()
validating_result = Presenter.validate(oper_data.build_param_validating_rules(view_data))
oper_data.update_presenter_data(view_data, validating_result)
else:
validating_result = Presenter.validate(oper_data.build_param_validating_rules())
oper_data.update_presenter_data(validating_result)
oper_data.present_view_data()
def remove_all_transes(self):
self._oper_trans(OperData(None,
self.build_param_validating_rules_for_remove_all,
self.update_presenter_data_for_remove_all,
self.present_view_data_for_remove_all))
def build_param_validating_rules_for_remove_all(self):
return [ParamValidatingRule(self.chain_transes,
Presenter.empty_list,
self.view.notify_chain_empty,
ValidatingResult.VRFR_CHAIN_EMPTY)]
def update_presenter_data_for_remove_all(self, validating_result):
if validating_result.is_succeeded:
del self.chain_transes[:]
self.update_chain_selected_index_for_remove_all()
self.update_avail_selected_index_for_remove_all(validating_result.failed_reason)
def present_view_data_for_remove_all(self):
self.view.present_chain_transes(self.chain_transes)
self.view.set_chain_selected_index(self.chain_selected_index)
self.view.set_avail_selected_index(self.avail_selected_index)修改apply_trans_chain()方法,新增collect_view_data_for_apply()、update_presenter_data_for_apply()和present_view_data_for_apply()方法。build_param_validating_rules_for_apply()已有。
为了让update_presenter_data_for_apply()方法更名副其实,新增result_str成员变量,并初始化为None。
presenter.py Presenter Class
def __init__(self, view, businesslogic):
self.view = view
self.businesslogic = businesslogic
self.view.set_presenter(self)
self.avail_selected_index = 0
self.chain_selected_index = NONE_SELECTED_INDEX
self.avail_transes = None
self.chain_transes = []
self.result_str = None
def apply_trans_chain(self):
self._oper_trans(OperData(self.collect_view_data_for_apply,
self.build_param_validating_rules_for_apply,
self.update_presenter_data_for_apply,
self.present_view_data_for_apply))
def collect_view_data_for_apply(self):
return self.view.get_source_str()
def build_param_validating_rules_for_apply(self, source_str):
return [ParamValidatingRule(source_str,
Presenter.empty_str,
self.view.notify_source_str_empty,
ValidatingResult.VRFR_SOURCE_STR_EMPTY),
ParamValidatingRule(source_str,
Presenter.illegal_source_str,
self.view.notify_source_str_illegal,
ValidatingResult.VRFR_SOURCE_STR_ILLEGAL),
ParamValidatingRule(self.chain_transes,
Presenter.empty_list,
self.view.notify_chain_empty,
ValidatingResult.VRFR_CHAIN_EMPTY)]
def update_presenter_data_for_apply(self, source_str, validating_result):
self.result_str = self.businesslogic.transform(source_str, self.chain_transes) \
if validating_result.is_succeeded else ''
self.update_avail_selected_index_for_apply(validating_result.failed_reason)
def present_view_data_for_apply(self):
self.view.present_result_str(self.result_str)
self.view.set_avail_selected_index(self.avail_selected_index)这里依然运用了“计算描述与执行分离”的设计思想。定义OperData就是在描述计算,而Presenter._oper_trans就是在执行计算,是一个解释器。
定义OperData的过程就是在用自定义的领域特定语言(DSL: Domain-Specific Language)定义一份规格说明(Specification)。这份规格说明是一个纯数据,是声明性的、抽象的。只要把这份规格说明传给解释器Presenter._oper_trans解释执行,结果就出来了。这个领域特定语言最主要的语素是OperData(collect_view_data, build_param_validating_rules, update_presenter_data, present_view_data)。它们是针对“操作转换器”这个特定问题领域抽象出来的计算模型。这是最本质的抽象,跟OO或函数式编程没有关系,因为它们都是实现层面的,不是设计层面的。事实上,我们现在使用函数式编程实现的,也可以改为用OO实现,但OperData描述不变。
我们可以使用实现语言(Python/Java/C等)内置的语言特性实现DSL,例如这里的OperData,也可以自定义一套跟实现语言无关的小语言实现。
用DSL编写规格说明(Specification)也是在“编程”,只不过是在我们自己抽象出的针对“操作转换器”这个特定问题领域的计算模型上用DSL表达计算,即“编程”,跟我们用Python编程并没有本质区别。用Python编程不过是在Python语言提供的计算模型上用Python语言提供的DSL表达计算,即“编程”。
编写add_trans、remove_trans、remove_all_transes、apply_trans_chain的代码就是在这里抽象出来的计算模型上用DSL(OperData)在编程,编写这些操作的规格说明(Specification)。
请学员先总结一下。
- TDD三步军规、小步。
- 测试:
- 始终选择当前最有价值的测试用例;
- 测试三段式:Arrange、Act、Assert;
- 不要忘了测试代码也需要重构。
- 每次修改代码,都运行一下测试用例,注意红绿绿的节奏。
- 写代码应做到:
- 简洁明了:概念定义清晰、准确;简明清晰地表达语义;
- 表达到位:层次分明、表现意图;跟问题领域的概念、规则相吻合;
- 安全可靠:状态、时序管理得当;具有必要的测试安全网络。
- 规范
- 目录结构;
- 模块名;
- 类名;
- 方法/函数名。
-
【必选】字符串转换器支持“Reverse”转换。举个例子:
"Hello, world." -- Reverse --> ".dlrow , olleH" -
【必选】新增“Add All”(添加所有转换器)功能。
字符串转换器支持“Reverse”转换。
主要修改BusinessLogicImpl。
代码路径:dummy.reverse
tests.test_businesslogicimpl.py TestBusinessLogicImpl Class
def test_get_all_transes(self):
self.assertEquals([UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS, REVERSE_TRANS],
self.impl.get_all_transes())
def test_transform_reverse(self):
self.assertEquals(' .dlrow ,olleH ', self.impl.transform(' Hello, world. ', [REVERSE_TRANS]))注意test_get_all_transes()中期望值的顺序是Map的内建顺序,不是put的顺序。
trans.py
REVERSE_TRANS = 'Reverse'businesslogic.py BusinessLogicImpl Class
TRANS_FUNC_MAP = {
UPPER_TRANS:lambda s: BusinessLogicImpl.upper(s),
LOWER_TRANS:lambda s: BusinessLogicImpl.lower(s),
TRIM_PREFIX_SPACES_TRANS:lambda s: BusinessLogicImpl.trim_prefix_spaces(s),
REVERSE_TRANS:lambda s: BusinessLogicImpl.reverse(s)
}
@staticmethod
def reverse(s):
return s[::-1]运行起来,如下图所示:

新增“Add All”(添加所有转换器)功能。
代码路径:dummy.addall
无论转换器链列表是否为空,无论可用转换器列表是否选中转换器,点击Add All按钮,所有转换器均被添加到转换器链列表。转换器链列表和可用转换器列表完全一样。可用转换器列表中的转换器条目不变,选中第一个转换器。转换器链列表选中最后一个转换器。
tests.test_presenter.py TestPresenter Class
def test_add_all_transes(self):
self.presenter.add_all_transes()
self.assertEquals([UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS], self.viewstub.get_chain_transes())
self.assertEquals(0, self.viewstub.get_avail_selected_index())
self.assertEquals(2, self.viewstub.get_chain_selected_index())在View这一层验证View拿到的转换器链列表、可用转换器列表选中转换器的索引和转换器链列表选中转换器的索引。
presenter.py Presenter Class
def _oper_trans(self, oper_data):
if oper_data.collect_view_data and oper_data.build_param_validating_rules:
view_data = oper_data.collect_view_data()
validating_result = Presenter.validate(oper_data.build_param_validating_rules(view_data))
oper_data.update_presenter_data(view_data, validating_result)
elif not oper_data.collect_view_data and oper_data.build_param_validating_rules:
validating_result = Presenter.validate(oper_data.build_param_validating_rules())
oper_data.update_presenter_data(validating_result)
else:
oper_data.update_presenter_data()
oper_data.present_view_data()Presenter准备新增的add_all_transes()方法不需要collect_view_data和build_param_validating_rules,所以_oper_trans()方法要先改一下。再新增add_all_transes()、update_presenter_data_for_add_all()、update_chain_selected_index_for_add_all()、update_avail_selected_index_for_add_all()和present_view_data_for_add_all()方法。
presenter.py Presenter Class
def add_all_transes(self):
self._oper_trans(OperData(None,
None,
self.update_presenter_data_for_add_all,
self.present_view_data_for_add_all))
def update_presenter_data_for_add_all(self):
del self.chain_transes[:]
self.chain_transes.extend(self.avail_transes)
self.update_chain_selected_index_for_add_all()
self.update_avail_selected_index_for_add_all()
def update_chain_selected_index_for_add_all(self):
self.chain_selected_index = len(self.chain_transes) - 1
def update_avail_selected_index_for_add_all(self):
self.avail_selected_index = 0
def present_view_data_for_add_all(self):
self.view.present_chain_transes(self.chain_transes)
self.view.set_avail_selected_index(self.avail_selected_index)
self.view.set_chain_selected_index(self.chain_selected_index)在add_all_transes()方法中,先清空chain_transes,再把整个avail_transes添加到chain_transes。更新转换器链列表选中转换器的索引chain_selected_index为最后一个索引。更新可用转换器列表选中转换器的索引avail_selected_index为0。将chain_transes推送给View显示为转换器链列表。告诉View可用转换器列表选中选中转换器的索引和转换器链列表选中转换器的索引。
Presenter类抽取clear_chain_transes()方法。
presenter.py Presenter Class
def update_presenter_data_for_remove_all(self, validating_result):
if validating_result.is_succeeded:
self.clear_chain_transes()
self.update_chain_selected_index_for_remove_all()
self.update_avail_selected_index_for_remove_all(validating_result.failed_reason)
def update_presenter_data_for_add_all(self):
self.clear_chain_transes()
self.chain_transes.extend(self.avail_transes)
self.update_chain_selected_index_for_add_all()
self.update_avail_selected_index_for_add_all()
def clear_chain_transes(self):
del self.chain_transes[:]view.py ViewImpl Class
def init_operbtnsframe(self, parent):
operbtnsframe = Frame(parent)
operbtnsframe.pack(fill=BOTH)
topemptyframe = Frame(operbtnsframe)
topemptyframe.pack(side=TOP)
Label(topemptyframe, text="").pack()
bottomemptyframe = Frame(operbtnsframe)
bottomemptyframe.pack(side=BOTTOM)
Label(bottomemptyframe, text="").pack()
Button(operbtnsframe, text='Add >>', width=10, command=self.add_transformer).pack(pady=10)
Button(operbtnsframe, text='Remove <<', width=10, command=self.remove_transformer).pack(pady=10)
Button(operbtnsframe, text='Remove All', width=10, command=self.remove_all_transformers).pack(pady=10)
Button(operbtnsframe, text='Add All', width=10, command=self.add_all_transformers).pack(pady=10)
def add_all_transformers(self):
self.presenter.add_all_transes()运行起来,如下图所示:
