201712 DDD DSL Design War FizzBuzzWhizz in Python - xiaoxianfaye/Courses GitHub Wiki

1 Problem

你是一名体育老师,在某次课距离下课还有五分钟时,你决定搞一个游戏。此时有100名学生在上课。游戏的规则是:

  1. 设定三个不同的特殊数:3、5、7。

  2. 让所有学生排成一队,然后按顺序报数。

  3. 学生报数时,如果所报数字是第一个特殊数(3)的倍数,那么不能说该数字,而要说Fizz;如果所报数字是第二个特殊数(5)的倍数,那么要说Buzz;如果所报数字是第三个特殊数(7)的倍数,那么要说Whizz。

  4. 学生报数时,如果所报数字同时是两个特殊数的倍数情况下,也要特殊处理,比如第一个特殊数和第二个特殊数的倍数,那么不能说该数字,而是要说FizzBuzz, 以此类推。如果同时是三个特殊数的倍数,那么要说FizzBuzzWhizz。

  5. 学生报数时,如果所报数字包含了第一个特殊数,那么也不能说该数字,而是要说相应的单词,比如本例中第一个特殊数是3,那么要报13的同学应该说Fizz。 如果数字中包含了第一个特殊数,那么忽略规则3和规则4,比如要报35的同学只报Fizz,不报BuzzWhizz。

请编写一个程序来模拟这个游戏。

2 Showcase & Discuss

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

3 Analysis

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

  1. 根据题目的第1点,特殊数需要作为输入条件吗?
    不需要,至少现在不需要,不是核心问题。

  2. 根据题目的第2点,程序要做什么?输入是什么?输出是什么?
    报数,整数到字符串的转换。输入1到100的整数,输出各自对应的字符串。

  3. 题目的第3点中描述的是什么?
    规则。定义为规则1。

  4. 题目的第4点中描述的规则定义为规则2。在“两个特殊数的倍数”的情况中,只有同时是3和5的倍数要处理吗?
    还要处理同时是3和7的倍数、同时是5和7的倍数。

  5. 题目的第5点中描述的规则定义为规则3。

  6. 除了规则1、规则2、规则3,还有没有其它规则?
    默认规则。如果规则1、规则2、规则3都不满足,直接报数本身。

  7. 规则1、规则2和规则3和默认规则之间是什么关系?
    按优先级由高到低:Rule3 -> Rule2 -> Rule1 -> Default Rule。

4 Design

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

4.1 Core Concepts

design core concepts

Rule(规则)和Relation(规则之间的关系),是这个问题领域的核心概念。

4.2 What does a Rule mean?

一个Rule是什么意思?什么是一个Rule?Rule的语义(Semantics)是什么?

一个Rule的输入是什么?输出是什么?

Rule: int -> {true, String} | {false, ""}

一个Rule的工作流程是什么样的?
接受一个int输入,判断int是否满足条件,满足的话就做动作,输出{true, String},不满足就输出{false, ""}。
(When an integer is inputted, predicate it. If predicated true, the relative action will be acted, {true, String} is outputted, otherwise, {false, ""} is outputted.)

构造一个Rule需要哪几个部分?

Predication: int -> boolean             判断条件
Action: int -> String                   条件满足时的动作
Result: {true, String} | {false, ""}    结果

构造出来的Rule称为“原子Rule(Atom Rule)”。

design atom rule

4.3 What does a Relation mean?

Rule之间有优先级,优先级其实就是Relation(关系)。

那么,Relation到底是什么意思?什么是Relation?Relation的语义(Semantics)是什么?

题目中的第3点、第4点和第5点都是Rule,它们之间有关系。这些Rule各自还有很多子Rule,子Rule之间也有关系,后面会看到它们是一个同构结构。

4.3.1 How to express Relations explicitly?

如何把Relation显式地表达出来呢?

需要注意的是,这里的表达不是从实现层面考虑如何if/else,哪些if放在前面,哪些if放在后面,哪些放在else里等等。如果这样思考表达,就犯了一个很常见的错误:很多在需求里非常清晰的概念,在代码里看不见,变成一个实现层面的东西,使得代码难以理解、难以更改。

先来看题目第3点中的Rule1。

Rule1有3个子Rule,均为Atom Rule。3个子Rule之间是“OR”的关系,可以映射到布尔代数里的“OR”。

Rule 1: or(r1_3, r1_5, r1_7) 

再来看题目第4点中的Rule2。

Rule2有4个子Rule,均为Atom Rule。4个子Rule之间是“AND”的关系,可以映射到布尔代数里的“AND”。“AND”以后的子Rule还要“OR”一下,构成Rule2。

需要注意的是,这里的Relation实际上包含了两层意思:一层就是布尔代数中的“真/假”,另一层是多个Rule的Action之间的关系。Rule2的子Rule之间的“AND”的关系,可以映射到布尔代数里的“AND”,只不过“AND”之后还有一个操作,操作多个子Rule的应用结果,这里的操作就是String的concat。

Rule 2: or(and(r1_3, r1_5, r1_7), 
            and(r1_3, r1_5),
            and(r1_3, r1_7),  
            and(r1_5, r1_7))

题目的第5点的Rule3是个独立的规则,是一个Atom Rule。

最后还有一个default规则,Rule1、2、3都不满足的时候,运用default规则,也是一个Atom Rule。

那么Rule1、Rule2、Rule3和Default Rule之间是什么关系呢?
按优先级由高到低:Rule3 -> Rule2 -> Rule1 -> Default Rule,可以映射到布尔代数中“带短路运算的OR”。

回过头再来看Rule1,从语义层面来讲,Rule1的3个子Rule之间的“OR”关系应该是不带短路运算的,但带了也没有影响,所以也可以用“带短路运算的OR”。同理,Rule2的后3个子Rule之间的“OR”关系也应该是不带短路运算的,但带了也没有影响。

