201710 DDD DSL Design War FizzBuzzWhizz - 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,不过就和实现语言相关了。以Java语言为例,atom、and、or、times、contains、alwaysTrue、toFizz、toBuzz、toWhizz、toStr都是API,用这些API编程写出的Specification如下:
public static Rule spec()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule r1 = or3(r1_3, r1_5, r1_7);
Rule r2 = or4(and3(r1_3, r1_5, r1_7),
and(r1_3, r1_5),
and(r1_3, r1_7),
and(r1_5, r1_7));
Rule r3 = atom(contains(3), toFizz());
Rule 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方式实现。
public static Rule spec()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule r1 = or3(r1_3, r1_5, r1_7);
Rule r2 = or4(and3(r1_3, r1_5, r1_7),
and(r1_3, r1_5),
and(r1_3, r1_7),
and(r1_5, r1_7));
Rule r3 = atom(contains(3), toFizz());
Rule 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。
代码包路径:fayelab.ddd.fbw.original
实现r1_3、r1_5、r1_7这三个Atom Rule。构造Atom Rule需要Predication和Action,所以要先实现times这个Predication以及toFizz、toBuzz和toWhizz这三个Action。
TimesTest.java
package fayelab.ddd.fbw.original.predication;
public class TimesTest extends TestCase
{
public void test_times_3()
{
Predication times3 = new Times(3);
assertTrue(times3.predicate(6));
assertFalse(times3.predicate(5));
}
public void test_times_5()
{
Predication times5 = new Times(5);
assertTrue(times5.predicate(10));
assertFalse(times5.predicate(11));
}
public void test_times_7()
{
Predication times7 = new Times(7);
assertTrue(times7.predicate(14));
assertFalse(times7.predicate(15));
}
}
Predication.java
package fayelab.ddd.fbw.original.predication;
public interface Predication
{
boolean predicate(int n);
}
Times.java
package fayelab.ddd.fbw.original.predication;
public class Times implements Predication
{
private int base;
public Times(int base)
{
this.base = base;
}
@Override
public boolean predicate(int n)
{
return n % base == 0;
}
}
ToFizzTest.java
package fayelab.ddd.fbw.original.action;
public class ToFizzTest extends TestCase
{
public void test_toFizz()
{
Action toFizz = new ToFizz();
assertEquals("Fizz", toFizz.act(3));
}
}
Action.java
package fayelab.ddd.fbw.original.action;
public interface Action
{
String act(int n);
}
ToFizz.java
package fayelab.ddd.fbw.original.action;
public class ToFizz implements Action
{
@Override
public String act(int n)
{
return "Fizz";
}
}
ToBuzzTest.java
package fayelab.ddd.fbw.original.action;
public class ToBuzzTest extends TestCase
{
public void test_toBuzz()
{
Action toBuzz = new ToBuzz();
assertEquals("Buzz", toBuzz.act(5));
}
}
ToBuzz.java
package fayelab.ddd.fbw.original.action;
public class ToBuzz implements Action
{
@Override
public String act(int n)
{
return "Buzz";
}
}
ToWhizzTest.java
package fayelab.ddd.fbw.original.action;
public class ToWhizzTest extends TestCase
{
public void test_toWhizz()
{
Action toWhizz = new ToWhizz();
assertEquals("Whizz", toWhizz.act(7));
}
}
ToWhizz.java
package fayelab.ddd.fbw.original.action;
public class ToWhizz implements Action
{
@Override
public String act(int n)
{
return "Whizz";
}
}
这里之所以没有写专门的AtomTest、OrTest和AndTest等测试类,是因为后面还会有Rule 1、Rule 2和Specification等业务规则的测试,放在AtomTest、OrTest和AndTest都不合适,所以就都放在RuleTest测试类中了。这样做还有一个好处,重构后的checkResult不用重复出现在AtomTest、OrTest和AndTest中。
RuleTest.java
package fayelab.ddd.fbw.original.rule;
public class RuleTest extends TestCase
{
public void test_atom_rule_1_3()
{
Rule r1_3 = new Atom(new Times(3), new ToFizz());
Result actual1 = r1_3.apply(3);
assertTrue(actual1.isSucceeded());
assertEquals("Fizz", actual1.getStr());
Result actual2 = r1_3.apply(4);
assertFalse(actual2.isSucceeded());
assertEquals("", actual2.getStr());
}
}
Rule.java
public interface Rule
{
Result apply(int n);
}
Result.java
package fayelab.ddd.fbw.original.rule;
public class Result
{
private boolean isSucceeded;
private String str;
public Result(boolean isSucceeded, String str)
{
this.isSucceeded = isSucceeded;
this.str = str;
}
public boolean isSucceeded()
{
return isSucceeded;
}
public String getStr()
{
return str;
}
}
Atom.java
package fayelab.ddd.fbw.original.rule;
public class Atom implements Rule
{
private Predication predication;
private Action action;
public Atom(Predication predication, Action action)
{
this.predication = predication;
this.action = action;
}
@Override
public Result apply(int n)
{
if(predication.predicate(n))
{
return new Result(true, action.act(n));
}
return new Result(false, "");
}
}
重构要点:
- RuleTest
- 抽取checkResult()方法;
- 新增SpecTool类,并在其中增加atom()、times()、toFizz()等静态工具方法。
- Result类中提供succeededResult()和failedResult()等静态工具方法,且构造函数的访问属性改为private。
- Atom类改为调用Result类提供的静态方法。
RuleTest.java
public class RuleTest extends TestCase
{
public void test_atom_rule_1_3()
{
Rule r1_3 = new Atom(new Times(3), new ToFizz());
checkResult(true, "Fizz", r1_3.apply(3));
checkResult(false, "", r1_3.apply(4));
Rule r1_3_t = atom(times(3), toFizz());
checkResult(true, "Fizz", r1_3_t.apply(3));
checkResult(false, "", r1_3_t.apply(4));
}
private void checkResult(boolean expectedSucceeded, String expectedStr, Result actual)
{
assertEquals(expectedSucceeded, actual.isSucceeded());
assertEquals(expectedStr, actual.getStr());
}
}
SpecTool.java
package fayelab.ddd.fbw.original;
public class SpecTool
{
public static Predication times(int n)
{
return new Times(n);
}
public static Action toFizz()
{
return new ToFizz();
}
public static Rule atom(Predication predication, Action action)
{
return new Atom(predication, action);
}
}
Result.java
private Result(boolean isSucceeded, String str)
{
this.isSucceeded = isSucceeded;
this.str = str;
}
public static Result succeededResult(String str)
{
return new Result(true, str);
}
public static Result failedResult()
{
return new Result(false, "");
}
Atom.java
@Override
public Result apply(int n)
{
if(predication.predicate(n))
{
return Result.succeededResult(action.act(n));
}
return Result.failedResult();
}
在SpecTool中补充toBuzz()、toWhizz()工具方法。调用SpecTool中的工具方法,继续补充r1_5和r1_7的测试用例。
RuleTest.java
public void test_atom_rule_1_5()
{
Rule r1_5 = atom(times(5), toBuzz());
checkResult(true, "Buzz", r1_5.apply(10));
checkResult(false, "", r1_5.apply(11));
}
public void test_atom_rule_1_7()
{
Rule r1_7 = atom(times(7), toWhizz());
checkResult(true, "Whizz", r1_7.apply(14));
checkResult(false, "", r1_7.apply(13));
}
SpecTool.java
public static Action toBuzz()
{
return new ToBuzz();
}
public static Action toWhizz()
{
return new ToWhizz();
}
实现Or Rule,构造出Rule1。
RuleTest.java
public void test_or_rule()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule or_35 = or(r1_3, r1_5);
checkResult(true, "Fizz", or_35.apply(6));
checkResult(true, "Buzz", or_35.apply(10));
checkResult(true, "Fizz", or_35.apply(15));
checkResult(false, "", or_35.apply(7));
}
SpecTool.java
public static Rule or(Rule rule1, Rule rule2)
{
return new Or(rule1, rule2);
}
Or.java
package fayelab.ddd.fbw.original.rule;
public class Or implements Rule
{
private Rule rule1;
private Rule rule2;
public Or(Rule rule1, Rule rule2)
{
this.rule1 = rule1;
this.rule2 = rule2;
}
@Override
public Result apply(int n)
{
Result result1 = rule1.apply(n);
if(result1.isSucceeded())
{
return result1;
}
return rule2.apply(n);
}
}
RuleTest.java
public void test_rule_1()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule r1 = or3(r1_3, r1_5, r1_7);
checkResult(true, "Fizz", r1.apply(6));
checkResult(true, "Buzz", r1.apply(10));
checkResult(true, "Whizz", r1.apply(14));
checkResult(false, "", r1.apply(13));
}
SpecTool.java
public static Rule or3(Rule rule1, Rule rule2, Rule rule3)
{
return or(rule1, or(rule2, rule3));
}
实现And Rule,构造出Rule2。
RuleTest.java
public void test_and_rule()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule and_35 = and(r1_3, r1_5);
checkResult(false, "", and_35.apply(3));
checkResult(false, "", and_35.apply(5));
checkResult(true, "FizzBuzz", and_35.apply(15));
checkResult(false, "", and_35.apply(16));
}
SpecTool.java
public static Rule and(Rule rule1, Rule rule2)
{
return new And(rule1, rule2);
}
And.java
package fayelab.ddd.fbw.original.rule;
public class And implements Rule
{
private Rule rule1;
private Rule rule2;
public And(Rule rule1, Rule rule2)
{
this.rule1 = rule1;
this.rule2 = rule2;
}
@Override
public Result apply(int n)
{
Result result1 = rule1.apply(n);
if(!result1.isSucceeded())
{
return Result.failedResult();
}
Result result2 = rule2.apply(n);
if(!result2.isSucceeded())
{
return Result.failedResult();
}
return Result.succeededResult(result1.getStr() + result2.getStr());
}
}
RuleTest.java
public void test_rule_2()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule r2 = or4(and3(r1_3, r1_5, r1_7),
and(r1_3, r1_5),
and(r1_3, r1_7),
and(r1_5, r1_7));
checkResult(false, "", r2.apply(3));
checkResult(false, "", r2.apply(5));
checkResult(false, "", r2.apply(7));
checkResult(true, "FizzBuzzWhizz", r2.apply(3*5*7));
checkResult(false, "", r2.apply(104));
checkResult(true, "FizzBuzz", r2.apply(15));
checkResult(false, "", r2.apply(14));
checkResult(true, "FizzWhizz", r2.apply(21));
checkResult(false, "", r2.apply(22));
checkResult(true, "BuzzWhizz", r2.apply(35));
checkResult(false, "", r2.apply(34));
}
SpecTool.java
public static Rule or4(Rule rule1, Rule rule2, Rule rule3, Rule rule4)
{
return or(rule1, or3(rule2, rule3, rule4));
}
public static Rule and3(Rule rule1, Rule rule2, Rule rule3)
{
return and(rule1, and(rule2, rule3));
}
实现contains Predication,构造出Rule3。
ContainsTest.java
package fayelab.ddd.fbw.original.predication;
public class ContainsTest extends TestCase
{
public void test_contains()
{
Predication contains3 = new Contains(3);
assertTrue(contains3.predicate(13));
assertTrue(contains3.predicate(35));
assertTrue(contains3.predicate(300));
assertFalse(contains3.predicate(24));
Predication contains3_t = contains(3);
assertTrue(contains3_t.predicate(13));
}
}
SpecTool.java
public static Predication contains(int digit)
{
return new Contains(digit);
}
Contains.java
package fayelab.ddd.fbw.original.predication;
public class Contains implements Predication
{
private int digit;
public Contains(int digit)
{
this.digit = digit;
}
@Override
public boolean predicate(int n)
{
int p1 = n % 10;
int p2 = (n / 10) % 10;
int p3 = (n / 100) % 10;
return p1 == digit || p2 == digit || p3 == digit;
}
}
RuleTest.java
public void test_rule_3()
{
Rule r3 = atom(contains(3), toFizz());
checkResult(true, "Fizz", r3.apply(3));
checkResult(true, "Fizz", r3.apply(13));
checkResult(true, "Fizz", r3.apply(31));
checkResult(false, "", r3.apply(24));
}
实现alwaysTrue Predication和toStr Action,构造出Default Rule。
AlwaysTrueTest.java
package fayelab.ddd.fbw.original.predication;
public class AlwaysTrueTest extends TestCase
{
public void test_alwaysTrue()
{
Predication alwaysTrue = new AlwaysTrue();
assertTrue(alwaysTrue.predicate(1));
assertTrue(alwaysTrue.predicate(3));
assertTrue(alwaysTrue.predicate(5));
Predication alwaysTrue_t = alwaysTrue();
assertTrue(alwaysTrue_t.predicate(1));
}
}
SpecTool.java
public static Predication alwaysTrue()
{
return new AlwaysTrue();
}
AlwaysTrue.java
package fayelab.ddd.fbw.original.predication;
public class AlwaysTrue implements Predication
{
@Override
public boolean predicate(int n)
{
return true;
}
}
ToStrTest.java
package fayelab.ddd.fbw.original.action;
public class ToStrTest extends TestCase
{
public void test_toStr()
{
Action toStr = new ToStr();
assertEquals("1", toStr.act(1));
assertEquals("10", toStr.act(10));
Action toStr_t = toStr();
assertEquals("1", toStr_t.act(1));
}
}
SpecTool.java
public static Action toStr()
{
return new ToStr();
}
ToStr.java
package fayelab.ddd.fbw.original.action;
public class ToStr implements Action
{
@Override
public String act(int n)
{
return String.valueOf(n);
}
}
RuleTest.java
public void test_default_rule()
{
Rule rd = atom(alwaysTrue(), toStr());
checkResult(true, "1", rd.apply(1));
checkResult(true, "3", rd.apply(3));
}
用API编写spec。
RuleTest.java
public void test_spec()
{
Rule spec = spec();
checkResult(true, "Fizz", spec.apply(35));
checkResult(true, "FizzBuzz", spec.apply(15));
checkResult(true, "FizzWhizz", spec.apply(21));
checkResult(true, "BuzzWhizz", spec.apply(70));
checkResult(true, "Fizz", spec.apply(9));
checkResult(true, "1", spec.apply(1));
}
SpecTool.java
public static Rule spec()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule r1 = or3(r1_3, r1_5, r1_7);
Rule r2 = or4(and3(r1_3, r1_5, r1_7),
and(r1_3, r1_5),
and(r1_3, r1_7),
and(r1_5, r1_7));
Rule r3 = atom(contains(3), toFizz());
Rule rd = atom(alwaysTrue(), toStr());
return or4(r3, r2, r1, rd);
}
action、predication、rule包各自增加自己的Test Suite,最后再加一个总的Test Suite。
action.AllTests.java
package fayelab.ddd.fbw.original.action;
public class AllTests
{
public static Test suite()
{
TestSuite suite = new TestSuite(AllTests.class.getName());
//$JUnit-BEGIN$
suite.addTestSuite(ToFizzTest.class);
suite.addTestSuite(ToBuzzTest.class);
suite.addTestSuite(ToWhizzTest.class);
suite.addTestSuite(ToStrTest.class);
//$JUnit-END$
return suite;
}
}
predication.AllTests.java
package fayelab.ddd.fbw.original.predication;
public class AllTests
{
public static Test suite()
{
TestSuite suite = new TestSuite(AllTests.class.getName());
//$JUnit-BEGIN$
suite.addTestSuite(TimesTest.class);
suite.addTestSuite(ContainsTest.class);
suite.addTestSuite(AlwaysTrueTest.class);
//$JUnit-END$
return suite;
}
}
rule.AllTests.java
public class AllTests
{
public static Test suite()
{
TestSuite suite = new TestSuite(AllTests.class.getName());
//$JUnit-BEGIN$
suite.addTestSuite(RuleTest.class);
//$JUnit-END$
return suite;
}
}
fbw.AllTests.java
package fayelab.ddd.fbw.original;
public class AllTests
{
public static Test suite()
{
TestSuite suite = new TestSuite(AllTests.class.getName());
//$JUnit-BEGIN$
suite.addTest(fayelab.ddd.fbw.original.action.AllTests.suite());
suite.addTest(fayelab.ddd.fbw.original.predication.AllTests.suite());
suite.addTest(fayelab.ddd.fbw.original.rule.AllTests.suite());
//$JUnit-END$
return suite;
}
}
最终实现FizzBuzzWhizz。
package fayelab.ddd.fbw.original;
public class FizzBuzzWhizz
{
public void run()
{
Rule spec = spec();
List<Result> results = IntStream.rangeClosed(1, 100)
.mapToObj(spec::apply)
.collect(Collectors.toList());
output(results);
}
private void output(List<Result> results)
{
results.stream().map(Result::getStr).forEach(System.out::println);
}
public static void main(String[] args)
{
new FizzBuzzWhizz().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题做了两个版本,包路径如下:
直白的:fayelab.ddd.fbw.eight.straightforward
带组合计算的:fayelab.ddd.fbw.eight.combination
第2题参见FizzBuzzWhizz Reloaded,包路径:faylab.ddd.fbwreloaded.functional。
ToHazzTest.java
package fayelab.ddd.fbw.eight.action;
public class ToHazzTest extends TestCase
{
public void test_toHazz()
{
Action toHazz = new ToHazz();
assertEquals("Hazz", toHazz.act(8));
Action toHazz_t = toHazz();
assertEquals("Hazz", toHazz_t.act(8));
}
}
action.AllTests.java
package fayelab.ddd.fbw.eight.action;
public class AllActionTests
{
public static Test suite()
{
TestSuite suite = new TestSuite(AllTests.class.getName());
//$JUnit-BEGIN$
suite.addTestSuite(ToFizzTest.class);
suite.addTestSuite(ToBuzzTest.class);
suite.addTestSuite(ToWhizzTest.class);
suite.addTestSuite(ToStrTest.class);
suite.addTestSuite(ToHazzTest.class);
//$JUnit-END$
return suite;
}
}
SpecTool.java
public static Action toHazz()
{
return new ToHazz();
}
ToHazz.java
package fayelab.ddd.fbw.eight.action;
public class ToHazz implements Action
{
@Override
public String act(int n)
{
return "Hazz";
}
}
RuleTest.java
public void test_atom_rule_1_8()
{
Rule r1_8 = atom(times(8), toHazz());
checkResult(true, "Hazz", r1_8.apply(16));
checkResult(false, "", r1_8.apply(13));
}
RuleTest.java
public void test_rule_1()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule r1_8 = atom(times(8), toHazz());
Rule r1 = or4(r1_3, r1_5, r1_7, r1_8);
checkResult(true, "Fizz", r1.apply(6));
checkResult(true, "Buzz", r1.apply(10));
checkResult(true, "Whizz", r1.apply(14));
checkResult(true, "Hazz", r1.apply(16));
checkResult(false, "", r1.apply(13));
}
RuleTest.java
public void test_rule_2()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule r1_8 = atom(times(8), toHazz());
Rule r2 = or(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));
checkResult(false, "", r2.apply(3));
checkResult(false, "", r2.apply(5));
checkResult(false, "", r2.apply(7));
checkResult(false, "", r2.apply(8));
checkResult(true, "FizzBuzzWhizzHazz", r2.apply(3*5*7*8));
checkResult(false, "", r2.apply(841));
checkResult(true, "FizzBuzzWhizz", r2.apply(3*5*7));
checkResult(false, "", r2.apply(104));
checkResult(true, "FizzBuzzHazz", r2.apply(3*5*8));
checkResult(false, "", r2.apply(121));
checkResult(true, "FizzWhizzHazz", r2.apply(3*7*8));
checkResult(false, "", r2.apply(167));
checkResult(true, "BuzzWhizzHazz", r2.apply(5*7*8));
checkResult(false, "", r2.apply(281));
checkResult(true, "FizzBuzz", r2.apply(15));
checkResult(false, "", r2.apply(14));
checkResult(true, "FizzWhizz", r2.apply(21));
checkResult(false, "", r2.apply(22));
checkResult(true, "FizzHazz", r2.apply(24));
checkResult(false, "", r2.apply(23));
checkResult(true, "BuzzWhizz", r2.apply(35));
checkResult(false, "", r2.apply(34));
checkResult(true, "BuzzHazz", r2.apply(40));
checkResult(false, "", r2.apply(41));
checkResult(true, "WhizzHazz", r2.apply(56));
checkResult(false, "", r2.apply(55));
}
SpecTool类增加and4()、or(...)、or(List)方法。
public static Rule or(Rule...rules)
{
return or(asList(rules));
}
private static Rule or(List<Rule> rules)
{
return rules.stream().reduce(Or::new).get();
}
public static Rule and4(Rule rule1, Rule rule2, Rule rule3, Rule rule4)
{
return and(rule1, and3(rule2, rule3, rule4));
}
SpecTool类增加or(...)、or(List)方法后,or3()和or4()方法就可以删掉了。
RuleTest.java中引用or4()的地方全部改为引用or(...)。
public void test_rule_1()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule r1_8 = atom(times(8), toHazz());
Rule r1 = or(r1_3, r1_5, r1_7, r1_8);
checkResult(true, "Fizz", r1.apply(6));
checkResult(true, "Buzz", r1.apply(10));
checkResult(true, "Whizz", r1.apply(14));
checkResult(true, "Hazz", r1.apply(16));
checkResult(false, "", r1.apply(13));
}
RuleTest.java
public void test_spec()
{
Rule spec = spec();
checkResult(true, "Fizz", spec.apply(35));
checkResult(true, "FizzBuzzWhizzHazz", spec.apply(3*5*7*8));
checkResult(true, "FizzBuzzWhizz", spec.apply(3*5*7));
checkResult(true, "FizzBuzzHazz", spec.apply(3*5*8));
checkResult(true, "FizzWhizzHazz", spec.apply(3*7*8));
checkResult(true, "BuzzWhizzHazz", spec.apply(5*7*8));
checkResult(true, "FizzBuzz", spec.apply(15));
checkResult(true, "FizzWhizz", spec.apply(21));
checkResult(true, "FizzHazz", spec.apply(24));
checkResult(true, "BuzzWhizz", spec.apply(70));
checkResult(true, "BuzzHazz", spec.apply(40));
checkResult(true, "WhizzHazz", spec.apply(56));
checkResult(true, "Fizz", spec.apply(9));
checkResult(true, "Buzz", spec.apply(5));
checkResult(true, "Whizz", spec.apply(7));
checkResult(true, "Hazz", spec.apply(8));
checkResult(true, "1", spec.apply(1));
}
SpecTool.java
public static Rule spec()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule r1_8 = atom(times(8), toHazz());
Rule r1 = or(r1_3, r1_5, r1_7, r1_8);
Rule r2 = or(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));
Rule r3 = atom(contains(3), toFizz());
Rule rd = atom(alwaysTrue(), toStr());
return or(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而成。
在ListTool列表工具类中实现两个方法:
- combinate: (List(m), n) -> ListOfList
求列表的C(m, n)组合,列表长度为m,记为List(m),1≤n≤m。输出元素为列表的列表。 - flatten: ListOfList -> List
把列表的元素(列表)拉平。
ListToolTest.java
package fayelab.ddd.fbw.eight.combination;
public class ListToolTest extends TestCase
{
public void test_combinate()
{
List<Integer> lst = asList(1, 2, 3, 4);
assertEquals(asList(), combinate(lst, 0));
assertEquals(asList(), combinate(lst, 5));
assertEquals(asList(asList(1), asList(2), asList(3), asList(4)),
combinate(lst, 1));
assertEquals(asList(asList(1, 2), asList(1, 3), asList(1, 4),
asList(2, 3), asList(2, 4),
asList(3, 4)),
combinate(lst, 2));
assertEquals(asList(asList(1, 2, 3), asList(1, 2, 4), asList(1, 3, 4),
asList(2, 3, 4)),
combinate(lst, 3));
assertEquals(asList(asList(1, 2, 3, 4)),
combinate(lst, 4));
}
public void test_flatten()
{
List<List<Integer>> lstOfLst = asList(asList(1, 2), asList(3, 4), asList(5));
assertEquals(asList(1, 2, 3, 4, 5), flatten(lstOfLst));
List<List<List<Integer>>> lstOfLst2 = asList(asList(asList(1, 2), asList(3, 4)),
asList(asList(5), asList(6)));
assertEquals(asList(asList(1, 2), asList(3, 4), asList(5), asList(6)),
flatten(lstOfLst2));
}
}
fbw.AllTests.java
public static Test suite()
{
TestSuite suite = new TestSuite(AllTests.class.getName());
//$JUnit-BEGIN$
suite.addTest(fayelab.ddd.fbw.eight.combination.action.AllTests.suite());
suite.addTest(fayelab.ddd.fbw.eight.combination.predication.AllTests.suite());
suite.addTest(fayelab.ddd.fbw.eight.combination.rule.AllTests.suite());
suite.addTestSuite(ListToolTest.class);
//$JUnit-END$
return suite;
}
ListTool.java
package fayelab.ddd.fbw.eight.combination;
public class ListTool
{
public static <T> List<List<T>> combinate(List<T> lst, int n)
{
if(n < 1 || n > lst.size())
{
return asList();
}
if(n == 1)
{
return lst.stream().map(ele -> asList(ele)).collect(toList());
}
if(n == lst.size())
{
return asList(lst);
}
T head = lst.get(0);
List<T> rest = lst.subList(1, lst.size());
List<List<T>> comb1 = combinate(rest, n - 1).stream()
.map(ele -> addHead(head, ele))
.collect(toList());
List<List<T>> comb2 = combinate(rest, n);
return concat(comb1, comb2);
}
public static <T> List<T> flatten(List<List<T>> lstOfLst)
{
return lstOfLst.stream().collect(ArrayList::new,
(result, lst) -> result.addAll(lst),
ArrayList::addAll);
}
private static <T> List<T> addHead(T head, List<T> lst)
{
List<T> result = new ArrayList<>();
result.add(head);
result.addAll(lst);
return result;
}
private static <T> List<List<T>> concat(List<List<T>> lstOfLst1, List<List<T>> lstOfLst2)
{
return Stream.concat(lstOfLst1.stream(), lstOfLst2.stream()).collect(toList());
}
}
为了不区分and3、and4和and,SpecTool还要提供and(list)方法,为了跟or保持一致,也提供and(...)方法,并删除 and3()和and4()方法。
SpecTool.java
public static Rule and(Rule...rules)
{
return and(asList(rules));
}
public static Rule and(List<Rule> rules)
{
return rules.stream().reduce(And::new).get();
}
RuleTest.java
public void test_rule_2()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule r1_8 = atom(times(8), toHazz());
List<Rule> atomRules = asList(r1_3, r1_5, r1_7, r1_8);
List<List<Rule>> lstOfRules = flatten(asList(combinate(atomRules, 4),
combinate(atomRules, 3),
combinate(atomRules, 2)));
Rule r2 = or(lstOfRules.stream().map(rules -> and(rules)).collect(toList()));
checkResult(false, "", r2.apply(3));
checkResult(false, "", r2.apply(5));
checkResult(false, "", r2.apply(7));
checkResult(false, "", r2.apply(8));
checkResult(true, "FizzBuzzWhizzHazz", r2.apply(3*5*7*8));
checkResult(false, "", r2.apply(841));
checkResult(true, "FizzBuzzWhizz", r2.apply(3*5*7));
checkResult(false, "", r2.apply(104));
checkResult(true, "FizzBuzzHazz", r2.apply(3*5*8));
checkResult(false, "", r2.apply(121));
checkResult(true, "FizzWhizzHazz", r2.apply(3*7*8));
checkResult(false, "", r2.apply(167));
checkResult(true, "BuzzWhizzHazz", r2.apply(5*7*8));
checkResult(false, "", r2.apply(281));
checkResult(true, "FizzBuzz", r2.apply(15));
checkResult(false, "", r2.apply(14));
checkResult(true, "FizzWhizz", r2.apply(21));
checkResult(false, "", r2.apply(22));
checkResult(true, "FizzHazz", r2.apply(24));
checkResult(false, "", r2.apply(23));
checkResult(true, "BuzzWhizz", r2.apply(35));
checkResult(false, "", r2.apply(34));
checkResult(true, "BuzzHazz", r2.apply(40));
checkResult(false, "", r2.apply(41));
checkResult(true, "WhizzHazz", r2.apply(56));
checkResult(false, "", r2.apply(55));
}
SpecTool.java
public static Rule spec()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule r1_8 = atom(times(8), toHazz());
List<Rule> atomRules = asList(r1_3, r1_5, r1_7, r1_8);
Rule r1 = or(atomRules);
List<List<Rule>> lstOfRules = flatten(asList(combinate(atomRules, 4),
combinate(atomRules, 3),
combinate(atomRules, 2)));
Rule r2 = or(lstOfRules.stream().map(rules -> and(rules)).collect(toList()));
Rule r3 = atom(contains(3), toFizz());
Rule rd = atom(alwaysTrue(), toStr());
return or(r3, r2, r1, rd);
}
SpecTool中or(List)和and(List)有重复,通过函数式编程范式消除重复。
public static Rule or(List<Rule> rules)
{
return combine(rules, Or::new);
}
public static Rule and(List<Rule> rules)
{
return combine(rules, And::new);
}
private static Rule combine(List<Rule> rules, BinaryOperator<Rule> biCombine)
{
return rules.stream().reduce(biCombine).get();
}