201803 TDD Coding Practice String Transformer Reloaded in Python - xiaoxianfaye/Courses GitHub Wiki

1 Review

1.1 Problem

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

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

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

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

python string transformer

1.2 Design

在上一次课程中,我们根据《测试总线势在必行——设计支持自动化验收测试的架构》这篇论文中介绍的“Bypassing the UI”的理论方法(下面第1张图)设计出一个分层系统(Layered System,下面第2张图)。

test bus bypassing ui 2

design layered system

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

1.3 Implementation

全局类图如下图所示:

design global view

2 Stop to Think

2.1 Problems

我们停下来思考一下。

请学员思考目前的设计与实现是否存在问题?
Presenter和View的交互接口太过细致,关注的是交互实现细节,而不是真正的交互逻辑

另外,每个参数的每种校验失败情况都对应了View接口中的一个通知方法,也偏向于“交互实现细节”,而不是真正的“交互逻辑”。

这会导致:

  1. View接口的频繁变更。
  2. Presenter测试代码与产品代码的频繁变更。
  3. 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库,可以使得可用转换器列表和转换器链列表在程序运行期间永远不会处于未选中任何转换器的状态,因此根本就不需要处理以上异常流程。

2.2 Directions of Improvement

有以下几个改进方向:

  • Presenter应关注交互逻辑而不是交互实现细节,才能使得View接口、Presenter测试代码及其产品代码都不会因为界面展现细节的变化而频繁变更,保持相对稳定。

  • 既然之前的Presenter有实现细节与控件相关,那就把这些实现细节放到ViewImpl中,使得Presenter实现与界面中使用的控件细节无关。

  • 定义统一的参数校验失败通知方法。

为此,Presenter与View的交互接口采用指令式,将计算描述与执行分离。Presenter计算好交互逻辑所需要的所有数据,封装为指令,传递给View,View根据指令控制界面控件展现细节。

这么实现的话,View不再是纯哑(Dummy)的,View中会有一些实现细节逻辑。需要注意,即便View中包含一些实现细节逻辑,也只能是和界面控件细节相关的逻辑,不能是业务逻辑,而且要尽量简单,简单到一眼就能看出有没有问题。

3 Implementation

按照上一次课程中总结的业务流程,还是从交付角度,以一个业务流程为单位,将所有业务流程按端到端(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

3.1 Init

初始化完成后:

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

3.1.1 Presenter

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

3.1.1.1 Test Code

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包括可用转换器列表和可用转换器列表选中转换器的索引。

3.1.1.2 Product Code

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]

3.1.2 BusinessLogicImpl

BusinessLogicImpl提供所有的转换器。

3.1.2.1 Test Code

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())

3.1.2.2 Product Code

新增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]

3.1.3 ViewImpl

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])

3.1.4 main

在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()

3.2 Build Transformer Chain

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

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

3.2.1 Add a Transformer

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

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

3.2.1.1.1 Presenter
3.2.1.1.1.1 Test Code

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包括转换器链列表、转换器链列表选中转换器的索引和可用转换器列表选中转换器的索引。

3.2.1.1.1.2 Product Code

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
3.2.1.1.2 ViewImpl

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()方法中要注意先设置转换器链列表选中转换器的索引、后设置可用转换器列表选中转换器的索引。

3.2.1.1.3 Assembly

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)

        ...

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

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

3.2.1.2.1 Presenter
3.2.1.2.1.1 Test Code

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())
3.2.1.2.1.2 Product Code

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

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

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

3.2.1.3.1 Presenter
3.2.1.3.1.1 Test Code

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的通知。

3.2.1.3.1.2 Product Code

关于参数校验,整体上依然采用之前的设计,但由于定义统一的参数校验失败通知方法,细节跟之前略有不同。

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
3.2.1.3.2 ViewImpl

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按钮,提示“请在可用转换器中指定一个转换器”。转换器链列表中的转换器条目不变,选中之前已经选中的转换器。可用转换器列表中的转换器条目不变,选中第一个转换器。

3.2.1.4.1 Presenter
3.2.1.4.1.1 Test Code

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())
3.2.1.4.1.2 Product Code

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
3.2.1.4.2 ViewImpl

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 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

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

3.2.2.1.1 Presenter
3.2.2.1.1.1 Test Code

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())
3.2.2.1.1.2 Product Code

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
3.2.2.1.2 ViewImpl

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按钮,选中的转换器被移除出转换器链。可用转换器列表中的转换器条目不变,选中被移除的转换器。转换器链列表选中第一个转换器。

3.2.2.2.1 Presenter
3.2.2.2.1.1 Test Code

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())
3.2.2.2.1.2 Product 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

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

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