4.3.2 What does the Or / And mean?

这里的“OR”和“AND”是什么?

“OR”和“AND”用来定义Rule之间的Relation。它们都由两个Rule构造出来(多个Rule只是语法糖衣),构造出来还是一个Rule,输入是int,输出是Result。它们被称为“组合子Rule”。

Both of them are constructed by two rules. Both of them are also rules, combination rules.

design combination rule

两个Rule用 “OR”或者“AND”组合以后还是一个Rule,就可以和其它的Rule再次组合,也就是说“OR”和“AND”组合子是封闭的。整个Rule世界是封闭的。

组合子满足封闭性非常重要,因为只有满足封闭性,组合子才能和其它原子或者组合子再组合……。可以想象,如果一个语言提供的组合手段是能够封闭的,那么它就能够高效地帮助你构建出非常复杂的东西。

在面向对象编程语言中,通常用“接口”表示一个对象是一个“什么”,接口方法的输入、输出决定了“什么”的特征。而在函数式编程语言中,通常用“函数”表示“什么”,函数的输入、输出决定了“什么”的特征。

4.3.3 Mapping Problem Domain to Algebra Semantic Domain

这样,我们就把整个问题领域映射到了布尔代数的语义领域。

design domain mapping

什么是同构(Isomorphic)? 在数学中研究同构的主要目的是为了把数学理论应用于不同的领域。如果两个结构是同构的,那么其上的对象会有相似的属性和操作,对某个结构成立的命题在另一个结构上也就成立。因此,如果在某个数学领域发现了一个对象结构同构于某个结构,且对于该结构已经证明了很多定理,那么这些定理马上就可以应用到该领域。如果某些数学方法可以用于该结构,那么这些方法也可以用于新领域的结构。这就使得理解和处理该对象结构变得容易,并往往可以让数学家对该领域有更深刻的理解。

如果两个领域是同构的,那么其上的对象会有相似的属性和操作,对某个领域成立的命题在另一个领域上也就成立。这就使得理解和处理新领域的对象变得容易,并可以对新领域有更深刻的理解。

百度百科: http://baike.baidu.com/link?url=6ZY_zvS8sPclyKvA1LDAZIFsY5bz6W-7N0WPV_5Xkdg4Ntqws6fYmX9lPLKA5bZqfEJmmSyU4di0hbdLGuUHlq

4.4 Representation

有了Atom Rule(Predication、Action、Result)和Combination Rule(Or、And),计算模型就有了,接下来就可以基于这个计算模型来考虑如何表述(represent)这些规则和规则之间的关系。

4.4.1 Atom Rules

r1_3 atom times 3 to_fizz
r1_5 atom times 5 to_buzz
r1_7 atom times 7 to_whizz

以Rule1的第1个子规则为例,名字为r1_3,它是一个原子Rule(atom)。构造原子Rule需要Predication和Action。times是Predication,参数为3,to_fizz是Action,不带参数。r1_5、r1_7同理。

4.4.2 Rule 1

r1 or r1_3 r1_5 r1_7

Rule1的名字为r1,它是一个or组合子Rule,由r1_3、r1_5、r1_7这3个子Rule“or”组合而成。

4.4.3 Rule 2

r1_357 and r1_3 r1_5 r1_7
r1_35 and r1_3 r1_5
r1_37 and r1_3 r1_7
r1_57 and r1_5 r1_7

r2 or r1_357 r1_35 r1_37 r1_57

要构造Rule2,先要构造4个子规则。以第1个子规则为例,名字为r1_357,它是一个and组合子Rule,由r1_3、r1_5、r1_7这3个子Rule“and”组合而成。r1_35、r1_37、r1_57同理。

Rule2的名字为r2,它由r1_357、r1_35、r1_37、r1_57这4个子Rule“and”组合而成。

4.4.4 Rule 3

r3 atom contains 3 to_fizz

Rule3名字为r3,它是一个原子Rule(atom)。构造原子Rule需要Predication和Action。contains是Predication,参数为3,to_fizz是Action,不带参数。

4.4.5 Default Rule

rd atom always_true to_str

Default Rule的名字为rd,它是一个原子Rule(atom)。构造原子Rule需要Predication和Action。always_true是Predication,不带参数,to_str是Action,不带参数。

4.4.6 Relations among Rule 1 Rule 2 Rule 3 and Default Rule

最后,Rule1、Rule2、Rule3和Default Rule之间的关系表达如下:

spec or r3 r2 r1 rd

把它们之间的关系命名为spec,它是一个or组合子Rule,由r3、r2、r1、rd这4个子Rule“or”组合而成。

4.4.7 Specification

把上面的表述完整地写出来,就是一份规格说明(Specification)。

r1_3 atom times 3 to_fizz
r1_5 atom times 5 to_buzz
r1_7 atom times 7 to_whizz

r1 or r1_3 r1_5 r1_7

r1_357 and r1_3 r1_5 r1_7
r1_35 and r1_3 r1_5
r1_37 and r1_3 r1_7
r1_57 and r1_5 r1_7

r2 or r1_357 r1_35 r1_37 r1_57

r3 atom contains 3 to_fizz

rd atom always_true to_str

spec or r3 r2 r1 rd

可以将这份Specification理解为一段代码,写这份Specification可以理解为是在编程。atom、and、or、times、contains、always_true、to_fizz、to_buzz、to_whizz、to_str是我们专门为这个问题领域编写的特定语言(DSL)。我们针对这个问题领域编写DSL,并用DSL编程,和实现语言无关。

