201712 DDD DSL Design War FizzBuzzWhizz in Python - xiaoxianfaye/Courses GitHub Wiki
- 1 Problem
- 2 Showcase & Discuss
- 3 Analysis
- 4 Design
- 5 Implementation
- 6 Summary
- 7 Homework
- 8 My Homework
你是一名体育老师,在某次课距离下课还有五分钟时,你决定搞一个游戏。此时有100名学生在上课。游戏的规则是:
-
设定三个不同的特殊数:3、5、7。
-
让所有学生排成一队,然后按顺序报数。
-
学生报数时,如果所报数字是第一个特殊数(3)的倍数,那么不能说该数字,而要说Fizz;如果所报数字是第二个特殊数(5)的倍数,那么要说Buzz;如果所报数字是第三个特殊数(7)的倍数,那么要说Whizz。
-
学生报数时,如果所报数字同时是两个特殊数的倍数情况下,也要特殊处理,比如第一个特殊数和第二个特殊数的倍数,那么不能说该数字,而是要说FizzBuzz, 以此类推。如果同时是三个特殊数的倍数,那么要说FizzBuzzWhizz。
-
学生报数时,如果所报数字包含了第一个特殊数,那么也不能说该数字,而是要说相应的单词,比如本例中第一个特殊数是3,那么要报13的同学应该说Fizz。 如果数字中包含了第一个特殊数,那么忽略规则3和规则4,比如要报35的同学只报Fizz,不报BuzzWhizz。
请编写一个程序来模拟这个游戏。
请学员展示自己之前的设计思路和实现方式,大家可以互相点评、讨论。以学员为主,讲师为辅。
分析:定义清楚问题是什么。
-
根据题目的第1点,特殊数需要作为输入条件吗?
不需要,至少现在不需要,不是核心问题。 -
根据题目的第2点,程序要做什么?输入是什么?输出是什么?
报数,整数到字符串的转换。输入1到100的整数,输出各自对应的字符串。 -
题目的第3点中描述的是什么?
规则。定义为规则1。 -
题目的第4点中描述的规则定义为规则2。在“两个特殊数的倍数”的情况中,只有同时是3和5的倍数要处理吗?
还要处理同时是3和7的倍数、同时是5和7的倍数。 -
题目的第5点中描述的规则定义为规则3。
-
除了规则1、规则2、规则3,还有没有其它规则?
默认规则。如果规则1、规则2、规则3都不满足,直接报数本身。 -
规则1、规则2和规则3和默认规则之间是什么关系?
按优先级由高到低:Rule3 -> Rule2 -> Rule1 -> Default Rule。
设计:问题分析清楚以后,提出解决问题的逻辑框架。
Rule(规则)和Relation(规则之间的关系),是这个问题领域的核心概念。
一个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)”。
Rule之间有优先级,优先级其实就是Relation(关系)。
那么,Relation到底是什么意思?什么是Relation?Relation的语义(Semantics)是什么?
题目中的第3点、第4点和第5点都是Rule,它们之间有关系。这些Rule各自还有很多子Rule,子Rule之间也有关系,后面会看到它们是一个同构结构。
如何把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”关系也应该是不带短路运算的,但带了也没有影响。
这里的“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.
两个Rule用 “OR”或者“AND”组合以后还是一个Rule,就可以和其它的Rule再次组合,也就是说“OR”和“AND”组合子是封闭的。整个Rule世界是封闭的。
组合子满足封闭性非常重要,因为只有满足封闭性,组合子才能和其它原子或者组合子再组合……。可以想象,如果一个语言提供的组合手段是能够封闭的,那么它就能够高效地帮助你构建出非常复杂的东西。
在面向对象编程语言中,通常用“接口”表示一个对象是一个“什么”,接口方法的输入、输出决定了“什么”的特征。而在函数式编程语言中,通常用“函数”表示“什么”,函数的输入、输出决定了“什么”的特征。
这样,我们就把整个问题领域映射到了布尔代数的语义领域。
什么是同构(Isomorphic)? 在数学中研究同构的主要目的是为了把数学理论应用于不同的领域。如果两个结构是同构的,那么其上的对象会有相似的属性和操作,对某个结构成立的命题在另一个结构上也就成立。因此,如果在某个数学领域发现了一个对象结构同构于某个结构,且对于该结构已经证明了很多定理,那么这些定理马上就可以应用到该领域。如果某些数学方法可以用于该结构,那么这些方法也可以用于新领域的结构。这就使得理解和处理该对象结构变得容易,并往往可以让数学家对该领域有更深刻的理解。
如果两个领域是同构的,那么其上的对象会有相似的属性和操作,对某个领域成立的命题在另一个领域上也就成立。这就使得理解和处理新领域的对象变得容易,并可以对新领域有更深刻的理解。
有了Atom Rule(Predication、Action、Result)和Combination Rule(Or、And),计算模型就有了,接下来就可以基于这个计算模型来考虑如何表述(represent)这些规则和规则之间的关系。
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同理。
r1 or r1_3 r1_5 r1_7
Rule1的名字为r1,它是一个or组合子Rule,由r1_3、r1_5、r1_7这3个子Rule“or”组合而成。
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”组合而成。
r3 atom contains 3 to_fizz
Rule3名字为r3,它是一个原子Rule(atom)。构造原子Rule需要Predication和Action。contains是Predication,参数为3,to_fizz是Action,不带参数。
rd atom always_true to_str
Default Rule的名字为rd,它是一个原子Rule(atom)。构造原子Rule需要Predication和Action。always_true是Predication,不带参数,to_str是Action,不带参数。
最后,Rule1、Rule2、Rule3和Default Rule之间的关系表达如下:
spec or r3 r2 r1 rd
把它们之间的关系命名为spec,它是一个or组合子Rule,由r3、r2、r1、rd这4个子Rule“or”组合而成。
把上面的表述完整地写出来,就是一份规格说明(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都是精确无二义、清晰、和需求对应的。
以上内容就是设计的结果。通过前面的分析和定义,把问题转换成上面的表述,把问题需求变成规格说明,变成一个设计。
做一个设计小结。
我们在做领域驱动设计时,首先考虑是否能将问题领域映射到一个熟悉的同构领域。
如果能,就可以借用那个领域的机制来表达问题领域的概念,而不是重新发明。
如果能找到这样一种同构领域,应该是一个最好的结果,如果实在找不到,再自己发明。但一般来说,最终一定能在数学层面上找到一个同构领域,有可能只是没找到而已。
找到问题域中的核心概念,把问题域映射到语义域(Semantic Domain),并站在语义域考虑如何执行,而不去考虑底层如何实现。
在思考问题时,不从通用语言层面思考(例如Java类、接口、继承等),正如我们在用Java/Python语言写程序时不会像用汇编语言写程序那样思考,而是从问题本身找到它的语义域(Semantic Domain),这个域最好是一个数学上的。
语义的计算模型是核心,DSL只是皮毛。
这是一种设计方法,也是本系列课程的核心所在。
这种设计方法的适用性范围很广,从一个小的模块,到一个大的系统,到一个平台,到整个业务架构都是适用的。一旦掌握了这种方法,再去看问题,就会和以前的看法完全不一样,能看到一些更为本质的东西。而且,用这种方法做出来的设计不仅能够很好地满足目前的需求,对于日后的系统演化和发展都是适用的。
“基于类型设计”是一种比较好的设计方法。
用这种设计方法可以思考类型、而不考虑如何实现。
类型是一种在概念层面上的契约。
实现:选择实现技术把逻辑框架的软件模型实现出来。
在本课程中,我们用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):
-
Atom Rules 实现r1_3、r1_5、r1_7这三个Atom Rule。构造Atom Rule需要Predication和Action,所以要先实现Times这个Predication以及ToFizz、ToBuzz和ToWhizz这三个Action。
-
OR Rule,Rule 1
实现OR Rule,构造出Rule1。 -
AND Rule,Rule 2
实现AND Rule,构造出Rule2。 -
Rule 3
实现Contains Predication,构造出Rule3。 -
Default Rule
实现AlwaysTrue Predication和ToStr Action,构造出Default Rule。 -
Specification
用API编写spec。 -
FizzBuzzWhizz
最终实现FizzBuzzWhizz。
代码路径:original
实现r1_3、r1_5、r1_7这三个Atom Rule。构造Atom Rule需要Predication和Action,所以要先实现Times这个Predication以及ToFizz、ToBuzz和ToWhizz这三个Action。
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
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'
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, ''
继续补充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))
实现OR Rule,构造出Rule1。
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)
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))
实现AND Rule,构造出Rule2。
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]])
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))
实现Contains Predication,构造出Rule3。
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)
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))
实现AlwaysTrue Predication和ToStr Action,构造出Default Rule。
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
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)
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))
用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)
最终实现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()
设计就是把问题变成可计算的。
设计出的计算模型所提供的语义和问题领域的根本需求是否匹配,匹配就是好的设计,不匹配就是不好的设计。
将FizzBuzzWhizz这个问题领域映射到了布尔代数的领域,借用了布尔代数的“OR”和“AND”来表达Rule之间的Relation。
- 对问题领域进行深入分析,发现问题领域的核心需求;
- 通过核心需求驱动出计算模型和语义;
- 再围绕这个计算模型提供一套语言,给外面的人使用这个计算模型提供一个接口,这个接口可以是API、可以是数据表达、也可以是语言;
- 最终要实现这个计算模型,实现的方法有解释器和编译器两种。
这就是DDD!这才是DDD!
这就是DSL!这才是DSL!
“编程”不过是在某个计算模型上用某种语言去表达计算。 | 用DSL编程不过是在问题领域的计算模型上用DSL来表达计算。 |
计算模型是相应领域中的“通用机器”。 | 问题领域的计算模型是问题领域的通用机器。 |
编程语言不过是描述计算机器的一种方法。 | DSL描述的是DSL语言的计算机器。 |
程序是对特定机器的描述,这个特定机器可以被通用机器仿真。 | 用DSL程序实现了问题领域中的某个功能,这个DSL程序就是对这个功能(特定机器)的描述。 |
- 从问题领域导出核心需求,得到领域的计算模型(通用计算机器),在上面可以包装一个DSL语言或者数据或者API,基于这些开发程序和应用。
- 领域的计算模型和通用语言的计算模型之间存在鸿沟,可以用解释器或者编译器来填补。
- 解释器和编译器听起来很复杂,其实思想很简单,而且我们没有必要实现一个工业级别的、非常全面的解释器和编译器,只要借鉴这个思想实现我们的计算模型就够了。
Common Languages(Java/C) UML对应Java/C/Python通用语言。
Common Languages(Java/C) UM对应Java/C/Python通用语言提供的计算模型(通用计算机器)。
提升设计能力的根本在于提升计算模型构造和语义定义能力。
Thinking, thinking, thinking …
Practice, practice and practice …
No shortcuts.
- 【必选】新增特殊数:8,除了以下规则有调整以外,题目中的其它内容不变:
- 规则1增加:如果所报数字是第四个特殊数(8)的倍数,那么要说Hazz;
- 规则2增加:如果同时是三个特殊数的倍数,那么要说FizzBuzzWhizz,以此类推。如果同时是四个特殊数的倍数,那么要说FizzBuzzWhizzHazz。
- 【可选】用函数式编程方式实现FizzBuzzWhizz。
第1题做了两个版本,路径如下:
直白的:eight.straightforward
带组合计算的:eight.combination
第2题参见FizzBuzzWhizz Reloaded,路径:functional。
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'
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))
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))
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))
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)
在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而成。
先实现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))
为了不区分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))