3.2.2.3.1 Presenter
3.2.2.3.1.1 Test Code

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())
3.2.2.3.1.2 Product 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

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按钮,提示“请在转换器链中指定一个转换器”。可用转换器列表中的转换器条目不变,选中之前已经选中的转换器。转换器链列表中的转换器条目不变,选中第一个转换器。

3.2.2.4.1 Presenter
3.2.2.4.1.1 Test Code

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())
3.2.2.4.1.2 Product Code

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)
3.2.2.4.2 ViewImpl

view.py ViewImpl Class

    VRFR_TIP_MAP = {
        ...

        ValidatingResult.VRFR_CHAIN_TRANS_NOT_SPECIFIED:
            'Specify a transformer from the chain, please.'
    }

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

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

3.2.2.5.1 Presenter
3.2.2.5.1.1 Test Code

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())
3.2.2.5.1.2 Product Code

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 == []
3.2.2.5.2 ViewImpl

view.py ViewImpl Class

    VRFR_TIP_MAP = {
        ...

        ValidatingResult.VRFR_CHAIN_EMPTY:
            'Specify the transformer chain, please.'
    }

3.2.3 Remove All Transformers

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

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

3.2.3.1.1 Presenter
3.2.3.1.1.1 Test Code

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())
3.2.3.1.1.2 Product Code

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
3.2.3.1.2 ViewImpl

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])

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

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

3.2.3.2.1 Presenter
3.2.3.2.1.1 Test Code

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())
3.2.3.2.1.2 Product Code

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

3.3 Apply Transformer Chain

3.3.1 Normal Business Process 1: Apply the transformer chain

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

3.3.1.1 Presenter

3.3.1.1.1 Test Code

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())
3.3.1.1.2 Product Code

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.'

3.3.1.2 BusinessLogicImpl

3.3.1.2.1 Test Code

tests.test_businesslogicimpl.py TestBusinessLogicImpl Class

    def test_transform_upper(self):
        self.assertEquals('HELLO, WORLD.', self.impl.transform('Hello, world.', [UPPER_TRANS]))
3.3.1.2.2 Product Code

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()

3.3.1.3 ViewImpl

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])

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

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

3.3.2.1 Presenter

3.3.2.1.1 Test Code

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())
3.3.2.1.2 Product Code

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 == ''

3.3.2.2 ViewImpl

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()

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

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

3.3.3.1 Presenter

3.3.3.1.1 Test Code

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())
3.3.3.1.2 Product Code

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

3.3.3.2 ViewImpl

3.3.3.2.1 Product Code

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()
3.3.3.2.2 Rewrite ViewImpl.on_validating_failed()

用解释器的方式重写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删掉了。

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

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

3.3.4.1 Presenter

3.3.4.1.1 Test Code

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())
3.3.4.1.2 Write the Code

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

3.3.4.2 ViewImpl

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])

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

3.4.1 Test Cases

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

3.4.2 Test Code

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]))

3.4.3 Product Code

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()

3.5 _oper_trans

3.5.1 _oper_trans & OperData

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

3.5.2 add_trans

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})

3.5.3 remove_trans

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})

3.5.4 remove_all_transes

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})

3.5.5 apply_trans_chain

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})

3.5.5 _build_present_view_data_for_building_trans_chain

仔细观察,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())

4 Improvement

对比一下之前的View接口类和现在的View接口类,很明显,现在的View接口关注的是交互逻辑,之前的View接口关注的是交互实现细节。统一的参数校验失败通知方法,也体现出了这一点。

这样做的好处在于:

  • View接口、Presenter测试代码与产品代码都不会因为界面展现细节的变化而频繁变更,保持相对稳定。
  • Presenter实现与界面中使用的控件细节无关,因为那些细节被放到了ViewImpl中。

Presenter与View的交互接口采用指令式,将计算描述与执行分离。Presenter计算好交互逻辑所需要的所有数据,封装为指令,传递给View,View根据指令控制界面控件展现细节。

这么实现的话,View不再是纯哑(Dummy)的,View中会有一些实现细节逻辑。需要注意,即便View中包含一些实现细节逻辑,也只能是和界面控件细节相关的逻辑,不能是业务逻辑,而且要尽量简单,简单到一眼就能看出有没有问题。

5 My Homework

5.1 Reverse

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

主要修改BusinessLogicImpl。

代码路径:instruction.reverse

5.1.1 Test Code

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的顺序。

5.1.2 Product Code

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]

5.2 Add All

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

代码路径:instruction.addall

5.2.1 Business Process of Add All Transformers

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

5.2.2 Implementation

5.2.2.1 Presenter

5.2.2.1.1 Test Code

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
5.2.2.1.2 Product Code

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[:]

5.2.2.2 ViewImpl

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])
⚠️ **GitHub.com Fallback** ⚠️