201803 TDD Coding Practice String Transformer Reloaded in Python - xiaoxianfaye/Courses GitHub Wiki
- 1 Review
- 2 Stop to Think
-
3 Implementation
- 3.1 Init
-
3.2 Build Transformer Chain
-
3.2.1 Add a Transformer
- 3.2.1.1 Normal Business Process 1: Add the transformer which is not the last
- 3.2.1.2 Normal Business Process 2: Add the transformer which is the last
- 3.2.1.3 Abnormal Business Process 1: Add a transformer which has been already existed in the chain
- 3.2.1.4 Abnormal Business Process 2: Add a transformer but none of the available transformers is specified
-
3.2.2 Remove a Transformer
- 3.2.2.1 Normal Business Process 1: Remove the transformer which is not the last when the chain has more than one transformers
- 3.2.2.2 Normal Business Process 2: Remove the transformer which is the last when the chain has more than one transformers
- 3.2.2.3 Normal Business Process 3: Remove the transformer when the chain has only one transformer
- 3.2.2.4 Abnormal Business Process 1: Remove a transformer when the chain is not empty but none of the transformers in the chain is specified
- 3.2.2.5 Abnormal Business Process 2: Remove a transformer when the chain is empty
- 3.2.3 Remove All Transformers
-
3.2.1 Add a Transformer
-
3.3 Apply Transformer Chain
- 3.3.1 Normal Business Process 1: Apply the transformer chain
- 3.3.2 Abnormal Business Process 1: Apply the transformer chain but the source string is empty
- 3.3.3 Abnormal Business Process 2: Apply the transformer chain but the source string is illegal
- 3.3.4 Abnormal Business Process 3: Apply the transformer chain but the transformer chain is empty
- 3.4 BusinessLogicImpl (In Addition to Upper, Other Transformers and Transformer Chain)
- 3.5 _oper_trans
- 4 Improvement
- 5 My Homework
实现一个基于GUI的应用——字符串转换器。
用户在“Source String(源字符串)”文本框中输入一个英文字符串,然后选择所需要的转换器,点击“Apply(应用)”按钮,系统会按照用户所选择的转换器链对输入字符串进行转换,并将结果显示在“Result String(结果字符串)”文本框中。
目前有3个转换器:Upper(转换为大写)、Lower(转换为小写)和TrimPrefixSpaces(去除前缀空格)。
举个例子,用户输入“ hello, world. ”,并依次添加了Upper和TrimPrefixSpaces转换器到转换器链中,点击Apply按钮后的界面可参考下图:
在上一次课程中,我们根据《测试总线势在必行——设计支持自动化验收测试的架构》这篇论文中介绍的“Bypassing the UI”的理论方法(下面第1张图)设计出一个分层系统(Layered System,下面第2张图)。
- View(视图层):Layout、Accept User Input / Output to User
- Presenter(表示层):Client-side Presentation、Validation、Calculation
- Business Logic(业务逻辑层):Business Logic
全局类图如下图所示:
我们停下来思考一下。
请学员思考目前的设计与实现是否存在问题?
Presenter和View的交互接口太过细致,关注的是交互实现细节,而不是真正的交互逻辑。
另外,每个参数的每种校验失败情况都对应了View接口中的一个通知方法,也偏向于“交互实现细节”,而不是真正的“交互逻辑”。
这会导致:
- View接口的频繁变更。
- Presenter测试代码与产品代码的频繁变更。
- Presenter实现与界面中使用的控件细节密切相关。
而View接口、Presenter测试代码与产品代码本应是系统中相对稳定的部分,不应也不宜频繁变更,更不应与界面使用的控件细节密切相关。
关于第3点,再详细说明一下。
在之前的实现里,使用了Python自带的Tkinter库实现界面控件和布局,正是由于Tkinter的控件焦点控制机制导致几个细节问题需要特别处理。而其他的界面控件库的焦点控制机制可能跟Tkinter不一样,几个细节问题的特别处理也未必一样甚至可能根本就不需要。
- 由于只有最后设置选中的控件才会高亮显示出选中状态,所以在Presenter产品代码中要特别注意View.set_chain_selected_index()和View.set_avail_selected_index()方法的调用顺序。
如果使用Java Swing库,View.set_chain_selected_index()和View.set_avail_selected_index()方法的先后调用顺序无关紧要。
- 可用转换器列表和转换器链列表可能处于未选中任何转换器的状态,因此必须要增加“添加转换器时未指定可用转换器”和“移除转换器时未指定转换器链列表中的转换器”等异常业务流程的处理。
如果使用Java Swing库,可以使得可用转换器列表和转换器链列表在程序运行期间永远不会处于未选中任何转换器的状态,因此根本就不需要处理以上异常流程。
有以下几个改进方向:
-
Presenter应关注交互逻辑而不是交互实现细节,才能使得View接口、Presenter测试代码及其产品代码都不会因为界面展现细节的变化而频繁变更,保持相对稳定。
-
既然之前的Presenter有实现细节与控件相关,那就把这些实现细节放到ViewImpl中,使得Presenter实现与界面中使用的控件细节无关。
-
定义统一的参数校验失败通知方法。
为此,Presenter与View的交互接口采用指令式,将计算描述与执行分离。Presenter计算好交互逻辑所需要的所有数据,封装为指令,传递给View,View根据指令控制界面控件展现细节。
这么实现的话,View不再是纯哑(Dummy)的,View中会有一些实现细节逻辑。需要注意,即便View中包含一些实现细节逻辑,也只能是和界面控件细节相关的逻辑,不能是业务逻辑,而且要尽量简单,简单到一眼就能看出有没有问题。
按照上一次课程中总结的业务流程,还是从交付角度,以一个业务流程为单位,将所有业务流程按端到端(View-Presenter-BusinessLogic)逐一重新实现。
1 Init
2 Build Transformer Chain
2.1 Add a Transformer
2.1.1 Normal Business Process 1: Add the transformer which is not the last
2.1.2 Normal Business Process 2: Add the transformer which is the last
2.1.3 Abnormal Business Process 1: Add a transformer which has been already existed in the chain
2.1.4 Abnormal Business Process 2: Add a transformer but none of the available transformers is specified
2.2 Remove a Transformer
2.2.1 Normal Business Process 1: Remove the transformer which is not the last when the chain has more than one transformers
2.2.2 Normal Business Process 2: Remove the transformer which is the last when the chain has more than one transformers
2.2.3 Normal Business Process 3: Remove the transformer when the chain has only one transformer
2.2.4 Abnormal Business Process 1: Remove a transformer when the chain is not empty but none of the transformers in the chain is specified
2.2.5 Abnormal Business Process 2: Remove a transformer when the chain is empty
2.3 Remove All Transformers
2.3.1 Normal Business Process 1: Remove all transformers when the chain is not empty
2.3.2 Abnormal Business Process 1: Remove all transformers when the chain is empty
3 Apply Transformer Chain
3.1 Normal Business Process 1: Apply the transformer chain
3.2 Abnormal Business Process 1: Apply the transformer chain but the source string is empty
3.3 Abnormal Business Process 2: Apply the transformer chain but the source string is illegal
3.4 Abnormal Business Process 3: Apply the transformer chain but the transformer chain is empty
上一次课程中提到了只包括界面控件和布局但不包括业务流程的ViewImpl类,这次依然在这个类的基础上实现完整的ViewImpl。
代码详见view(Only Layout).py。
另外,上一次课程中介绍过的重构细节、运行结果均不再详细展示。
代码路径:instruction.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
from interaction import *
from trans import *
class TestPresenter(unittest.TestCase):
def setUp(self):
self.viewstub = ViewStub()
self.businesslogicstub = BusinessLogicStub()
self.presenter = Presenter(self.viewstub, self.businesslogicstub)
self.presenter.init()
def test_init(self):
expected = {AVAIL_TRANSES:[UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS],
AVAIL_SELECTED_INDEX:0}
self.assertEquals(expected, self.viewstub.get_on_init_data())
在View这一层验证View拿到的on_init_data是否符合预期。on_init_data包括可用转换器列表和可用转换器列表选中转换器的索引。
1 新增ViewStub类。
tests.test_presenter.py ViewStub Class
from view import View
class ViewStub(View):
def get_on_init_data(self): pass
2 ViewStub继承自View,新增View(接口)类。
view.py View (Interface) Class
class View(object): pass
3 新增BusinessLogicStub类。
tests.test_presenter.py BusinessLogicStub Class
from businesslogic import BusinessLogic
class BusinessLogicStub(BusinessLogic): pass
4 BusinessLogicStub继承自BusinessLogic,新增BusinessLogic(接口)类。
businesslogic.py BusinessLogic (Interface) Class
class BusinessLogic(object): pass
5 定义View与Presenter交互用的常量。
interaction.py
AVAIL_TRANSES = 'avail_transes'
AVAIL_SELECTED_INDEX = 'avail_selected_index'
6 定义TRANS常量。
trans.py
UPPER_TRANS = 'Upper'
LOWER_TRANS = 'Lower'
TRIM_PREFIX_SPACES_TRANS = 'TrimPrefixSpaces'
7 新增Presenter类。
presenter.py Presenter Class
from interaction import *
class Presenter(object):
def __init__(self, view, businesslogic):
self.view = view
self.businesslogic = businesslogic
self.avail_selected_index = 0
def init(self):
self.view.on_init({AVAIL_TRANSES:self.businesslogic.get_all_transes(),
AVAIL_SELECTED_INDEX:self.avail_selected_index})
在Presenter.init()方法中,将从businesslogic获得的所有转换器和可用转换器列表选中转换器的索引封装为一个映射类型的数据,通过调用View.on_init()方法传递给View。
8 View类新增on_init()方法。
view.py View (Interface) Class
def on_init(self, data): pass
9 ViewStub类实现View新增方法。
tests.test_presenter.py ViewStub Class
# Override
def on_init(self, data):
self.on_init_data = data
def get_on_init_data(self):
return self.on_init_data
10 BusinessLogic类新增get_all_transes()方法。
businesslogic.py BusinessLogic Class
def get_all_transes(self): pass
11 BusinessLogicStub类实现BusinessLogic新增方法。
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 TestBusinessLogicImpl
tests.test_businesslogicimpl.py TestBusinessLogicImpl Class
import unittest
from businesslogic import BusinessLogicImpl
from trans import *
class TestBusinessLogicImpl(unittest.TestCase):
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())
新增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
from interaction import *
class ViewImpl(View):
...
# Override
def on_init(self, data):
ViewImpl.set_list_data(self.lstavail, data[AVAIL_TRANSES])
ViewImpl.set_list_selected_index(self.lstavail, data[AVAIL_SELECTED_INDEX])
在view.py的“main”里构造产品环境中的presenter,并调用presenter.init()方法完成初始化。
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按钮,选中的转换器被添加到转换器链列表末尾,转换器链列表选中新添加的转换器。可用转换器列表中的转换器条目不变,选中之前选中的转换器的下一个转换器。
tests.test_presenter.py TestPresenter Class
def test_add_not_the_last_trans(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:UPPER_TRANS})
self.presenter.add_trans()
expected = {CHAIN_TRANSES:[UPPER_TRANS],
CHAIN_SELECTED_INDEX:0,
AVAIL_SELECTED_INDEX:1}
self.assertEquals(expected, self.viewstub.get_on_add_trans_data())
在View这一层验证View拿到的on_add_trans_data是否符合预期。on_add_trans_data包括转换器链列表、转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引。
1 ViewStub
tests.test_presenter.py ViewStub Class
def set_add_trans_data(self, data): pass
def get_on_add_trans_data(self): pass
2 定义View与Presenter交互用的常量。
interaction.py
AVAIL_SELECTED_TRANS = 'avail_selected_trans'
CHAIN_TRANSES = 'chain_transes'
CHAIN_SELECTED_INDEX = 'chain_selected_index'
3 Presenter
presenter.py Presenter Class
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.on_init({AVAIL_TRANSES:self.avail_transes,
AVAIL_SELECTED_INDEX:self.avail_selected_index})
def add_trans(self):
avail_selected_trans = self.view.collect_add_trans_data()[AVAIL_SELECTED_TRANS]
self.chain_transes.append(avail_selected_trans)
self.update_chain_selected_index_for_add(avail_selected_trans)
self.update_avail_selected_index_for_add(avail_selected_trans)
self.view.on_add_trans({CHAIN_TRANSES:self.chain_transes,
CHAIN_SELECTED_INDEX:self.chain_selected_index,
AVAIL_SELECTED_INDEX:self.avail_selected_index})
def update_chain_selected_index_for_add(self, avail_selected_trans):
self.chain_selected_index = self.chain_transes.index(avail_selected_trans)
def update_avail_selected_index_for_add(self, avail_selected_trans):
self.avail_selected_index = self.avail_transes.index(avail_selected_trans) + 1
这次将NONE_SELECTED_INDEX定义在interaction.py中,值为-1,表示无选中索引。
interaction.py
NONE_SELECTED_INDEX = -1
在Presenter.add_trans()方法中,调用View.collect_add_trans_data()方法从View获取add_trans所需数据,这个映射类型的数据包括选中的可用转换器。将该转换器添加到chain_transes末尾。在Presenter.update_chain_selected_index_for_add()方法中计算转换器链列表选中转换器的索引并更新chain_selected_index。在Presenter.update_avail_selected_index_for_add()方法中计算可用转换器列表选中转换器的索引并更新avail_selected_index。将转换器链列表、转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引封装为一个映射类型的数据,通过调用View.on_add_trans()方法传递给View。
4 View
view.py View (Interface) Class
def collect_add_trans_data(self): pass
def on_add_trans(self, data): pass
5 ViewStub
tests.test_presenter.py ViewStub Class
# Override
def collect_add_trans_data(self):
return self.add_trans_data
def set_add_trans_data(self, data):
self.add_trans_data = data
# Override
def on_add_trans(self, data):
self.on_add_trans_data = data
def get_on_add_trans_data(self):
return self.on_add_trans_data
view.py ViewImpl Class
# Override
def collect_add_trans_data(self):
return {AVAIL_SELECTED_TRANS:ViewImpl.get_list_selected_item(self.lstavail)}
# Override
def on_add_trans(self, data):
ViewImpl.set_list_data(self.lstchain, data[CHAIN_TRANSES])
ViewImpl.set_list_selected_index(self.lstchain, data[CHAIN_SELECTED_INDEX])
ViewImpl.set_list_selected_index(self.lstavail, data[AVAIL_SELECTED_INDEX])
由于Tkinter的控件焦点控制机制,只有最后设置选中的控件才会高亮显示出选中状态,所以ViewImpl.on_add_trans()方法中要注意先设置转换器链列表选中转换器的索引、后设置可用转换器列表选中转换器的索引。
view.py View (Interface) Class
def set_presenter(self, presenter): pass
view.py ViewImpl Class
def add_transformer(self):
self.presenter.add_trans()
# Override
def set_presenter(self, presenter):
self.presenter = presenter
tests.test_presenter.py ViewStub Class
# Override
def set_presenter(self, presenter): pass
presenter.py Presenter Class
def __init__(self, view, businesslogic):
self.view = view
self.businesslogic = businesslogic
self.view.set_presenter(self)
...
在可用转换器列表中选中最后一个转换器,点击Add按钮,选中的转换器被添加到转换器链列表末尾,转换器链列表选中新添加的转换器。可用转换器列表中的转换器条目不变,选中第一个转换器。
tests.test_presenter.py TestPresenter Class
def test_add_the_last_trans(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:TRIM_PREFIX_SPACES_TRANS})
self.presenter.add_trans()
expected = {CHAIN_TRANSES:[TRIM_PREFIX_SPACES_TRANS],
CHAIN_SELECTED_INDEX:0,
AVAIL_SELECTED_INDEX:0}
self.assertEquals(expected, self.viewstub.get_on_add_trans_data())
presenter.py Presenter Class
def update_avail_selected_index_for_add(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) - 1
在可用转换器列表中选中一个在转换器链列表中已存在的转换器,点击Add按钮,提示“待添加的转换器在转换器链中已存在”。转换器链列表中的转换器条目不变,选中已存在的转换器。可用转换器列表中的转换器条目不变,选中之前选中的转换器。
tests.test_presenter.py TestPresenter Class
def test_add_already_existed_in_chain_trans(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:UPPER_TRANS})
self.presenter.add_trans()
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:LOWER_TRANS})
self.presenter.add_trans()
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:LOWER_TRANS})
self.presenter.add_trans()
self.assertEquals(
{VALIDATING_FAILED_REASON:ValidatingResult.VRFR_ADD_ALREADY_EXISTED_IN_CHAIN_TRANS},
self.viewstub.get_on_validating_failed_data())
expected = {CHAIN_TRANSES:[UPPER_TRANS, LOWER_TRANS],
CHAIN_SELECTED_INDEX:1,
AVAIL_SELECTED_INDEX:1}
self.assertEquals(expected, self.viewstub.get_on_add_trans_data())
在验证View拿到的on_add_trans_data是否符合预期之前,先验证View是否收到参数校验失败原因为add_already_existed_in_chain_trans的通知。
关于参数校验,整体上依然采用之前的设计,但由于定义统一的参数校验失败通知方法,细节跟之前略有不同。
1 定义View与Presenter交互用的常量。
interaction.py
VALIDATING_FAILED_REASON = 'validating_failed_reason'
2 ValidatingResult
validator.py ValidatingResult Class
class ValidatingResult(object):
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)
跟之前的ValidatingResult没有区别。
3 ParamValidatingRule
validator.py ParamValidatingRule Class
class ParamValidatingRule(object):
def __init__(self, param, failed_pred, failed_reason):
self.param = param
self.failed_pred = failed_pred
self.failed_reason = failed_reason
跟之前的ParamValidatingRule相比,少了failed_action。由于定义统一的参数校验失败通知方法,所以不再需要各自的failed_action。
4 Validator
validator.py Validator Class
class Validator(object):
@staticmethod
def validate_param(param_validating_rule):
if param_validating_rule.failed_pred(param_validating_rule.param):
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()
跟之前的Validator相比,在Validator.validate_param()方法中,如果failed_pred为真,不再调用failed_action。也是因为定义统一的参数校验失败通知方法。
5 ViewStub
tests.test_presenter.py ViewStub Class
def get_on_validating_failed_data(self): pass
6 Presenter
presenter.py Presenter Class
from validator import ValidatingResult, ParamValidatingRule, Validator
def add_trans(self):
avail_selected_trans = self.view.collect_add_trans_data()[AVAIL_SELECTED_TRANS]
validating_result = self.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)
self.update_avail_selected_index_for_add(avail_selected_trans, validating_result.failed_reason)
self.view.on_add_trans({CHAIN_TRANSES:self.chain_transes,
CHAIN_SELECTED_INDEX:self.chain_selected_index,
AVAIL_SELECTED_INDEX:self.avail_selected_index})
def build_param_validating_rules_for_add(self, avail_selected_trans):
return [ParamValidatingRule(avail_selected_trans,
self.already_existed_in_chain,
ValidatingResult.VRFR_ADD_ALREADY_EXISTED_IN_CHAIN_TRANS)]
def update_avail_selected_index_for_add(self, avail_selected_trans, validating_result_failed_reason):
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
def validate(self, param_validating_rules):
validating_result = Validator.validate(param_validating_rules)
if not validating_result.is_succeeded:
self.view.on_validating_failed({VALIDATING_FAILED_REASON:validating_result.failed_reason})
return validating_result
def already_existed_in_chain(self, trans):
return trans in self.chain_transes
- Presenter.validate()方法根据param_validating_rules进行参数校验。之所以在Presenter中再定义一个validate方法,而不是直接修改Validator.validate()方法,是因为不想让Validator和View打交道,增加不必要的耦合。
- View.on_validating_failed()方法是统一的参数校验失败通知方法,当有参数校验失败时,通过调用这个方法将参数校验失败原因传递给View。
7 ViewStub
tests.test_presenter.py ViewStub Class
# Override
def on_validating_failed(self, data):
self.on_validating_failed_data = data
def get_on_validating_failed_data(self):
return self.on_validating_failed_data
view.py ViewImpl Class
import tkMessageBox
from validator import ValidatingResult
class ViewImpl(object):
VRFR_TIP_MAP = {
ValidatingResult.VRFR_ADD_ALREADY_EXISTED_IN_CHAIN_TRANS:
'The transformer to be added has been already existed in the chain.'
}
# Override
def on_validating_failed(self, data):
tkMessageBox.showinfo('Information', ViewImpl.VRFR_TIP_MAP[data[VALIDATING_FAILED_REASON]])
在ViewImpl类中定义一个参数校验失败原因与提示信息的映射。
3.2.1.4 Abnormal Business Process 2: Add a transformer but none of the available transformers is specified
在可用转换器列表中未选中任何转换器,点击Add按钮,提示“请在可用转换器中指定一个转换器”。转换器链列表中的转换器条目不变,选中之前已经选中的转换器。可用转换器列表中的转换器条目不变,选中第一个转换器。
tests.test_presenter.py TestPresenter Class
def test_add_trans_but_avail_trans_not_specified(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:UPPER_TRANS})
self.presenter.add_trans()
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:LOWER_TRANS})
self.presenter.add_trans()
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:None})
self.presenter.add_trans()
self.assertEquals(
{VALIDATING_FAILED_REASON:ValidatingResult.VRFR_AVAIL_TRANS_NOT_SPECIFIED},
self.viewstub.get_on_validating_failed_data())
expected = {CHAIN_TRANSES:[UPPER_TRANS, LOWER_TRANS],
CHAIN_SELECTED_INDEX:1,
AVAIL_SELECTED_INDEX:0}
self.assertEquals(expected, self.viewstub.get_on_add_trans_data())
1 ValidatingResult
validator.py ValidatingResult Class
VRFR_AVAIL_TRANS_NOT_SPECIFIED = 'validating_result_failed_reason_avail_trans_not_specified'
2 Presenter
presenter.py Presenter Class
def add_trans(self):
avail_selected_trans = self.view.collect_add_trans_data()[AVAIL_SELECTED_TRANS]
validating_result = self.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.on_add_trans({CHAIN_TRANSES:self.chain_transes,
CHAIN_SELECTED_INDEX:self.chain_selected_index,
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,
ValidatingResult.VRFR_AVAIL_TRANS_NOT_SPECIFIED),
ParamValidatingRule(avail_selected_trans,
self.already_existed_in_chain,
ValidatingResult.VRFR_ADD_ALREADY_EXISTED_IN_CHAIN_TRANS)]
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 + 1
@staticmethod
def trans_not_specified(trans):
return trans == None
view.py ViewImpl Class
VRFR_TIP_MAP = {
ValidatingResult.VRFR_AVAIL_TRANS_NOT_SPECIFIED:
'Specify an available transformer, please.',
ValidatingResult.VRFR_ADD_ALREADY_EXISTED_IN_CHAIN_TRANS:
'The transformer to be added has been already existed in the chain.'
}
3.2.2.1 Normal Business Process 1: Remove the transformer which is not the last when the chain has more than one transformers
当转换器链列表包含多于一个的转换器时,在转换器链列表中选中不是最后的一个转换器,点击Remove按钮,选中的转换器被移除出转换器链。可用转换器列表中的转换器条目不变,选中被移除的转换器。转换器链列表选中之前选中的转换器的下一个转换器。
tests.test_presenter.py TestPresenter Class
def test_remove_not_the_last_trans_when_chain_has_more_than_one_transes(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:UPPER_TRANS})
self.presenter.add_trans()
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:LOWER_TRANS})
self.presenter.add_trans()
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:TRIM_PREFIX_SPACES_TRANS})
self.presenter.add_trans()
self.viewstub.set_remove_trans_data({CHAIN_SELECTED_TRANS:LOWER_TRANS})
self.presenter.remove_trans()
expected = {CHAIN_TRANSES:[UPPER_TRANS, TRIM_PREFIX_SPACES_TRANS],
AVAIL_SELECTED_INDEX:1,
CHAIN_SELECTED_INDEX:1}
self.assertEquals(expected, self.viewstub.get_on_remove_trans_data())
1 ViewStub
tests.test_presenter.py ViewStub Class
def set_remove_trans_data(self, data): pass
def get_on_remove_trans_data(self): pass
2 定义View与Presenter交互用的常量。
interaction.py
CHAIN_SELECTED_TRANS = 'chain_selected_trans'
3 Presenter
presenter.py Presenter Class
def remove_trans(self):
chain_selected_trans = self.view.collect_remove_trans_data()[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.on_remove_trans({CHAIN_TRANSES:self.chain_transes,
AVAIL_SELECTED_INDEX:self.avail_selected_index,
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)
4 View
view.py View (Interface) Class
def collect_remove_trans_data(self): pass
def on_remove_trans(self, data): pass
5 ViewStub
tests.test_presenter.py ViewStub Class
# Override
def collect_remove_trans_data(self):
return self.remove_trans_data
def set_remove_trans_data(self, data):
self.remove_trans_data = data
# Override
def on_remove_trans(self, data):
self.on_remove_trans_data = data
def get_on_remove_trans_data(self):
return self.on_remove_trans_data
view.py ViewImpl Class
def remove_transformer(self):
self.presenter.remove_trans()
# Override
def collect_remove_trans_data(self):
return {CHAIN_SELECTED_TRANS:ViewImpl.get_list_selected_item(self.lstchain)}
# Override
def on_remove_trans(self, data):
ViewImpl.set_list_data(self.lstchain, data[CHAIN_TRANSES])
ViewImpl.set_list_selected_index(self.lstavail, data[AVAIL_SELECTED_INDEX])
ViewImpl.set_list_selected_index(self.lstchain, data[CHAIN_SELECTED_INDEX])
3.2.2.2 Normal Business Process 2: Remove the transformer which is the last when the chain has more than one transformers
当转换器链列表包含多于一个的转换器时,在转换器链列表中选中最后一个转换器,点击Remove按钮,选中的转换器被移除出转换器链。可用转换器列表中的转换器条目不变,选中被移除的转换器。转换器链列表选中第一个转换器。
tests.test_presenter.py TestPresenter Class
def test_remove_the_last_trans_when_chain_has_more_than_one_transes(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:UPPER_TRANS})
self.presenter.add_trans()
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:LOWER_TRANS})
self.presenter.add_trans()
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:TRIM_PREFIX_SPACES_TRANS})
self.presenter.add_trans()
self.viewstub.set_remove_trans_data({CHAIN_SELECTED_TRANS:TRIM_PREFIX_SPACES_TRANS})
self.presenter.remove_trans()
expected = {CHAIN_TRANSES:[UPPER_TRANS, LOWER_TRANS],
AVAIL_SELECTED_INDEX:2,
CHAIN_SELECTED_INDEX:0}
self.assertEquals(expected, self.viewstub.get_on_remove_trans_data())
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
当转换器链列表仅包含一个转换器时,在转换器链列表中选中这个转换器,点击Remove按钮,选中的转换器被移除出转换器链,转换器链列表为空,无选中项。可用转换器列表中的转换器条目不变,选中被移除的转换器。
tests.test_presenter.py TestPresenter Class
def test_remove_a_trans_when_chain_has_only_one_transes(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:UPPER_TRANS})
self.presenter.add_trans()
self.viewstub.set_remove_trans_data({CHAIN_SELECTED_TRANS:UPPER_TRANS})
self.presenter.remove_trans()
expected = {CHAIN_TRANSES:[],
AVAIL_SELECTED_INDEX:0,
CHAIN_SELECTED_INDEX:NONE_SELECTED_INDEX}
self.assertEquals(expected, self.viewstub.get_on_remove_trans_data())
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
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按钮,提示“请在转换器链中指定一个转换器”。可用转换器列表中的转换器条目不变,选中之前已经选中的转换器。转换器链列表中的转换器条目不变,选中第一个转换器。
tests.test_presenter.py TestPresenter Class
def test_remove_trans_when_chain_is_not_empty_but_chain_trans_not_specified(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:UPPER_TRANS})
self.presenter.add_trans()
self.viewstub.set_remove_trans_data({CHAIN_SELECTED_TRANS:None})
self.presenter.remove_trans()
self.assertEquals(
{VALIDATING_FAILED_REASON:ValidatingResult.VRFR_CHAIN_TRANS_NOT_SPECIFIED},
self.viewstub.get_on_validating_failed_data())
expected = {CHAIN_TRANSES:[UPPER_TRANS],
AVAIL_SELECTED_INDEX:1,
CHAIN_SELECTED_INDEX:0}
self.assertEquals(expected, self.viewstub.get_on_remove_trans_data())
1 Validator
validator.py Validator Class
VRFR_CHAIN_TRANS_NOT_SPECIFIED = 'validating_result_failed_reason_chain_trans_not_specified'
2 Presenter
presenter.py Presenter Class
def remove_trans(self):
chain_selected_trans = self.view.collect_remove_trans_data()[CHAIN_SELECTED_TRANS]
validating_result = self.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.on_remove_trans({CHAIN_TRANSES:self.chain_transes,
AVAIL_SELECTED_INDEX:self.avail_selected_index,
CHAIN_SELECTED_INDEX:self.chain_selected_index})
def build_param_validating_rules_for_remove(self, chain_selected_trans):
return [ParamValidatingRule(chain_selected_trans,
Presenter.trans_not_specified,
ValidatingResult.VRFR_CHAIN_TRANS_NOT_SPECIFIED)]
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)
view.py ViewImpl Class
VRFR_TIP_MAP = {
...
ValidatingResult.VRFR_CHAIN_TRANS_NOT_SPECIFIED:
'Specify a transformer from the chain, please.'
}
转换器链列表为空,无选中项,点击Remove按钮,提示“转换器链为空”。可用转换器列表中的转换器条目不变,选中第一个转换器。
tests.test_presenter.py TestPresenter Class
def test_remove_trans_when_chain_is_empty(self):
self.presenter.remove_trans()
self.assertEquals({VALIDATING_FAILED_REASON:ValidatingResult.VRFR_CHAIN_EMPTY},
self.viewstub.get_on_validating_failed_data())
expected = {CHAIN_TRANSES:[],
AVAIL_SELECTED_INDEX:0,
CHAIN_SELECTED_INDEX:NONE_SELECTED_INDEX}
self.assertEquals(expected, self.viewstub.get_on_remove_trans_data())
1 ViewStub
tests.test_presenter.py ViewStub Class
def __init__(self):
self.remove_trans_data = None
如果remove_trans_data从来没有被赋过值,就直接返回,Python会报如下错误信息。所以在构造函数中先赋一个初值。
AttributeError: 'ViewStub' object has no attribute 'remove_trans_data'
2 Validator
validator.py Validator Class
VRFR_CHAIN_EMPTY = 'validating_result_failed_reason_chain_empty'
3 Presenter
presenter.py Presenter Class
def remove_trans(self):
remove_trans_data = self.view.collect_remove_trans_data()
chain_selected_trans = remove_trans_data[CHAIN_SELECTED_TRANS] if remove_trans_data else None
validating_result = self.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.on_remove_trans({CHAIN_TRANSES:self.chain_transes,
AVAIL_SELECTED_INDEX:self.avail_selected_index,
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,
ValidatingResult.VRFR_CHAIN_EMPTY),
ParamValidatingRule(chain_selected_trans,
Presenter.trans_not_specified,
ValidatingResult.VRFR_CHAIN_TRANS_NOT_SPECIFIED)]
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 == []
view.py ViewImpl Class
VRFR_TIP_MAP = {
...
ValidatingResult.VRFR_CHAIN_EMPTY:
'Specify the transformer chain, please.'
}
转换器链列表不为空,无论转换器链列表是否选中转换器,点击Remove All按钮,所有转换器均被移除出转换器链,转换器链列表为空,无选中项。可用转换器列表中的转换器条目不变,选中第一个转换器。
tests.test_presenter.py TestPresenter Class
def test_remove_all_transes_when_chain_is_not_empty(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:UPPER_TRANS})
self.presenter.add_trans()
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:LOWER_TRANS})
self.presenter.add_trans()
self.presenter.remove_all_transes()
expected = {CHAIN_TRANSES:[],
AVAIL_SELECTED_INDEX:0,
CHAIN_SELECTED_INDEX:NONE_SELECTED_INDEX}
self.assertEquals(expected, self.viewstub.get_on_remove_all_transes_data())
1 ViewStub
tests.test_presenter.py ViewStub Class
def get_on_remove_all_transes_data(self): pass
2 Presenter
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.on_remove_all_transes({CHAIN_TRANSES:self.chain_transes,
CHAIN_SELECTED_INDEX:self.chain_selected_index,
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 = 0
3 View
view.py View (Interface) Class
def on_remove_all_transes(self, data): pass
4 ViewStub
tests.test_presenter.py ViewStub Class
# Override
def on_remove_all_transes(self, data):
self.on_remove_all_transes_data = data
def get_on_remove_all_transes_data(self):
return self.on_remove_all_transes_data
view.py ViewImpl Class
def remove_all_transformers(self):
self.presenter.remove_all_transes()
# Override
def on_remove_all_transes(self, data):
ViewImpl.set_list_data(self.lstchain, data[CHAIN_TRANSES])
ViewImpl.set_list_selected_index(self.lstchain, data[CHAIN_SELECTED_INDEX])
ViewImpl.set_list_selected_index(self.lstavail, data[AVAIL_SELECTED_INDEX])
转换器链列表为空,无选中项,点击Remove All按钮,提示“请指定转换器链”。可用转换器列表中的转换器条目不变,选中之前选中的转换器。
tests.test_presenter.py TestPresenter Class
def test_remove_all_transes_when_chain_is_empty(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:LOWER_TRANS})
self.presenter.add_trans()
self.viewstub.set_remove_trans_data({CHAIN_SELECTED_TRANS:LOWER_TRANS})
self.presenter.remove_trans()
self.presenter.remove_all_transes()
self.assertEquals({VALIDATING_FAILED_REASON:ValidatingResult.VRFR_CHAIN_EMPTY},
self.viewstub.get_on_validating_failed_data())
expected = {CHAIN_TRANSES:[],
AVAIL_SELECTED_INDEX:1,
CHAIN_SELECTED_INDEX:NONE_SELECTED_INDEX}
self.assertEquals(expected, self.viewstub.get_on_remove_all_transes_data())
presenter.py Presenter Class
def remove_all_transes(self):
validating_result = self.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.on_remove_all_transes({CHAIN_TRANSES:self.chain_transes,
CHAIN_SELECTED_INDEX:self.chain_selected_index,
AVAIL_SELECTED_INDEX:self.avail_selected_index})
def build_param_validating_rules_for_remove_all(self):
return [ParamValidatingRule(self.chain_transes,
Presenter.empty_list,
ValidatingResult.VRFR_CHAIN_EMPTY)]
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
输入合法的源字符串,构建好非空的转换器链,点击Apply按钮,将转换器链中的转换器从上到下依次应用到源字符串上,得到最终的结果字符串。
tests.test_presenter.py TestPresenter Class
def test_apply_trans_chain(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:UPPER_TRANS})
self.presenter.add_trans()
self.viewstub.set_apply_trans_chain_data({SOURCE_STR:'Hello, world.'})
self.presenter.apply_trans_chain()
self.assertEquals({RESULT_STR:'HELLO, WORLD.'}, self.viewstub.get_on_apply_trans_chain_data())
1 ViewStub
tests.test_presenter.py ViewStub Class
def set_apply_trans_chain_data(self, data): pass
def get_on_apply_trans_chain_data(self): pass
2 定义View与Presenter交互用的常量。
interaction.py
SOURCE_STR = 'source_str'
RESULT_STR = 'result_str'
3 Presenter
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):
source_str = self.view.collect_apply_trans_chain_data()[SOURCE_STR]
self.result_str = self.businesslogic.transform(source_str, self.chain_transes)
self.view.on_apply_trans_chain({RESULT_STR:self.result_str})
4 View
view.py View (Interface) Class
def collect_apply_trans_chain_data(self): pass
def on_apply_trans_chain(self, data): pass
5 ViewStub
tests.test_presenter.py ViewStub Class
# Override
def collect_apply_trans_chain_data(self):
return self.apply_trans_chain_data
def set_apply_trans_chain_data(self, data):
self.apply_trans_chain_data = data
# Override
def on_apply_trans_chain(self, data):
self.on_apply_trans_chain_data = data
def get_on_apply_trans_chain_data(self):
return self.on_apply_trans_chain_data
6 BusinessLogic
businesslogic.py BusinessLogic (Interface) Class
def transform(self, source_str, transes): pass
7 BusinessLogicStub
tests.test_presenter.py BusinessLogicStub Class
def transform(self, source_str, transes):
return 'HELLO, WORLD.'
tests.test_businesslogicimpl.py TestBusinessLogicImpl Class
def test_transform_upper(self):
self.assertEquals('HELLO, WORLD.', self.impl.transform('Hello, world.', [UPPER_TRANS]))
businesslogic.py BusinessLogicImpl Class
TRANS_FUNC_MAP = {
UPPER_TRANS:lambda s: BusinessLogicImpl.upper(s)
}
# Override
def transform(self, source_str, transes):
return BusinessLogicImpl.TRANS_FUNC_MAP[transes[0]](source_str)
@staticmethod
def upper(s):
return s.upper()
view.py ViewImpl Class
def apply_transformer_chain(self):
self.presenter.apply_trans_chain()
# Override
def collect_apply_trans_chain_data(self):
return {SOURCE_STR:ViewImpl.get_entry_txt(self.txtsourcestr)}
# Override
def on_apply_trans_chain(self, data):
ViewImpl.set_entry_txt(self.resultstr, data[RESULT_STR])
构建好非空的转换器链,但未输入源字符串或源字符串为空,点击Apply按钮,提示“请输入源字符串”,焦点定位到源字符串文本框。
tests.test_presenter.py TestPresenter Class
def test_apply_trans_chain_when_source_str_is_empty(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:UPPER_TRANS})
self.presenter.add_trans()
self.viewstub.set_apply_trans_chain_data({SOURCE_STR:''})
self.presenter.apply_trans_chain()
self.assertEquals({VALIDATING_FAILED_REASON:ValidatingResult.VRFR_SOURCE_STR_EMPTY},
self.viewstub.get_on_validating_failed_data())
self.assertEquals({RESULT_STR:''}, self.viewstub.get_on_apply_trans_chain_data())
1 ViewStub
tests.test_presenter.py ViewStub Class
def __init__(self):
...
self.on_apply_trans_chain_data = None
如果on_apply_trans_chain_data从来没有被赋过值,就直接返回,Python会报如下错误信息。所以在构造函数中先赋一个初值。
AttributeError: 'ViewStub' object has no attribute 'on_apply_trans_chain_data'
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.collect_apply_trans_chain_data()[SOURCE_STR]
validating_result = self.validate(self.build_param_validating_rules_for_apply(source_str))
self.result_str = self.businesslogic.transform(source_str, self.chain_transes) \
if validating_result.is_succeeded else ''
self.view.on_apply_trans_chain({RESULT_STR:self.result_str})
def build_param_validating_rules_for_apply(self, source_str):
return [ParamValidatingRule(source_str,
Presenter.empty_str,
ValidatingResult.VRFR_SOURCE_STR_EMPTY)]
@staticmethod
def empty_str(s):
return s == ''
view.py ViewImpl Class
VRFR_TIP_MAP = {
...
ValidatingResult.VRFR_SOURCE_STR_EMPTY:
'Specify the source string, please.'
}
# Override
def on_validating_failed(self, data):
tkMessageBox.showinfo('Information', ViewImpl.VRFR_TIP_MAP[data[VALIDATING_FAILED_REASON]])
if data == ValidatingResult.VRFR_SOURCE_STR_EMPTY:
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_add_trans_data({AVAIL_SELECTED_TRANS:UPPER_TRANS})
self.presenter.add_trans()
self.viewstub.set_apply_trans_chain_data({SOURCE_STR:'a中文b'})
self.presenter.apply_trans_chain()
self.assertEquals({VALIDATING_FAILED_REASON:ValidatingResult.VRFR_SOURCE_STR_ILLEGAL},
self.viewstub.get_on_validating_failed_data())
self.assertEquals({RESULT_STR:''}, self.viewstub.get_on_apply_trans_chain_data())
1 Validator
validator.py Validator Class
VRFR_SOURCE_STR_ILLEGAL = 'validating_result_failed_reason_source_str_illegal'
2 Presenter
presenter.py Presenter Class
import re
def build_param_validating_rules_for_apply(self, source_str):
return [ParamValidatingRule(source_str,
Presenter.empty_str,
ValidatingResult.VRFR_SOURCE_STR_EMPTY),
ParamValidatingRule(source_str,
Presenter.illegal_source_str,
ValidatingResult.VRFR_SOURCE_STR_ILLEGAL)]
@staticmethod
def illegal_source_str(s):
pattern = re.compile(u'[\u4e00-\u9fa5]+')
return pattern.search(s.decode('utf-8')) is not None
view.py ViewImpl Class
VRFR_TIP_MAP = {
...
ValidatingResult.VRFR_SOURCE_STR_ILLEGAL:
'Specify the legal source string, please.'
}
# Override
def on_validating_failed(self, data):
tkMessageBox.showinfo('Information', ViewImpl.VRFR_TIP_MAP[data[VALIDATING_FAILED_REASON]])
if data == ValidatingResult.VRFR_SOURCE_STR_EMPTY:
self.txtsourcestr.focus_set()
elif data == ValidatingResult.VRFR_SOURCE_STR_ILLEGAL:
self.txtsourcestr.focus_set()
self.txtsourcestr.select_range(0, END)
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()
用解释器的方式重写ViewImpl.on_validating_failed()方法。
view.py ViewImpl Class
@staticmethod
def focus_entry_and_select_all(entry):
entry.focus_set()
entry.select_range(0, END)
ACTION_SHOW_INFO = 'show_info'
ACTION_FOCUS_AND_SELECT_ALL_SOURCE_STR = 'focus_and_select_all_source_str'
VRFR_ACTIONS_MAP = {
ValidatingResult.VRFR_AVAIL_TRANS_NOT_SPECIFIED:
[(ACTION_SHOW_INFO, 'Specify an available transformer, please.')],
ValidatingResult.VRFR_ADD_ALREADY_EXISTED_IN_CHAIN_TRANS:
[(ACTION_SHOW_INFO, 'The transformer to be added has been already existed in the chain.')],
ValidatingResult.VRFR_CHAIN_TRANS_NOT_SPECIFIED:
[(ACTION_SHOW_INFO, 'Specify a transformer from the chain, please.')],
ValidatingResult.VRFR_CHAIN_EMPTY:
[(ACTION_SHOW_INFO, 'Specify the transformer chain, please.')],
ValidatingResult.VRFR_SOURCE_STR_EMPTY:
[(ACTION_SHOW_INFO, 'Specify the source string, please.'),
(ACTION_FOCUS_AND_SELECT_ALL_SOURCE_STR, None)],
ValidatingResult.VRFR_SOURCE_STR_ILLEGAL:
[(ACTION_SHOW_INFO, 'Specify the legal source string, please.'),
(ACTION_FOCUS_AND_SELECT_ALL_SOURCE_STR, None)]
}
# Override
def on_validating_failed(self, data):
actions = ViewImpl.VRFR_ACTIONS_MAP[data[VALIDATING_FAILED_REASON]]
for (action_type, action_param) in actions:
if action_type == ViewImpl.ACTION_SHOW_INFO:
tkMessageBox.showinfo('Information', action_param)
elif action_type == ViewImpl.ACTION_FOCUS_AND_SELECT_ALL_SOURCE_STR:
ViewImpl.focus_entry_and_select_all(self.txtsourcestr)
原来的VRFR_TIP_MAP删掉了。
输入合法的源字符串,但转换器链为空,点击Apply按钮,提示“请指定转换器链”。可用转换器列表中的转换器条目不变,选中第一个转换器。
tests.test_presenter.py TestPresenter Class
def test_apply_trans_chain_when_chain_is_empty(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:LOWER_TRANS})
self.presenter.add_trans()
self.viewstub.set_remove_trans_data({CHAIN_SELECTED_TRANS:LOWER_TRANS})
self.presenter.remove_trans()
self.viewstub.set_apply_trans_chain_data({SOURCE_STR:'Hello, world.'})
self.presenter.apply_trans_chain()
self.assertEquals({VALIDATING_FAILED_REASON:ValidatingResult.VRFR_CHAIN_EMPTY},
self.viewstub.get_on_validating_failed_data())
self.assertEquals({AVAIL_SELECTED_INDEX:0, RESULT_STR:''},
self.viewstub.get_on_apply_trans_chain_data())
- 期望值Map Keys的顺序是Map的內建顺序,不是put的顺序。
- 由于on_apply_trans_chain_data的数据结构变了,所以apply_trans_chain相关的测试用例都需要修改。
tests.test_presenter.py TestPresenter Class
def test_apply_trans_chain(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:UPPER_TRANS})
self.presenter.add_trans()
self.viewstub.set_apply_trans_chain_data({SOURCE_STR:'Hello, world.'})
self.presenter.apply_trans_chain()
self.assertEquals({AVAIL_SELECTED_INDEX:1, RESULT_STR:'HELLO, WORLD.'},
self.viewstub.get_on_apply_trans_chain_data())
def test_apply_trans_chain_when_source_str_is_empty(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:UPPER_TRANS})
self.presenter.add_trans()
self.viewstub.set_apply_trans_chain_data({SOURCE_STR:''})
self.presenter.apply_trans_chain()
self.assertEquals({VALIDATING_FAILED_REASON:ValidatingResult.VRFR_SOURCE_STR_EMPTY},
self.viewstub.get_on_validating_failed_data())
self.assertEquals({AVAIL_SELECTED_INDEX:1, RESULT_STR:''},
self.viewstub.get_on_apply_trans_chain_data())
def test_apply_trans_chain_when_source_str_is_illegal(self):
self.viewstub.set_add_trans_data({AVAIL_SELECTED_TRANS:UPPER_TRANS})
self.presenter.add_trans()
self.viewstub.set_apply_trans_chain_data({SOURCE_STR:'a中文b'})
self.presenter.apply_trans_chain()
self.assertEquals({VALIDATING_FAILED_REASON:ValidatingResult.VRFR_SOURCE_STR_ILLEGAL},
self.viewstub.get_on_validating_failed_data())
self.assertEquals({AVAIL_SELECTED_INDEX:1, RESULT_STR:''},
self.viewstub.get_on_apply_trans_chain_data())
presenter.py Presenter Class
def apply_trans_chain(self):
source_str = self.view.collect_apply_trans_chain_data()[SOURCE_STR]
validating_result = self.validate(self.build_param_validating_rules_for_apply(source_str))
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)
self.view.on_apply_trans_chain({RESULT_STR:self.result_str,
AVAIL_SELECTED_INDEX:self.avail_selected_index})
def build_param_validating_rules_for_apply(self, source_str):
return [ParamValidatingRule(source_str,
Presenter.empty_str,
ValidatingResult.VRFR_SOURCE_STR_EMPTY),
ParamValidatingRule(source_str,
Presenter.illegal_source_str,
ValidatingResult.VRFR_SOURCE_STR_ILLEGAL),
ParamValidatingRule(self.chain_transes,
Presenter.empty_list,
ValidatingResult.VRFR_CHAIN_EMPTY)]
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
view.py ViewImpl Class
# Override
def on_apply_trans_chain(self, data):
ViewImpl.set_entry_txt(self.resultstr, data[RESULT_STR])
ViewImpl.set_list_selected_index(self.lstavail, data[AVAIL_SELECTED_INDEX])
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):
self.assertEquals('hello, world.', self.impl.transform('Hello, world.', [LOWER_TRANS]))
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]))
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
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 get_all_transes(self):
return BusinessLogicImpl.TRANS_FUNC_MAP.keys()
# 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)
@staticmethod
def lower(s):
return s.lower()
@staticmethod
def trim_prefix_spaces(s):
return s.lstrip()
presenter.py OperData Class
def _oper_trans(self, oper_data):
if oper_data.collect_view_data:
view_data = oper_data.collect_view_data()
validating_result = self.validate(oper_data.build_param_validating_rules(view_data))
oper_data.update_presenter_data(view_data, validating_result)
else:
validating_result = self.validate(oper_data.build_param_validating_rules())
oper_data.update_presenter_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
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.collect_add_trans_data()[AVAIL_SELECTED_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.on_add_trans({CHAIN_TRANSES:self.chain_transes,
CHAIN_SELECTED_INDEX:self.chain_selected_index,
AVAIL_SELECTED_INDEX:self.avail_selected_index})
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):
remove_trans_data = self.view.collect_remove_trans_data()
return remove_trans_data[CHAIN_SELECTED_TRANS] if remove_trans_data else None
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.on_remove_trans({CHAIN_TRANSES:self.chain_transes,
AVAIL_SELECTED_INDEX:self.avail_selected_index,
CHAIN_SELECTED_INDEX:self.chain_selected_index})
presenter.py Presenter Class
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 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.on_remove_all_transes({CHAIN_TRANSES:self.chain_transes,
CHAIN_SELECTED_INDEX:self.chain_selected_index,
AVAIL_SELECTED_INDEX:self.avail_selected_index})
presenter.py Presenter Class
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.collect_apply_trans_chain_data()[SOURCE_STR]
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.on_apply_trans_chain({RESULT_STR:self.result_str,
AVAIL_SELECTED_INDEX:self.avail_selected_index})
仔细观察,present_view_data_for_add()、present_view_data_for_remove()和present_view_data_for_remove_all()方法都是将chain_transes、chain_selected_index和avail_selected_index推送给View显示。目前的实现方案中的展示操作都是顺序无关的,因此可以抽取_build_present_view_data_for_building_trans_chain()方法。而上一次课程中的实现方案的展示操作是顺序相关的,不能抽取公共方法。
presenter.py Presenter Class
def _build_present_view_data_for_building_trans_chain(self):
return {CHAIN_TRANSES:self.chain_transes,
CHAIN_SELECTED_INDEX:self.chain_selected_index,
AVAIL_SELECTED_INDEX:self.avail_selected_index}
def present_view_data_for_add(self):
self.view.on_add_trans(self._build_present_view_data_for_building_trans_chain())
def present_view_data_for_remove(self):
self.view.on_remove_trans(self._build_present_view_data_for_building_trans_chain())
def present_view_data_for_remove_all(self):
self.view.on_remove_all_transes(self._build_present_view_data_for_building_trans_chain())
对比一下之前的View接口类和现在的View接口类,很明显,现在的View接口关注的是交互逻辑,之前的View接口关注的是交互实现细节。统一的参数校验失败通知方法,也体现出了这一点。
这样做的好处在于:
- View接口、Presenter测试代码与产品代码都不会因为界面展现细节的变化而频繁变更,保持相对稳定。
- Presenter实现与界面中使用的控件细节无关,因为那些细节被放到了ViewImpl中。
Presenter与View的交互接口采用指令式,将计算描述与执行分离。Presenter计算好交互逻辑所需要的所有数据,封装为指令,传递给View,View根据指令控制界面控件展现细节。
这么实现的话,View不再是纯哑(Dummy)的,View中会有一些实现细节逻辑。需要注意,即便View中包含一些实现细节逻辑,也只能是和界面控件细节相关的逻辑,不能是业务逻辑,而且要尽量简单,简单到一眼就能看出有没有问题。
字符串转换器支持“Reverse”转换。
主要修改BusinessLogicImpl。
代码路径:instruction.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”(添加所有转换器)功能。
代码路径:instruction.addall
无论转换器链列表是否为空,无论可用转换器列表是否选中转换器,点击Add All按钮,所有转换器均被添加到转换器链列表。转换器链列表和可用转换器列表完全一样。可用转换器列表中的转换器条目不变,选中第一个转换器。转换器链列表选中最后一个转换器。
tests.test_presenter.py TestPresenter Class
def test_add_all_transes(self):
self.presenter.add_all_transes()
expected = {CHAIN_TRANSES:[UPPER_TRANS, LOWER_TRANS, TRIM_PREFIX_SPACES_TRANS],
AVAIL_SELECTED_INDEX:0,
CHAIN_SELECTED_INDEX:2}
self.assertEquals(expected, self.viewstub.get_on_add_all_transes_data())
tests.test_presenter.py ViewStub Class
# Override
def on_add_all_transes(self, data):
self.on_add_all_transes_data = data
def get_on_add_all_transes_data(self):
return self.on_add_all_transes_data
1 View
view.py View (Interface) Class
def on_add_all_transes(self, data): pass
2 Presenter
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 = self.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 = self.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()
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):
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 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.on_add_all_transes(self._build_present_view_data_for_building_trans_chain())
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 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()
# Override
def on_add_all_transes(self, data):
ViewImpl.set_list_data(self.lstchain, data[CHAIN_TRANSES])
ViewImpl.set_list_selected_index(self.lstavail, data[AVAIL_SELECTED_INDEX])
ViewImpl.set_list_selected_index(self.lstchain, data[CHAIN_SELECTED_INDEX])