用DSL编写的程序怎么运行呢?可以用实现语言写一个解析器,将程序转换为实现语言中的计算对象,再编写一个解释器或编译器执行这个程序即可。

我们也可以提供一套API,不过就和实现语言相关了。以Python语言为例,Atom、AND、OR、Times、Contains、AlwaysTrue、ToFizz、ToBuzz、ToWhizz、ToStr都是API,用这些API编程写出的Specification如下:

def spec():
    r1_3 = Atom(Times(3), ToFizz())
    r1_5 = Atom(Times(5), ToBuzz())
    r1_7 = Atom(Times(7), ToWhizz())

    r1 = OR3(r1_3, r1_5, r1_7)
    r2 = OR4(AND3(r1_3, r1_5, r1_7),
             AND(r1_3, r1_5),
             AND(r1_3, r1_7),
             AND(r1_5, r1_7))
    r3 = Atom(Contains(3), ToFizz())
    rd = Atom(AlwaysTrue(), ToStr())
    
    return OR4(r3, r2, r1, rd)

无论是用DSL还是API编写的Spec都是精确无二义、清晰、和需求对应的。

以上内容就是设计的结果。通过前面的分析和定义,把问题转换成上面的表述,把问题需求变成规格说明,变成一个设计

4.5 Design Summary

做一个设计小结。

4.5.1 Mapping to Isomorphic Domain

我们在做领域驱动设计时,首先考虑是否能将问题领域映射到一个熟悉的同构领域。

如果能,就可以借用那个领域的机制来表达问题领域的概念,而不是重新发明。

如果能找到这样一种同构领域,应该是一个最好的结果,如果实在找不到,再自己发明。但一般来说,最终一定能在数学层面上找到一个同构领域,有可能只是没找到而已。

4.5.2 Core Concepts & Semantic Domain

找到问题域中的核心概念,把问题域映射到语义域(Semantic Domain),并站在语义域考虑如何执行,而不去考虑底层如何实现。

在思考问题时,不从通用语言层面思考(例如Java类、接口、继承等),正如我们在用Java/Python语言写程序时不会像用汇编语言写程序那样思考,而是从问题本身找到它的语义域(Semantic Domain),这个域最好是一个数学上的。

语义的计算模型是核心,DSL只是皮毛。

这是一种设计方法,也是本系列课程的核心所在。

这种设计方法的适用性范围很广,从一个小的模块,到一个大的系统,到一个平台,到整个业务架构都是适用的。一旦掌握了这种方法,再去看问题,就会和以前的看法完全不一样,能看到一些更为本质的东西。而且,用这种方法做出来的设计不仅能够很好地满足目前的需求,对于日后的系统演化和发展都是适用的。

4.5.3 Type-based Design

“基于类型设计”是一种比较好的设计方法。

用这种设计方法可以思考类型、而不考虑如何实现。

类型是一种在概念层面上的契约。

5 Implementation

实现:选择实现技术把逻辑框架的软件模型实现出来。

在本课程中,我们用API方式实现,在后续的《FizzBuzzWhizz Reloaded》课程中,我们用DSL方式实现。

def spec():
    r1_3 = Atom(Times(3), ToFizz())
    r1_5 = Atom(Times(5), ToBuzz())
    r1_7 = Atom(Times(7), ToWhizz())

    r1 = OR3(r1_3, r1_5, r1_7)
    r2 = OR4(AND3(r1_3, r1_5, r1_7),
             AND(r1_3, r1_5),
             AND(r1_3, r1_7),
             AND(r1_5, r1_7))
    r3 = Atom(Contains(3), ToFizz())
    rd = Atom(AlwaysTrue(), ToStr())
    
    return OR4(r3, r2, r1, rd)

实现步骤(Steps):

  1. Atom Rules 实现r1_3、r1_5、r1_7这三个Atom Rule。构造Atom Rule需要Predication和Action,所以要先实现Times这个Predication以及ToFizz、ToBuzz和ToWhizz这三个Action。

  2. OR Rule,Rule 1
    实现OR Rule,构造出Rule1。

  3. AND Rule,Rule 2
    实现AND Rule,构造出Rule2。

  4. Rule 3
    实现Contains Predication,构造出Rule3。

  5. Default Rule
    实现AlwaysTrue Predication和ToStr Action,构造出Default Rule。

  6. Specification
    用API编写spec。

  7. FizzBuzzWhizz
    最终实现FizzBuzzWhizz。

代码路径:original

5.1 Atom Rules: r1_3 r1_5 r1_7

实现r1_3、r1_5、r1_7这三个Atom Rule。构造Atom Rule需要Predication和Action,所以要先实现Times这个Predication以及ToFizz、ToBuzz和ToWhizz这三个Action。

5.1.1 Predication: Times

test_all.py

import unittest

from tests.predication.test_predication import TestTimes

if __name__ == '__main__':
    unittest.main()

test_predication.py

import unittest

from predication import Times

class TestTimes(unittest.TestCase):
    def test_times(self):
        times3 = Times(3)
        self.assertTrue(times3.predicate(6))
        self.assertFalse(times3.predicate(5))

predication.py

class Predication(object):
    def predicate(self, n):
        pass

class Times(Predication):
    def __init__(self, base):
        self.base = base

    def predicate(self, n):
        return n % self.base == 0

5.1.2 Action: ToFizz ToBuzz ToWhizz

test_all.py

from tests.test_action import TestToFizz, TestToBuzz, TestToWhizz

test_action.py

import unittest

from action import ToFizz, ToBuzz, ToWhizz

class TestToFizz(unittest.TestCase):
    def test_toFizz(self):
        toFizz = ToFizz()
        self.assertEquals('Fizz', toFizz.act(3))

class TestToBuzz(unittest.TestCase):
    def test_toBuzz(self):
        toBuzz = ToBuzz()
        self.assertEquals('Buzz', toBuzz.act(5))

class TestToWhizz(unittest.TestCase):
    def test_toWhizz(self):
        toWhizz = ToWhizz()
        self.assertEquals('Whizz', toWhizz.act(7))

action.py

class Action(object):
    def act(self, n):
        pass

class ToFizz(Action):
    def act(self, n):
        return 'Fizz'

class ToBuzz(Action):
    def act(self, n):
        return 'Buzz'

class ToWhizz(Action):
    def act(self, n):
        return 'Whizz'

5.1.3 Atom: r1_3 r1_5 r1_7

5.1.3.1 r1_3

test_all.py

from tests.test_rule import TestAtom

test_rule.py TestAtom Class

import unittest

from rule import Atom
from predication import Times
from action import ToFizz

class TestAtom(unittest.TestCase):
    def test_atom_rule_1_3(self):
        r1_3 = Atom(Times(3), ToFizz())
        self.assertEquals((True, 'Fizz'), r1_3.apply(3))
        self.assertEquals((False, ''), r1_3.apply(4))

rule.py Atom Class

class Rule(object):
    def apply(self, n):
        pass

class Atom(Rule):
    def __init__(self, predication, action):
        self.predication = predication
        self.action = action

    def apply(self, n):
        if self.predication.predicate(n):
            return True, self.action.act(n)

        return False, ''

5.1.3.2 r1_5 r1_7

继续补充r1_5和r1_7的测试用例。

test_rule.py TestRule Class

from action import ToFizz, ToBuzz, ToWhizz

    def test_atom_rule_1_5(self):
        r1_5 = Atom(Times(5), ToBuzz())
        self.assertEquals((True, 'Buzz'), r1_5.apply(10))
        self.assertEquals((False, ''), r1_5.apply(11))
    
    def test_atom_rule_1_7(self):
        r1_7 = Atom(Times(7), ToWhizz())
        self.assertEquals((True, 'Whizz'), r1_7.apply(14))
        self.assertEquals((False, ''), r1_7.apply(13))

5.2 OR Rule & Rule 1

实现OR Rule,构造出Rule1。

5.2.1 OR Rule

test_all.py

from tests.test_rule import TestAtom, TestOR

test_rule.py TestOR Class

from rule import Atom, OR

class TestOR(unittest.TestCase):
    def test_or_rule(self):
        r1_3 = Atom(Times(3), ToFizz())
        r1_5 = Atom(Times(5), ToBuzz())
        
        or_35 = OR(r1_3, r1_5)
        self.assertEquals((True, 'Fizz'), or_35.apply(6))
        self.assertEquals((True, 'Buzz'), or_35.apply(10))
        self.assertEquals((True, 'Fizz'), or_35.apply(15))
        self.assertEquals((False, ''), or_35.apply(7))

rule.py OR Class

class OR(Rule):
    def __init__(self, rule1, rule2):
        self.rule1 = rule1
        self.rule2 = rule2

    def apply(self, n):
        result1 = self.rule1.apply(n)
        if result1[0]:
            return result1

        return self.rule2.apply(n)

5.2.2 Rule 1

test_all.py

from tests.test_rule import TestAtom, TestOR, TestRule

test_rule.py TestRule Class

from rule import Atom, OR, OR3

class TestRule(unittest.TestCase):
    def test_rule_1(self):
        r1_3 = Atom(Times(3), ToFizz())
        r1_5 = Atom(Times(5), ToBuzz())
        r1_7 = Atom(Times(7), ToWhizz())
        
        r1 = OR3(r1_3, r1_5, r1_7)
        self.assertEquals((True, 'Fizz'), r1.apply(6))
        self.assertEquals((True, 'Buzz'), r1.apply(10))
        self.assertEquals((True, 'Whizz'), r1.apply(14))
        self.assertEquals((False, ''), r1.apply(13))

rule.py

def OR3(rule1, rule2, rule3):
    return OR(rule1, OR(rule2, rule3))

5.3 AND Rule & Rule 2

实现AND Rule,构造出Rule2。

5.3.1 AND Rule

test_all.py

from tests.test_rule import TestAtom, TestOR, TestAND, TestRule

test_rule.py TestAND Class

from rule import Atom, OR, AND, OR3

class TestAND(unittest.TestCase):
    def test_and_rule(self):
        r1_3 = Atom(Times(3), ToFizz())
        r1_5 = Atom(Times(5), ToBuzz())

        and_35 = AND(r1_3, r1_5)
        self.assertEquals((False, ''), and_35.apply(3))
        self.assertEquals((False, ''), and_35.apply(5))
        self.assertEquals((True, 'FizzBuzz'), and_35.apply(15))
        self.assertEquals((False, ''), and_35.apply(16))

rule.py AND Class

class AND(Rule):
    def __init__(self, rule1, rule2):
        self.rule1 = rule1
        self.rule2 = rule2

    def apply(self, n):
        result1 = self.rule1.apply(n)
        if not result1[0]:
            return False, ''

        result2 = self.rule2.apply(n)
        if not result2[0]:
            return False, ''

        return True, ''.join([result1[1], result2[1]])

5.3.2 Rule 2

test_rule.py TestRule Class

from rule import Atom, OR, AND, OR3, OR4, AND3

    def test_rule_2(self):
        r1_3 = Atom(Times(3), ToFizz())
        r1_5 = Atom(Times(5), ToBuzz())
        r1_7 = Atom(Times(7), ToWhizz())
        
        r2 = OR4(AND3(r1_3, r1_5, r1_7),
                 AND(r1_3, r1_5),
                 AND(r1_3, r1_7),
                 AND(r1_5, r1_7))
        self.assertEquals((False, ''), r2.apply(3))
        self.assertEquals((False, ''), r2.apply(5))
        self.assertEquals((False, ''), r2.apply(7))
        self.assertEquals((True, 'FizzBuzzWhizz'), r2.apply(3*5*7))
        self.assertEquals((False, ''), r2.apply(104))
        self.assertEquals((True, 'FizzBuzz'), r2.apply(15))
        self.assertEquals((False, ''), r2.apply(14))
        self.assertEquals((True, 'FizzWhizz'), r2.apply(21))
        self.assertEquals((False, ''), r2.apply(22))
        self.assertEquals((True, 'BuzzWhizz'), r2.apply(35))
        self.assertEquals((False, ''), r2.apply(34))

rule.py

def OR4(rule1, rule2, rule3, rule4):
    return OR(rule1, OR3(rule2, rule3, rule4))

def AND3(rule1, rule2, rule3):
    return AND(rule1, AND(rule2, rule3))

5.4 Atom Rule: Rule 3

实现Contains Predication,构造出Rule3。

5.4.1 Predication: Contains

test_all.py

from tests.test_predication import TestTimes, TestContains

test_predication.py

from predication import Times, Contains

class TestContains(unittest.TestCase):
    def test_contains(self):
        contains3 = Contains(3)
        self.assertTrue(contains3.predicate(13))
        self.assertTrue(contains3.predicate(35))
        self.assertTrue(contains3.predicate(300))
        self.assertFalse(contains3.predicate(24))

predication.py

class Contains(Predication):
    def __init__(self, digit):
        self.digit = digit

    def predicate(self, n):
        return str(self.digit) in str(n)

5.4.2 Atom: Rule 3

test_rule.py TestRule Class

from predication import Times, Contains

    def test_rule_3(self):
        r3 = Atom(Contains(3), ToFizz())
        self.assertEquals((True, 'Fizz'), r3.apply(3))
        self.assertEquals((True, 'Fizz'), r3.apply(13))
        self.assertEquals((True, 'Fizz'), r3.apply(31))
        self.assertEquals((False, ''), r3.apply(24))

5.5 Atom Rule: Default Rule

实现AlwaysTrue Predication和ToStr Action,构造出Default Rule。

5.5.1 Predication: AlwaysTrue

test_all.py

from tests.test_predication import TestTimes, TestContains, TestAlwaysTrue

test_predication.py

from predication import Times, Contains, AlwaysTrue

class TestAlwaysTrue(unittest.TestCase):
    def test_alwaysTrue(self):
        alwaysTrue = AlwaysTrue()
        self.assertTrue(alwaysTrue.predicate(1))
        self.assertTrue(alwaysTrue.predicate(3))
        self.assertTrue(alwaysTrue.predicate(5))

predication.py

class AlwaysTrue(Predication):
    def predicate(self, n):
        return True

5.5.2 Action: ToStr

test_all.py

from tests.test_action import TestToFizz, TestToBuzz, TestToWhizz, TestToStr

test_action.py

from action import ToFizz, ToBuzz, ToWhizz, ToStr

class TestToStr(unittest.TestCase):
    def test_toStr(self):
        toStr = ToStr()
        self.assertEquals('1', toStr.act(1))
        self.assertEquals('10', toStr.act(10))

action.py

class ToStr(Action):
    def act(self, n):
        return str(n)

5.5.4 Atom: Default Rule

test_rule.py TestRule Class

from predication import Times, Contains, AlwaysTrue
from action import ToFizz, ToBuzz, ToWhizz, ToStr

    def test_default_rule(self):
        rd = Atom(AlwaysTrue(), ToStr())
        self.assertEquals((True, '1'), rd.apply(1))
        self.assertEquals((True, '3'), rd.apply(3))

5.6 Specification

用API编写Specification。

test_rule.py TestRule Class

from rule import Atom, OR, AND, OR3, OR4, AND3, spec

    def test_spec(self):
        s = spec()
        self.assertEquals((True, 'Fizz'), s.apply(35))
        self.assertEquals((True, 'FizzBuzz'), s.apply(15))
        self.assertEquals((True, 'FizzWhizz'), s.apply(21))
        self.assertEquals((True, 'BuzzWhizz'), s.apply(70))
        self.assertEquals((True, 'Fizz'), s.apply(9))
        self.assertEquals((True, '1'), s.apply(1))

rule.py

from predication import Times, Contains, AlwaysTrue
from action import ToFizz, ToBuzz, ToWhizz, ToStr

def spec():
    r1_3 = Atom(Times(3), ToFizz())
    r1_5 = Atom(Times(5), ToBuzz())
    r1_7 = Atom(Times(7), ToWhizz())

    r1 = OR3(r1_3, r1_5, r1_7)
    r2 = OR4(AND3(r1_3, r1_5, r1_7),
             AND(r1_3, r1_5),
             AND(r1_3, r1_7),
             AND(r1_5, r1_7))
    r3 = Atom(Contains(3), ToFizz())
    rd = Atom(AlwaysTrue(), ToStr())
    
    return OR4(r3, r2, r1, rd)

5.7 FizzBuzzWhizz

最终实现FizzBuzzWhizz。

fizzbuzzwhizz.py

from rule import spec

def run():
    s = spec()
    results = [s.apply(n) for n in range(1, 101)]
    output(results)

def output(results):
    print [result[1] for result in results]

if __name__ == '__main__':
    run()

6 Summary

6.1 Global View

summary global view

6.2 What is Design?

设计就是把问题变成可计算的。

design computable

6.3 What is Good Design?

设计出的计算模型所提供的语义和问题领域的根本需求是否匹配,匹配就是好的设计,不匹配就是不好的设计。

good design

将FizzBuzzWhizz这个问题领域映射到了布尔代数的领域,借用了布尔代数的“OR”和“AND”来表达Rule之间的Relation。

6.4 How to do Design?

how to do design

  • 对问题领域进行深入分析,发现问题领域的核心需求;
  • 通过核心需求驱动出计算模型和语义;
  • 再围绕这个计算模型提供一套语言,给外面的人使用这个计算模型提供一个接口,这个接口可以是API、可以是数据表达、也可以是语言;
  • 最终要实现这个计算模型,实现的方法有解释器和编译器两种。

这就是DDD!这才是DDD!
这就是DSL!这才是DSL!

6.5 What is Programming?

“编程”不过是在某个计算模型上用某种语言去表达计算。 用DSL编程不过是在问题领域的计算模型上用DSL来表达计算。
计算模型是相应领域中的“通用机器”。 问题领域的计算模型是问题领域的通用机器。
编程语言不过是描述计算机器的一种方法。 DSL描述的是DSL语言的计算机器。
程序是对特定机器的描述,这个特定机器可以被通用机器仿真。 用DSL程序实现了问题领域中的某个功能,这个DSL程序就是对这个功能(特定机器)的描述。

6.6 Design & Programming

design and programming

  • 从问题领域导出核心需求,得到领域的计算模型(通用计算机器),在上面可以包装一个DSL语言或者数据或者API,基于这些开发程序和应用。
  • 领域的计算模型和通用语言的计算模型之间存在鸿沟,可以用解释器或者编译器来填补。
  • 解释器和编译器听起来很复杂,其实思想很简单,而且我们没有必要实现一个工业级别的、非常全面的解释器和编译器,只要借鉴这个思想实现我们的计算模型就够了。

Common Languages(Java/C) UML对应Java/C/Python通用语言。
Common Languages(Java/C) UM对应Java/C/Python通用语言提供的计算模型(通用计算机器)。

6.7 How to improve Design Capability?

提升设计能力的根本在于提升计算模型构造和语义定义能力。

Thinking, thinking, thinking …
Practice, practice and practice …
No shortcuts.

7 Homework

  1. 【必选】新增特殊数:8,除了以下规则有调整以外,题目中的其它内容不变:
  • 规则1增加:如果所报数字是第四个特殊数(8)的倍数,那么要说Hazz;
  • 规则2增加:如果同时是三个特殊数的倍数,那么要说FizzBuzzWhizz,以此类推。如果同时是四个特殊数的倍数,那么要说FizzBuzzWhizzHazz。
  1. 【可选】用函数式编程方式实现FizzBuzzWhizz。

8 My Homework

第1题做了两个版本,路径如下:
直白的:eight.straightforward
带组合计算的:eight.combination

第2题参见FizzBuzzWhizz Reloaded,路径:functional。

8.1 straightforward

8.1.1 ToHazz

test_all.py

from tests.test_action import TestToFizz, TestToBuzz, TestToWhizz, TestToStr, TestToHazz

test_action.py

from action import ToFizz, ToBuzz, ToWhizz, ToStr, ToHazz

class TestToHazz(unittest.TestCase):
    def test_toHazz(self):
        toHazz = ToHazz()
        self.assertEquals("Hazz", toHazz.act(8))

action.py

class ToHazz(Action):
    def act(self, n):
        return 'Hazz'

8.1.2 Rule 1_8

test_rule.py TestAtom Class

from action import ToFizz, ToBuzz, ToWhizz, ToStr, ToHazz

    def test_atom_rule_1_8(self):
        r1_8 = Atom(Times(8), ToHazz())
        self.assertEquals((True, 'Hazz'), r1_8.apply(16))
        self.assertEquals((False, ''), r1_8.apply(13))

8.1.3 Rule 1

test_rule.py TestRule Class

    def test_rule_1(self):
        r1_3 = Atom(Times(3), ToFizz())
        r1_5 = Atom(Times(5), ToBuzz())
        r1_7 = Atom(Times(7), ToWhizz())
        r1_8 = Atom(Times(8), ToHazz())
        
        r1 = OR4(r1_3, r1_5, r1_7, r1_8)
        self.assertEquals((True, 'Fizz'), r1.apply(6))
        self.assertEquals((True, 'Buzz'), r1.apply(10))
        self.assertEquals((True, 'Whizz'), r1.apply(14))
        self.assertEquals((True, 'Hazz'), r1.apply(16))
        self.assertEquals((False, ''), r1.apply(13))

8.1.4 Rule 2

test_rule.py TestRule Class

from rule import Atom, OR, AND, OR3, OR4, ORN, AND3, AND4, spec

    def test_rule_2(self):
        r1_3 = Atom(Times(3), ToFizz())
        r1_5 = Atom(Times(5), ToBuzz())
        r1_7 = Atom(Times(7), ToWhizz())
        r1_8 = Atom(Times(8), ToHazz())
        
        r2 = ORN(AND4(r1_3, r1_5, r1_7, r1_8),
                 AND3(r1_3, r1_5, r1_7),
                 AND3(r1_3, r1_5, r1_8),
                 AND3(r1_3, r1_7, r1_8),
                 AND3(r1_5, r1_7, r1_8),
                 AND(r1_3, r1_5),
                 AND(r1_3, r1_7),
                 AND(r1_3, r1_8),
                 AND(r1_5, r1_7),
                 AND(r1_5, r1_8),
                 AND(r1_7, r1_8))

        self.assertEquals((False, ''), r2.apply(3))
        self.assertEquals((False, ''), r2.apply(5))
        self.assertEquals((False, ''), r2.apply(7))
        self.assertEquals((False, ''), r2.apply(8))
        self.assertEquals((True, 'FizzBuzzWhizzHazz'), r2.apply(3*5*7*8))
        self.assertEquals((False, ''), r2.apply(841))
        self.assertEquals((True, 'FizzBuzzWhizz'), r2.apply(3*5*7))
        self.assertEquals((False, ''), r2.apply(104))
        self.assertEquals((True, 'FizzBuzzHazz'), r2.apply(3*5*8))
        self.assertEquals((False, ''), r2.apply(121))
        self.assertEquals((True, 'FizzWhizzHazz'), r2.apply(3*7*8))
        self.assertEquals((False, ''), r2.apply(167))
        self.assertEquals((True, 'BuzzWhizzHazz'), r2.apply(5*7*8))
        self.assertEquals((False, ''), r2.apply(281))
        self.assertEquals((True, 'FizzBuzz'), r2.apply(15))
        self.assertEquals((False, ''), r2.apply(14))
        self.assertEquals((True, 'FizzWhizz'), r2.apply(21))
        self.assertEquals((False, ''), r2.apply(22))
        self.assertEquals((True, 'FizzHazz'), r2.apply(24))
        self.assertEquals((False, ''), r2.apply(23))
        self.assertEquals((True, 'BuzzWhizz'), r2.apply(35))
        self.assertEquals((False, ''), r2.apply(34))
        self.assertEquals((True, 'BuzzHazz'), r2.apply(40))
        self.assertEquals((False, ''), r2.apply(41))
        self.assertEquals((True, 'WhizzHazz'), r2.apply(56))
        self.assertEquals((False, ''), r2.apply(55))

rule.py,增加AND4()、ORN()、_ORN()函数。

def AND4(rule1, rule2, rule3, rule4):
    return AND(rule1, AND3(rule2, rule3, rule4))

def ORN(*rules):
    return _ORN(list(rules))

def _ORN(rules):
    if len(rules) == 1:
        return rules[0]

    return OR(rules[0], _ORN(rules[1:]))

增加ORN()函数后,OR3()和OR4()函数就可以删掉了。test_rule.TestRule类中调用OR3()和OR4()的地方全部改为调用ORN()。

test_rule.py

from rule import Atom, OR, AND, ORN, AND3, AND4, spec

    def test_rule_1(self):
        r1_3 = Atom(Times(3), ToFizz())
        r1_5 = Atom(Times(5), ToBuzz())
        r1_7 = Atom(Times(7), ToWhizz())
        r1_8 = Atom(Times(8), ToHazz())
        
        r1 = ORN(r1_3, r1_5, r1_7, r1_8)
        self.assertEquals((True, 'Fizz'), r1.apply(6))
        self.assertEquals((True, 'Buzz'), r1.apply(10))
        self.assertEquals((True, 'Whizz'), r1.apply(14))
        self.assertEquals((True, 'Hazz'), r1.apply(16))
        self.assertEquals((False, ''), r1.apply(13))

8.1.5 Specification

test_rule.py TestRule Class

    def test_spec(self):
        s = spec()
        self.assertEquals((True, 'Fizz'), s.apply(35))
        self.assertEquals((True, 'FizzBuzzWhizzHazz'), s.apply(3*5*7*8))
        self.assertEquals((True, 'FizzBuzzWhizz'), s.apply(3*5*7))
        self.assertEquals((True, 'FizzBuzzHazz'), s.apply(3*5*8))
        self.assertEquals((True, 'FizzWhizzHazz'), s.apply(3*7*8))
        self.assertEquals((True, 'BuzzWhizzHazz'), s.apply(5*7*8))
        self.assertEquals((True, 'FizzBuzz'), s.apply(15))
        self.assertEquals((True, 'FizzWhizz'), s.apply(21))
        self.assertEquals((True, 'FizzHazz'), s.apply(24))
        self.assertEquals((True, 'BuzzWhizz'), s.apply(70))
        self.assertEquals((True, 'BuzzHazz'), s.apply(40))
        self.assertEquals((True, 'WhizzHazz'), s.apply(56))
        self.assertEquals((True, 'Fizz'), s.apply(9))
        self.assertEquals((True, 'Buzz'), s.apply(5))
        self.assertEquals((True, 'Whizz'), s.apply(7))
        self.assertEquals((True, 'Hazz'), s.apply(8))
        self.assertEquals((True, '1'), s.apply(1))

rule.py

from action import ToFizz, ToBuzz, ToWhizz, ToStr, ToHazz

def spec():
    r1_3 = Atom(Times(3), ToFizz())
    r1_5 = Atom(Times(5), ToBuzz())
    r1_7 = Atom(Times(7), ToWhizz())
    r1_8 = Atom(Times(8), ToHazz())

    r1 = ORN(r1_3, r1_5, r1_7, r1_8)
    r2 = ORN(AND4(r1_3, r1_5, r1_7, r1_8),
             AND3(r1_3, r1_5, r1_7),
             AND3(r1_3, r1_5, r1_8),
             AND3(r1_3, r1_7, r1_8),
             AND3(r1_5, r1_7, r1_8),
             AND(r1_3, r1_5),
             AND(r1_3, r1_7),
             AND(r1_3, r1_8),
             AND(r1_5, r1_7),
             AND(r1_5, r1_8),
             AND(r1_7, r1_8))
    r3 = Atom(Contains(3), ToFizz())
    rd = Atom(AlwaysTrue(), ToStr())
    
    return ORN(r3, r2, r1, rd)

8.2 combination

在straightforward版本中,仔细观察spec中的r2,可以借用数学中的“组合”概念重构。

从元素个数为m的集合中任意抽取n个元素组成的集合的集合,记作C(m, n)。其中,C是Combination(组合),1≤n≤m。r1_3、r1_5、r1_7和r1_8构成一个集合,那么r2由该集合的C(4, 4)、C(4, 3)、C(4, 2)拉平(flatten)以后的所有元素相or而成。

8.2.1 combinate & flatten

先实现combinate和flatten方法:

  • combinate: (List(m), n) -> ListOfList
    求列表的C(m, n)组合,列表长度为m,记为List(m),1≤n≤m。输出元素为列表的列表。
  • flatten: ListOfList -> List
    把列表的元素(列表)拉平。

test_rule.py TestRule Class

from rule import Atom, OR, AND, ORN, AND3, AND4, spec, combinate, flatten

    def test_combinate(self):
        lst = [1, 2, 3, 4]
        self.assertEquals([[1], [2], [3], [4]], combinate(lst, 1))
        self.assertEquals([[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]], combinate(lst, 2))
        self.assertEquals([[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]], combinate(lst, 3))
        self.assertEquals([[1, 2, 3, 4]], combinate(lst, 4))

    def test_flatten(self):
        lstOfLst = [[1, 2], [3, 4, 5], [6]]
        self.assertEquals([1, 2, 3, 4, 5, 6], flatten(lstOfLst))
        
        lstOfLst = [[[1, 2], [3, 4]], [[5], [6]]]
        self.assertEquals([[1, 2], [3, 4], [5], [6]], flatten(lstOfLst))

rule.py

import itertools

def combinate(lst, n):
    return [list(ele) for ele in itertools.combinations(lst, n)]

def flatten(embeddedlist):
    return list(itertools.chain.from_iterable(embeddedlist))

8.2.2 Specification

为了不区分AND3、AND4和AND,提供ANDN()和_ANDN()函数,并删除 AND3()和AND4()方法。

rule.py

def ANDN(*rules):
    return _ANDN(list(rules))

def _ANDN(rules):
    if len(rules) == 1:
        return rules[0]

    return AND(rules[0], _ANDN(rules[1:]))

test_rule.py TestRule Class

from rule import Atom, OR, AND, ORN, _ORN, ANDN, _ANDN, spec, combinate, flatten

    def test_rule_2(self):
        r1_3 = Atom(Times(3), ToFizz())
        r1_5 = Atom(Times(5), ToBuzz())
        r1_7 = Atom(Times(7), ToWhizz())
        r1_8 = Atom(Times(8), ToHazz())

        atom_rules = [r1_3, r1_5, r1_7, r1_8]
        combinated_rules = flatten([combinate(atom_rules, 4), 
                                    combinate(atom_rules, 3), 
                                    combinate(atom_rules, 2)])
        r2 = _ORN([_ANDN(rules) for rules in combinated_rules])
        
        self.assertEquals((False, ''), r2.apply(3))
        self.assertEquals((False, ''), r2.apply(5))
        self.assertEquals((False, ''), r2.apply(7))
        self.assertEquals((False, ''), r2.apply(8))
        self.assertEquals((True, 'FizzBuzzWhizzHazz'), r2.apply(3*5*7*8))
        self.assertEquals((False, ''), r2.apply(841))
        self.assertEquals((True, 'FizzBuzzWhizz'), r2.apply(3*5*7))
        self.assertEquals((False, ''), r2.apply(104))
        self.assertEquals((True, 'FizzBuzzHazz'), r2.apply(3*5*8))
        self.assertEquals((False, ''), r2.apply(121))
        self.assertEquals((True, 'FizzWhizzHazz'), r2.apply(3*7*8))
        self.assertEquals((False, ''), r2.apply(167))
        self.assertEquals((True, 'BuzzWhizzHazz'), r2.apply(5*7*8))
        self.assertEquals((False, ''), r2.apply(281))
        self.assertEquals((True, 'FizzBuzz'), r2.apply(15))
        self.assertEquals((False, ''), r2.apply(14))
        self.assertEquals((True, 'FizzWhizz'), r2.apply(21))
        self.assertEquals((False, ''), r2.apply(22))
        self.assertEquals((True, 'FizzHazz'), r2.apply(24))
        self.assertEquals((False, ''), r2.apply(23))
        self.assertEquals((True, 'BuzzWhizz'), r2.apply(35))
        self.assertEquals((False, ''), r2.apply(34))
        self.assertEquals((True, 'BuzzHazz'), r2.apply(40))
        self.assertEquals((False, ''), r2.apply(41))
        self.assertEquals((True, 'WhizzHazz'), r2.apply(56))
        self.assertEquals((False, ''), r2.apply(55))

rule.py

def spec():
    r1_3 = Atom(Times(3), ToFizz())
    r1_5 = Atom(Times(5), ToBuzz())
    r1_7 = Atom(Times(7), ToWhizz())
    r1_8 = Atom(Times(8), ToHazz())

    r1 = ORN(r1_3, r1_5, r1_7, r1_8)

    atom_rules = [r1_3, r1_5, r1_7, r1_8]
    combinated_rules = flatten([combinate(atom_rules, 4), 
                                combinate(atom_rules, 3), 
                                combinate(atom_rules, 2)])
    r2 = _ORN([_ANDN(rules) for rules in combinated_rules])

    r3 = Atom(Contains(3), ToFizz())
    rd = Atom(AlwaysTrue(), ToStr())
    
    return ORN(r3, r2, r1, rd)

rule.py中的_ORN()和_ANDN()函数有重复,通过函数式编程范式消除重复。

def _ORN(rules):
    return _combine(rules, OR)

def _ANDN(rules):
    return _combine(rules, AND)

def _combine(rules, func):
    if len(rules) == 1:
        return rules[0]

    return func(rules[0], _combine(rules[1:], func))
⚠️ **GitHub.com Fallback** ⚠️