201710 DDD DSL Design War FizzBuzzWhizz Reloaded - xiaoxianfaye/Courses GitHub Wiki
- 1 Review
- 2 Stop to Think
- 3 Interpreter
-
4 Interpreter & Compiler
- 4.1 Overview
- 4.2 Program + Parser + Interpreter
- 4.3 Program + Parser + Compiler
- 5 Summary
- 6 Homework
你是一名体育老师,在某次课距离下课还有五分钟时,你决定搞一个游戏。此时有100名学生在上课。游戏的规则是:
-
设定三个不同的特殊数:3、5、7。
-
让所有学生排成一队,然后按顺序报数。
-
学生报数时,如果所报数字是第一个特殊数(3)的倍数,那么不能说该数字,而要说Fizz;如果所报数字是第二个特殊数(5)的倍数,那么要说Buzz;如果所报数字是第三个特殊数(7)的倍数,那么要说Whizz。
-
学生报数时,如果所报数字同时是两个特殊数的倍数情况下,也要特殊处理,比如第一个特殊数和第二个特殊数的倍数,那么不能说该数字,而是要说FizzBuzz, 以此类推。如果同时是三个特殊数的倍数,那么要说FizzBuzzWhizz。
-
学生报数时,如果所报数字包含了第一个特殊数,那么也不能说该数字,而是要说相应的单词,比如本例中第一个特殊数是3,那么要报13的同学应该说Fizz。 如果数字中包含了第一个特殊数,那么忽略规则3和规则4,比如要报35的同学只报Fizz,不报BuzzWhizz。
请编写一个程序来模拟这个游戏。
设计的第一步:找到合适的语义。
What is Semantics?
- 在解决一个问题的时候,经常问“What does this mean?”;
- 既能够被精确清晰地表达出来,又能够教给计算机去做。
How to find an appropriate Semantics?
- 首先考虑是否能将问题领域映射到一个熟悉的同构领域。如果能,就可以借用那个领域的机制来表达问题领域的概念,而不是重新发明。
- 最好是映射到数学领域。一般来说,最终一定能在数学层面上找到一个同构领域,有可能只是没找到而已。
把问题领域映射到了数学上的布尔代数的语义领域。
设计的第二步:形式化地表达语义。
语义(Semantics)需要形式化地表达(Formalization),否则没法可计算化。
一旦要表达,就必须选择一种记法,用符号来表达。这种记法:
- 一定要是可计算的;
- 语义层次要高。
注意:先不要考虑如何实现。
与实现语言无关的DSL 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
Java 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);
}
这里的Specification是精确无二义的,而且是可计算的,因为布尔代数本身就是精确、可计算的。
以上都是语义(Semantics)层面的。不考虑实现,只考虑语义。
找到一个跟FizzBuzzWhizz问题领域完全同构的布尔代数领域,把问题领域映射到布尔 代数领域,用布尔代数领域的语言(AND、OR)形式化地表达问题领域的整个语义模型。
设计的两个步骤:
- 找到合适的同构语义领域;
- 用同构语义领域的语言精确地形式化地表达语义。
这就是“建模”的本质。
在语义层面设计好以后,接下来就是选择合适的编程语言实现。
在实现层面,用一系列“接口”、“对象”来实现语义。
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);
}
在实现层面,用一系列“接口”和“对象”来实现语义。
代码包路径:fayelab.ddd.fbwreloaded.oo
在实现层面,用一系列“函数”来实现语义。
代码包路径:fayelab.ddd.fbwreloaded.functional
FizzBuzzWhizzTest.java
package fayelab.ddd.fbwreloaded.functional;
public class FizzBuzzWhizzTest extends TestCase
{
public void test_times()
{
Predicate<Integer> times3 = times(3);
assertTrue(times3.test(3));
assertTrue(times3.test(6));
assertFalse(times3.test(5));
Predicate<Integer> times5 = times(5);
assertTrue(times5.test(5));
assertTrue(times5.test(10));
assertFalse(times5.test(7));
Predicate<Integer> times7 = times(7);
assertTrue(times7.test(7));
assertTrue(times7.test(14));
assertFalse(times7.test(3));
}
public void test_contains()
{
Predicate<Integer> contains3 = contains(3);
assertTrue(contains3.test(3));
assertTrue(contains3.test(13));
assertTrue(contains3.test(31));
assertTrue(contains3.test(103));
assertTrue(contains3.test(130));
assertTrue(contains3.test(310));
assertFalse(contains3.test(5));
}
public void test_alwaysTrue()
{
Predicate<Integer> alwaysTrue = alwaysTrue();
assertTrue(alwaysTrue.test(1));
}
}
FizzBuzzWhizz.java
package fayelab.ddd.fbwreloaded.functional;
public class FizzBuzzWhizz
{
static Predicate<Integer> times(int base)
{
return n -> n % base == 0;
}
static Predicate<Integer> contains(int digit)
{
return n -> {
int p1 = n % 10;
int p2 = (n / 10) % 10;
int p3 = (n / 10 / 10) % 10;
return p1 == digit || p2 == digit || p3 == digit;
};
}
static Predicate<Integer> alwaysTrue()
{
return n -> true;
}
}
以 “times”为例。从语言层面来看,times(base)是一个High-order Function,因为返回的是一个函数。Java中更准确的说法是“函数接口”,Java中Lambda表达式的类型就是函数接口。从语义层面来看,times(base)是一个函数,这个函数的输入是一个整数,输出是一个布尔值。从语义理解更重要、也更容易。
FizzBuzzWhizzTest.java
public void test_toFizz()
{
Function<Integer, String> toFizz = toFizz();
assertEquals("Fizz", toFizz.apply(3));
}
public void test_toBuzz()
{
Function<Integer, String> toBuzz = toBuzz();
assertEquals("Buzz", toBuzz.apply(5));
}
public void test_toWhizz()
{
Function<Integer, String> toWhizz = toWhizz();
assertEquals("Whizz", toWhizz.apply(7));
}
public void test_toStr()
{
Function<Integer, String> toStr = toStr();
assertEquals("6", toStr.apply(6));
}
FizzBuzzWhizz.java
static Function<Integer, String> toFizz()
{
return n -> "Fizz";
}
static Function<Integer, String> toBuzz()
{
return n -> "Buzz";
}
static Function<Integer, String> toWhizz()
{
return n -> "Whizz";
}
static Function<Integer, String> toStr()
{
return n -> String.valueOf(n);
}
以 “toStr”为例。从语言层面来看,toStr()是一个High-order Function,因为返回的是一个函数。从语义层面来看,toStr()是一个函数,这个函数的输入是一个整数,输出是一个字符串。
FizzBuzzWhizzTest.java
public void test_atom()
{
Function<Integer, Optional<String>> r1_3 = atom(times(3), toFizz());
assertEquals("Fizz", r1_3.apply(6).get());
assertEquals(Optional.empty(), r1_3.apply(7));
Function<Integer, Optional<String>> r1_5 = atom(times(5), toBuzz());
assertEquals("Buzz", r1_5.apply(10).get());
assertEquals(Optional.empty(), r1_5.apply(11));
Function<Integer, Optional<String>> r1_7 = atom(times(7), toWhizz());
assertEquals("Whizz", r1_7.apply(14).get());
assertEquals(Optional.empty(), r1_7.apply(13));
Function<Integer, Optional<String>> r3 = atom(contains(3), toFizz());
assertEquals("Fizz", r3.apply(3).get());
assertEquals("Fizz", r3.apply(13).get());
assertEquals("Fizz", r3.apply(31).get());
assertEquals(Optional.empty(), r3.apply(24));
Function<Integer, Optional<String>> rd = atom(alwaysTrue(), toStr());
assertEquals("6", rd.apply(6).get());
}
FizzBuzzWhizz.java
static Function<Integer, Optional<String>> atom(
Predicate<Integer> predication, Function<Integer, String> action)
{
return n -> {
if(predication.test(n))
{
return Optional.of(action.apply(n));
}
return Optional.empty();
};
}
从语言层面来看,atom(predication, action)是一个High-order Function,因为返回的是一个函数。从语义层面来看,atom(predication, action)是一个函数,这个函数的输入是一个整数,输出是一个转换的结果,但也有可能没结果,用Optional这个语言构造来表达。
atom(predication, action)是一个构造器,接受一个predication和一个action构造出一个原子rule。这里的rule不再是一个对象,而是一个函数。这个函数的输入是一个整数,输出是一个Optional。
FizzBuzzWhizzTest.java
public void test_or()
{
Function<Integer, Optional<String>> r1_3 = atom(times(3), toFizz());
Function<Integer, Optional<String>> r1_5 = atom(times(5), toBuzz());
Function<Integer, Optional<String>> r1_7 = atom(times(7), toWhizz());
Function<Integer, Optional<String>> rd = atom(alwaysTrue(), toStr());
Function<Integer, Optional<String>> or_35 = or(r1_3, r1_5);
assertEquals("Fizz", or_35.apply(6).get());
assertEquals("Buzz", or_35.apply(10).get());
assertEquals("Fizz", or_35.apply(15).get());
assertEquals(Optional.empty(), or_35.apply(7));
Function<Integer, Optional<String>> or_357 = or3(r1_3, r1_5, r1_7);
assertEquals("Fizz", or_357.apply(6).get());
assertEquals("Buzz", or_357.apply(10).get());
assertEquals("Whizz", or_357.apply(14).get());
assertEquals(Optional.empty(), or_357.apply(13));
Function<Integer, Optional<String>> or_357d = or4(r1_3, r1_5, r1_7, rd);
assertEquals("Fizz", or_357d.apply(6).get());
assertEquals("Buzz", or_357d.apply(10).get());
assertEquals("Whizz", or_357d.apply(14).get());
assertEquals("13", or_357d.apply(13).get());
}
FizzBuzzWhizz.java
static Function<Integer, Optional<String>> or(
Function<Integer, Optional<String>> rule1, Function<Integer, Optional<String>> rule2)
{
return n -> {
Optional<String> result1 = rule1.apply(n);
if(result1.isPresent())
{
return result1;
}
return rule2.apply(n);
};
}
static Function<Integer, Optional<String>> or3(Function<Integer, Optional<String>> rule1,
Function<Integer, Optional<String>> rule2, Function<Integer, Optional<String>> rule3)
{
return or(rule1, or(rule2, rule3));
}
static Function<Integer, Optional<String>> or4(Function<Integer, Optional<String>> rule1,
Function<Integer, Optional<String>> rule2, Function<Integer, Optional<String>> rule3,
Function<Integer, Optional<String>> rule4)
{
return or(rule1, or3(rule2, rule3, rule4));
}
从语言层面来看,or(rule1, rule2)是一个High-order Function,因为返回的是一个函数。从语义层面来看,or(rule1, rule2)是一个函数,这个函数的输入是一个整数,输出是一个Optional。
or(rule1, rule2)是一个构造器,接受两个rule构造出一个新的rule。这里的rule不再是一个对象,而是一个函数。这个函数的输入是一个整数,输出是一个Optional。
FizzBuzzWhizzTest.java
public void test_and()
{
Function<Integer, Optional<String>> r1_3 = atom(times(3), toFizz());
Function<Integer, Optional<String>> r1_5 = atom(times(5), toBuzz());
Function<Integer, Optional<String>> r1_7 = atom(times(7), toWhizz());
Function<Integer, Optional<String>> and_35 = and(r1_3, r1_5);
assertEquals(Optional.empty(), and_35.apply(3));
assertEquals(Optional.empty(), and_35.apply(5));
assertEquals("FizzBuzz", and_35.apply(15).get());
assertEquals(Optional.empty(), and_35.apply(16));
Function<Integer, Optional<String>> and_37 = and(r1_3, r1_7);
assertEquals("FizzWhizz", and_37.apply(21).get());
assertEquals(Optional.empty(), and_37.apply(22));
Function<Integer, Optional<String>> and_57 = and(r1_5, r1_7);
assertEquals("BuzzWhizz", and_57.apply(35).get());
assertEquals(Optional.empty(), and_37.apply(36));
Function<Integer, Optional<String>> and_357 = and3(r1_3, r1_5, r1_7);
assertEquals("FizzBuzzWhizz", and_357.apply(3*5*7).get());
assertEquals(Optional.empty(), and_357.apply(104));
}
FizzBuzzWhizz.java
static Function<Integer, Optional<String>> and(
Function<Integer, Optional<String>> rule1, Function<Integer, Optional<String>> rule2)
{
return n -> {
Optional<String> result1 = rule1.apply(n);
if(!result1.isPresent())
{
return Optional.empty();
}
Optional<String> result2 = rule2.apply(n);
if(!result2.isPresent())
{
return Optional.empty();
}
return Optional.of(result1.get() + result2.get());
};
}
static Function<Integer, Optional<String>> and3(Function<Integer, Optional<String>> rule1,
Function<Integer, Optional<String>> rule2, Function<Integer, Optional<String>> rule3)
{
return and(rule1, and(rule2, rule3));
}
从语言层面来看,and(rule1, rule2)是一个High-order Function,因为返回的是一个函数。从语义层面来看,and(rule1, rule2)是一个函数,这个函数的输入是一个整数,输出是一个Optional。
and(rule1, rule2)是一个构造器,接受两个rule构造出一个新的rule。这里的rule不再是一个对象,而是一个函数。这个函数的输入是一个整数,输出是一个Optional。
对于and和or来说,参与计算的rule是原子rule还是组合rule都无所谓,只要实现同样的语义即可。这个语义就是一个函数,函数的输入是一个整数,输出是一个Optional。
and和or是组合子。两个rule用and或or组合以后还是一个rule,组合后的rule跟原子rule一样还可以和其它的rule再次组合,也就是说,and和or组合子在rule的世界里是封闭的,我们称这种性质为闭包性(Closure)。
组合子满足封闭性非常重要,因为只有满足封闭性,组合子才能和其它原子或者组合子再组合……。可以想象,如果一个语言提供的组合手段是能够封闭的,那么它就能够高效地帮助你构建出非常复杂的东西。
在OO实现方式中,通常用“接口”表示“是什么”,接口方法的输入、输出决定了“什么”的特征。用“接口”实现组合子的封闭性,对象只要满足了接口特征就是“什么”。 在Functional实现方式中,通常用“函数”表示“是什么”,函数的输入、输出决定了“什么”的特征。用“函数”实现组合子的封闭性,函数只要满足了输入输出特征就是“什么”。
FizzBuzzWhizzTest.java
public void test_spec()
{
Function<Integer, Optional<String>> spec = spec();
assertEquals("Fizz", spec.apply(35).get());
assertEquals("FizzWhizz", spec.apply(21).get());
assertEquals("BuzzWhizz", spec.apply(70).get());
assertEquals("Fizz", spec.apply(9).get());
assertEquals("1", spec.apply(1).get());
}
FizzBuzzWhizz.java
static Function<Integer, Optional<String>> spec()
{
Function<Integer, Optional<String>> r1_3 = atom(times(3), toFizz());
Function<Integer, Optional<String>> r1_5 = atom(times(5), toBuzz());
Function<Integer, Optional<String>> r1_7 = atom(times(7), toWhizz());
Function<Integer, Optional<String>> rd = atom(alwaysTrue(), toStr());
Function<Integer, Optional<String>> r1 = or3(r1_3, r1_5, r1_7);
Function<Integer, Optional<String>> r2 = or4(and3(r1_3, r1_5, r1_7),
and(r1_3, r1_5),
and(r1_3, r1_7),
and(r1_5, r1_7));
Function<Integer, Optional<String>> r3 = atom(contains(3), toFizz());
return or4(r3, r2, r1, rd);
}
如果不看类型,Functional实现方式里的Specification和OO实现方式里的Specification几乎是一样的。
FizzBuzzWhizz.java
public void run()
{
Function<Integer, Optional<String>> spec = spec();
List<Optional<String>> results = IntStream.rangeClosed(1, 100)
.mapToObj(spec::apply)
.collect(Collectors.toList());
output(results);
}
private void output(List<Optional<String>> results)
{
results.stream().map(Optional::get).forEach(System.out::println);
}
public static void main(String[] args)
{
new FizzBuzzWhizz().run();
}
语言能力是一把双刃剑。我们在享受编程语言的强大特性带来的好处的同时,也要警惕它们带来的阴暗面。正面影响是利用语言的强大特性实现起来很快,代码可能看上去会很“酷炫”。但其实负面影响更大,而且往往不容易察觉。语言能力太强大,在设计上就会偷懒,结果就是极大地损害了代码的可理解性,增加了不必要的复杂性。语言能力比较弱,其实是件好事情,会强迫你从问题出发、做出更好的设计,因为就那么点语言特性。
为了避免引起歧义,特别说明两点:
-
这里的“设计”是指课程中一直反复强调的“以语义为核心”的设计过程,不是指使用了接口、继承、OO设计模式或者高阶函数的那些所谓的“设计”。现在大家应该可以理解了,那些都只是语义实现手段。
-
那是不是说我们就不用学习、不使用语言新特性了呢?当然不是。在做好“以语义为核心”的设计的前提下,我们可以尽情享受语言的强大特性快速实现我们的设计。这就更需要我们不断提升自己的思考能力和把控能力。
如果没有接口,没有High-order Function,还能实现吗?如何实现呢?
在OO实现方式和函数式实现方式中,我们将语义的表达和执行解耦了,还存在耦合吗?解耦还不够彻底,还存在不易察觉的耦合。执行还可以再细分为执行方式和执行时机。在OO实现方式和函数式实现方式中,语义的表达和执行时机确实解耦了,Specification只是语义表达,定义Specification的时候并未执行,apply的时候才执行。但语义的表达和执行方式并未解耦。在OO实现方式中,语义的表达和执行方式均定义在了Atom、And、Or等对象中。在函数式实现方式中,语义的表达和执行方式均定义在了高阶函数中。这也是一种耦合,只是不那么容易被察觉。
举个例子,可以说得更清楚。现在要再实现一个功能——把FizzBuzzWhizz中的整个Rule的关系树打印出来。在OO实现方式和函数式实现方式中,要怎么修改代码呢?
需求变化了,代码就要修改。如果需求变化的复杂性和代码修改的复杂性是线性关系,这个设计就是一个非常好的设计,大部分情况下需求变化。代码修改一团糟,是完全非线性的。
能不能将语义的表达、执行时机和执行方式这三者彻底解耦呢?
代码包路径:fayelab.ddd.fbwreloaded.interpreter
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());
or4(r3, r2, r1, rd);
无论如何实现,Specification是不变的,语义是核心。Specification中的times|contains|alwaysTrue、toFizz|toBuzz|toWhizz|toStr、atom|and|or都表达了一个个的概念和语义。它们可以是对象,也可以是函数,也可以既不是对象也不是函数,那是什么呢?它们可以只是“数据”,表达语义即可。语义什么时候执行、如何执行是另外一件事情。
通过**“数据”**来表达领域的核心概念、逻辑和关系。
在Java语言中,所有自定义数据类型都需要用Class来定义。
Predication.java
package fayelab.ddd.fbwreloaded.interpreter;
public class Predication
{
enum Type {TIMES, CONTAINS, ALWAYSTRUE};
private Type type;
private int param;
public Predication(Type type, int param)
{
this.type = type;
this.param = param;
}
public Type getType()
{
return type;
}
public int getParam()
{
return param;
}
}
Predication不再是一个接口或者函数,而是一个纯数据类。用type标识Predication的类型,用param保存Predication所需要的参数。
Action.java
package fayelab.ddd.fbwreloaded.interpreter;
public class Action
{
enum Type {TOFIZZ, TOBUZZ, TOWHIZZ, TOSTR};
private Type type;
public Action(Type type)
{
this.type = type;
}
public Type getType()
{
return type;
}
}
Action不再是一个接口或者函数,而是一个纯数据类。用type标识Action的类型。
Rule.java
package fayelab.ddd.fbwreloaded.interpreter;
public class Rule
{
enum Type {ATOM, AND, OR};
private Type type;
private Object data;
public Rule(Type type, Object data)
{
this.type = type;
this.data = data;
}
public Type getType()
{
return type;
}
public Object getData()
{
return data;
}
}
class Atom
{
private Predication predication;
private Action action;
public Atom(Predication predication, Action action)
{
this.predication = predication;
this.action = action;
}
public Predication getPredication()
{
return predication;
}
public Action getAction()
{
return action;
}
}
class And
{
private Rule rule1;
private Rule rule2;
public And(Rule rule1, Rule rule2)
{
this.rule1 = rule1;
this.rule2 = rule2;
}
public Rule getRule1()
{
return rule1;
}
public Rule getRule2()
{
return rule2;
}
}
class Or
{
private Rule rule1;
private Rule rule2;
public Or(Rule rule1, Rule rule2)
{
this.rule1 = rule1;
this.rule2 = rule2;
}
public Rule getRule1()
{
return rule1;
}
public Rule getRule2()
{
return rule2;
}
}
Rule不再是一个接口或者函数,而是一个纯数据类,用type标识Rule的类型,用data保存Rule的具体数据。Atom、And和Or也不再是Rule接口的实现类或者函数,而是用来保存Rule具体数据的纯数据对象。这些纯数据对象中不包括任何行为。
And和Or的数据定义也是满足封闭性的。两个Rule用 And或Or组合以后还是一个Rule,组合后的Rule跟原子Rule一样还可以和其它的Rule再次组合,也就是说 ,And和Or组合子在Rule的世界里是封闭的。
SpecTool.java
package fayelab.ddd.fbwreloaded.interpreter;
public class SpecTool
{
private static int ANY_NUMBER = -1;
public static Predication times(int base)
{
return new Predication(TIMES, base);
}
public static Predication contains(int digit)
{
return new Predication(CONTAINS, digit);
}
public static Predication alwaysTrue()
{
return new Predication(ALWAYSTRUE, ANY_NUMBER);
}
public static Action toFizz()
{
return new Action(TOFIZZ);
}
public static Action toBuzz()
{
return new Action(TOBUZZ);
}
public static Action toWhizz()
{
return new Action(TOWHIZZ);
}
public static Action toStr()
{
return new Action(TOSTR);
}
public static Rule atom(Predication predication, Action action)
{
return new Rule(ATOM, new Atom(predication, action));
}
public static Rule or(Rule rule1, Rule rule2)
{
return new Rule(OR, new Or(rule1, rule2));
}
public static Rule or3(Rule rule1, Rule rule2, Rule rule3)
{
return or(rule1, or(rule2, rule3));
}
public static Rule or4(Rule rule1, Rule rule2, Rule rule3, Rule rule4)
{
return or(rule1, or3(rule2, rule3, rule4));
}
public static Rule and(Rule rule1, Rule rule2)
{
return new Rule(AND, new And(rule1, rule2));
}
public static Rule and3(Rule rule1, Rule rule2, Rule rule3)
{
return and(rule1, and(rule2, rule3));
}
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);
}
}
SpecTool中不再提供构造对象的工具方法,而是提供一系列构造数据的工具方法。
Specification看上去没变,但其实已经完全数据化了。OO实现方式和函数式实现方式中的Specification既有数据又有代码(行为)。这里的Specification只有数据,更抽象。
数据是声明性的(Declarative),告诉我们做什么(What)。代码是命令性的(Imperative),告诉我们如何做(How to)。声明性的语义表达力更强。
这里有个前提:在定义数据的时候,要从语义层面声明性地、精确地表达领域的核心概念、逻辑和关系(What),而不是从实现层面上定义数据结构(How to)。
这里的Specification其实只是一堆符号,它的语义在于如何去解释。
核心是数据,用数据去捕获领域的核心概念、逻辑和关系。
Specification定义好了,都是纯数据,怎么让它跑起来呢?
对于类似Java的静态语言,写好程序(.java)以后,需要先编译(Compile)成字节码(.class),再由虚拟机加载执行。
对于类似JavaScript的动态语言,写好程序以后,不需要编译(Compile),可以直接解释(Interpret)执行。
这些编译器(Compiler)和解释器(Interpreter)都是别人已经写好的,我们直接拿过来用就好。
同理,Specification是用我们自己设计的DSL写的程序,写好程序以后,可以直接解释执行,也可以先编译再执行。
这里的解释器和编译器需要我们自己写。不用害怕,不是那么困难。因为这里的解释器和编译器同样是面向特定领域的。
本次课程里,我们重点讲一下解释器,编译器以后有机会再讲。
Interpreter.java
private static Optional<String> applyRule(Rule rule, int n)
{
switch(rule.getType())
{
case ATOM:
return applyAtom((Atom)rule.getData(), n);
case OR:
return applyOr((Or)rule.getData(), n);
case AND:
return applyAnd((And)rule.getData(), n);
default:
return Optional.empty();
}
}
InterpreterTest.java
package fayelab.ddd.fbwreloaded.interpreter;
public class InterpreterTest extends TestCase
{
public void test_atom()
{
Rule r1_3 = atom(times(3), toFizz());
assertEquals("Fizz", interpret(r1_3, 6).get());
assertEquals(Optional.empty(), interpret(r1_3, 7));
Rule r1_5 = atom(times(5), toBuzz());
assertEquals("Buzz", interpret(r1_5, 10).get());
assertEquals(Optional.empty(), interpret(r1_5, 11));
Rule r1_7 = atom(times(7), toWhizz());
assertEquals("Whizz", interpret(r1_7, 14).get());
assertEquals(Optional.empty(), interpret(r1_7, 13));
Rule r3 = atom(contains(3), toFizz());
assertEquals("Fizz", interpret(r3, 3).get());
assertEquals("Fizz", interpret(r3, 13).get());
assertEquals("Fizz", interpret(r3, 31).get());
assertEquals(Optional.empty(), interpret(r3, 24));
Rule rd = atom(alwaysTrue(), toStr());
assertEquals("6", interpret(rd, 6).get());
}
}
Interpreter.java
package fayelab.ddd.fbwreloaded.interpreter;
public class Interpreter
{
public static Optional<String> interpret(Rule rule, int n)
{
return applyRule(rule, n);
}
private static Optional<String> applyRule(Rule rule, int n)
{
switch(rule.getType())
{
case ATOM:
return applyAtom((Atom)rule.getData(), n);
default:
return Optional.empty();
}
}
private static Optional<String> applyAtom(Atom atom, int n)
{
if(applyPredication(atom.getPredication(), n))
{
return Optional.of(applyAction(atom.getAction(), n));
}
return Optional.empty();
}
private static boolean applyPredication(Predication predication, int n)
{
switch(predication.getType())
{
case TIMES:
return times(predication.getParam(), n);
case CONTAINS:
return contains(predication.getParam(), n);
case ALWAYSTRUE:
return alwaysTrue();
default:
return false;
}
}
private static String applyAction(Action action, int n)
{
switch(action.getType())
{
case TOFIZZ:
return "Fizz";
case TOBUZZ:
return "Buzz";
case TOWHIZZ:
return "Whizz";
case TOSTR:
return String.valueOf(n);
default:
return "";
}
}
private static boolean times(int base, int n)
{
return n % base == 0;
}
private static boolean contains(int digit, int n)
{
int p1 = n % 10;
int p2 = (n / 10) % 10;
int p3 = (n / 10 / 10) % 10;
return p1 == digit || p2 == digit || p3 == digit;
}
private static boolean alwaysTrue()
{
return true;
}
}
InterpreterTest.java
public void test_or()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule rd = atom(alwaysTrue(), toStr());
Rule or_35 = or(r1_3, r1_5);
assertEquals("Fizz", interpret(or_35, 6).get());
assertEquals("Buzz", interpret(or_35, 10).get());
assertEquals("Fizz", interpret(or_35, 15).get());
assertEquals(Optional.empty(), interpret(or_35, 7));
Rule or_357 = or3(r1_3, r1_5, r1_7);
assertEquals("Fizz", interpret(or_357, 6).get());
assertEquals("Fizz", interpret(or_357, 6).get());
assertEquals("Buzz", interpret(or_357, 10).get());
assertEquals("Whizz", interpret(or_357, 14).get());
assertEquals(Optional.empty(), interpret(or_357, 13));
Rule or_357d = or4(r1_3, r1_5, r1_7, rd);
assertEquals("Fizz", interpret(or_357d, 6).get());
assertEquals("Buzz", interpret(or_357d, 10).get());
assertEquals("Whizz", interpret(or_357d, 14).get());
assertEquals("13", interpret(or_357d, 13).get());
}
Interpreter.java
private static Optional<String> applyRule(Rule rule, int n)
{
switch(rule.getType())
{
case ATOM:
return applyAtom((Atom)rule.getData(), n);
case OR:
return applyOr((Or)rule.getData(), n);
default:
return Optional.empty();
}
}
private static Optional<String> applyOr(Or or, int n)
{
Optional<String> result1 = applyRule(or.getRule1(), n);
if(result1.isPresent())
{
return result1;
}
return applyRule(or.getRule2(), n);
}
InterpreterTest.java
public void test_and()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule and_35 = and(r1_3, r1_5);
assertEquals(Optional.empty(), interpret(and_35, 3));
assertEquals(Optional.empty(), interpret(and_35, 5));
assertEquals("FizzBuzz", interpret(and_35, 15).get());
assertEquals(Optional.empty(), interpret(and_35, 16));
Rule and_37 = and(r1_3, r1_7);
assertEquals("FizzWhizz", interpret(and_37, 21).get());
assertEquals(Optional.empty(), interpret(and_37, 22));
Rule and_57 = and(r1_5, r1_7);
assertEquals("BuzzWhizz", interpret(and_57, 35).get());
assertEquals(Optional.empty(), interpret(and_57, 36));
Rule and_357 = and3(r1_3, r1_5, r1_7);
assertEquals("FizzBuzzWhizz", interpret(and_357, 3*5*7).get());
assertEquals(Optional.empty(), interpret(and_357, 104));
}
Interpreter.java
private static Optional<String> applyRule(Rule rule, int n)
{
switch(rule.getType())
{
case ATOM:
return applyAtom((Atom)rule.getData(), n);
case OR:
return applyOr((Or)rule.getData(), n);
case AND:
return applyAnd((And)rule.getData(), n);
default:
return Optional.empty();
}
}
private static Optional<String> applyAnd(And and, int n)
{
Optional<String> result1 = applyRule(and.getRule1(), n);
if(!result1.isPresent())
{
return Optional.empty();
}
Optional<String> result2 = applyRule(and.getRule2(), n);
if(!result2.isPresent())
{
return Optional.empty();
}
return Optional.of(result1.get() + result2.get());
}
FizzBuzzWhizz.java
package fayelab.ddd.fbwreloaded.interpreter;
public class FizzBuzzWhizz
{
public void run()
{
List<Optional<String>> results = IntStream.rangeClosed(1, 100)
.mapToObj(n -> interpret(spec(), n))
.collect(Collectors.toList());
output(results);
}
private void output(List<Optional<String>> results)
{
results.stream().map(Optional::get).forEach(System.out::println);
}
public static void main(String[] args)
{
new FizzBuzzWhizz().run();
}
}
- Look at a piece of data.
- Decide what kind of data it represents.
- Extract the components of the datum and do the right thing with them.
在前面的语义实现里,Specification是用我们自己设计的DSL写的程序,Interpreter是我们自己写的解释器,解释执行Specification。没有用到Java语言的高级特性,例如接口、对象、高阶函数等,只用了Java语言最基本的语言特性。
通过设计来解决问题,而不是用你选择的实现语言的构造来解决问题。先有设计,再选择合适的语言构造来表达,一定要强迫自己使用最简单的语言构造解决问题,除非实在没有办法,再选择高级的语言特性解决问题。要记住:语言越强,复杂性越高,而控制复杂性是软件工程里要解决的唯一问题。
在前面的实现里,语义的表达、执行方式、执行时机三者彻底解耦,实现了计算描述与执行的分离。
计算描述与执行分离这种设计思想非常强大。它带来的好处是:一方面在描述层面可以很自由,可以任意组装,不受限于执行层面;另一方面在执行层面可以做很多优化,比如并发,或者解释成其他的结果,比如把FizzBuzzWhizz中的整个Rule的关系树打印出来,只要再写一个解释器即可,需求变化的复杂性和代码修改的复杂性是线性关系,重要的是还不影响描述层面。
前提是先要有语义,解耦到什么程度,看具体问题和你的选择,但作为设计者要知道可能存在的耦合。
设计模式里最后一个模式是Interpreter模式,但这个模式很少有人提,但其实是最重要的。
switch/case一般会认为是一种bad smell,但switch/case并不总是魔鬼。其实switch/case是一种非常好的语言构造。代码有switch/case不能说好,但好的代码一定存在switch/case,说明程序中存在层次。
解释器的核心就是switch/case,switch/case的上面是Specification,switch/case下面是通用语言(Java/Python/C),switch/case把两者隔离开了。
所以,如果switch/case是用来解决层次问题的话,就是好的switch/case,千万不要把它改掉。一个好的程序一定是一个语义层次分明的程序,里面一定有switch/case,一层一层的,下层解释上层。
当然把程序写得很糟糕,也会有很多switch/case。
虽然在这次课程里重点讲解了Interpreter,但还是想从整体上将Interpreter和Compiler再给大家介绍一下。
下面从整体来介绍一下Interpreter和Compiler。
对问题领域进行深入分析,设计出其语义与问题领域根本需求相匹配的计算模型,围绕这个计算模型设计出贴合问题领域的DSL。
基于这个DSL设计一套小语言,用这套小语言编写程序(Program),作为解析器(Parser)的输入。解析器一般包括两个步骤:Tokenizer/Lexical Analysis和Syntax/Grammar。Tokenizer/Lexical Analysis会根据DSL的语法定义将分隔符分隔的每个字段取出,并进行词法分析。Syntax/Grammar会根据语法定义检查语法的合法性。这两个步骤的区分并不是那么严格,有时Tokenizer/Lexical Analysis也可以做一些语法检查。解析器一般不用自己实现,现在有很多开源的第三方工具帮我们做这件事情。
解析器的输出是生成抽象语法树(AST:Abstract Syntax Tree)。这是我们的设计中最核心的部分。这部分只有数据,数据是核心,通过数据和数据结构声明性地表达了领域的核心概念、逻辑和关系。
FizzBuzzWhizz的AST如下:
以上均属于描述层面。
执行层面有两种方式:解释器(Interpreter)和编译器(Compiler)。
- 可以将解释器的作用理解为一台语义执行机器,实现了当前的语义。Interpreter一般都包含switch/case和递归调用。
- 可以将编译器的作用理解为保持语义的等价语法变换。变换成了另外一种语言(比如字节码、机器指令等),最终都要由解释器(比如虚拟机、硬件等)解释执行。
解释器是一边解释、一边执行。这样的实现方式导致:一方面执行的中间结果没法重用的,另一方面无法做语义上的优化,所以效率相对比较低下,但也更为通用。
编译器是编译一次、可以反复执行。编译时可以针对特定语义进行语义上的优化,所以效率相对比较高,但比较特定。
下面来编写一个从Program到Parser到Interpreter的完整程序。
代码包路径:fayelab.ddd.fbwreloaded.complete.interpreter
为了完整的展示Overview中的过程,基于之前设计的DSL简单设计了一套小语言,并用这套小语言编写了程序“prog_1.fbw”。
prog_1.fbw
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
这套小语言的语法非常简单:
- 每行只描述一个规则(Rule);
- 每个Rule的字段用空格分隔。第1个字段含义固定是Rule的名称,第2个字段含义固定是Rule的类型,根据不同的Rule类型,后续字段的含义会有所不同:
a) 当Rule类型是“atom”时,后续字段的含义依次是Predication类型、Predication参数(可能没有,例如always_true就不需要这个字段)、Action类型; b) 当Rule类型是“or”或“and”时,后续字段的含义是参与or或者and运算的Rule名称。 - 被引用的Rule必须是提前定义好的;
- 全小写;
- 为了能够清晰表述,可以用空行分段,不小心多敲了空格也没关系。
实现解析器,包括以下步骤:
- 不想在Parser中区分“or2|3|4”和“and2|3”,所以在SpecTool类中增加or(...)、or(List)、and(...)和and(List)静态工具方法,并删除or3、or4和and3等静态工具方法。
- 在单元测试代码中需要比较期望Rule和实际解析出来的Rule的相等性,不想在Rule的各种数据对象中覆写equals()方法,所以实现RuleEqualityTool类提供Rule的相等性比较,后面会看到,从某种角度来说这其实也是一个解释器。
- 最终实现解析器Parser。
另外,由于只是练习演示需要,这里的Parser没有实现语法检查功能,只实现了简单的解析功能。
不想在Parser中区分“or2|3|4”和“and2|3”,所以在SpecTool类中增加or(...)、or(List)、and(...)和and(List)静态工具方法,并删除or3、or4和and3等静态工具方法。
InterpreterTest.java
public void test_or()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule rd = atom(alwaysTrue(), toStr());
Rule or_35 = or(r1_3, r1_5);
assertEquals("Fizz", interpret(or_35, 6).get());
assertEquals("Buzz", interpret(or_35, 10).get());
assertEquals("Fizz", interpret(or_35, 15).get());
assertEquals(Optional.empty(), interpret(or_35, 7));
Rule or_357 = or(r1_3, r1_5, r1_7);
assertEquals("Fizz", interpret(or_357, 6).get());
assertEquals("Fizz", interpret(or_357, 6).get());
assertEquals("Buzz", interpret(or_357, 10).get());
assertEquals("Whizz", interpret(or_357, 14).get());
assertEquals(Optional.empty(), interpret(or_357, 13));
Rule or_357d = or(r1_3, r1_5, r1_7, rd);
assertEquals("Fizz", interpret(or_357d, 6).get());
assertEquals("Buzz", interpret(or_357d, 10).get());
assertEquals("Whizz", interpret(or_357d, 14).get());
assertEquals("13", interpret(or_357d, 13).get());
Rule or_357d_2 = or(asList(r1_3, r1_5, r1_7, rd));
assertEquals("Fizz", interpret(or_357d_2, 6).get());
assertEquals("Buzz", interpret(or_357d_2, 10).get());
assertEquals("Whizz", interpret(or_357d_2, 14).get());
assertEquals("13", interpret(or_357d_2, 13).get());
}
public void test_and()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule and_35 = and(r1_3, r1_5);
assertEquals(Optional.empty(), interpret(and_35, 3));
assertEquals(Optional.empty(), interpret(and_35, 5));
assertEquals("FizzBuzz", interpret(and_35, 15).get());
assertEquals(Optional.empty(), interpret(and_35, 16));
Rule and_37 = and(r1_3, r1_7);
assertEquals("FizzWhizz", interpret(and_37, 21).get());
assertEquals(Optional.empty(), interpret(and_37, 22));
Rule and_57 = and(r1_5, r1_7);
assertEquals("BuzzWhizz", interpret(and_57, 35).get());
assertEquals(Optional.empty(), interpret(and_57, 36));
Rule and_357 = and(r1_3, r1_5, r1_7);
assertEquals("FizzBuzzWhizz", interpret(and_357, 3*5*7).get());
assertEquals(Optional.empty(), interpret(and_357, 104));
Rule and_357_2 = and(asList(r1_3, r1_5, r1_7));
assertEquals("FizzBuzzWhizz", interpret(and_357_2, 3*5*7).get());
assertEquals(Optional.empty(), interpret(and_357_2, 104));
}
SpecTool.java
public static Rule or(Rule...rules)
{
return or(asList(rules));
}
public static Rule or(List<Rule> rules)
{
return combine(rules, SpecTool::or);
}
public static Rule and(Rule...rules)
{
return and(asList(rules));
}
public static Rule and(List<Rule> rules)
{
return combine(rules, SpecTool::and);
}
private static Rule combine(List<Rule> rules, BiFunction<Rule, Rule, Rule> biCombine)
{
if(rules.size() == 1)
{
return rules.get(0);
}
return biCombine.apply(rules.get(0), combine(rules.subList(1, rules.size()), biCombine));
}
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 = or(r1_3, r1_5, r1_7);
Rule r2 = or(and(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 or(r3, r2, r1, rd);
}
在单元测试代码中需要比较期望Rule和实际解析出来的Rule的相等性,不想在Rule的各种数据对象中覆写equals()方法,所以实现RuleEqualityTool类提供Rule的相等性比较。后面会看到,从某种角度来说这其实也是一个解释器。
RuleEqualityToolTest.java
package fayelab.ddd.fbwreloaded.complete.interpreter;
public class RuleEqualityToolTest extends TestCase
{
public void test_equal_predication()
{
assertTrue(equalPredication(times(3), times(3)));
assertFalse(equalPredication(times(3), contains(3)));
assertFalse(equalPredication(times(3), times(5)));
}
}
RuleEqualityTool.java
package fayelab.ddd.fbwreloaded.complete.interpreter;
public class RuleEqualityTool
{
static boolean equalPredication(Predication predication1, Predication predication2)
{
return predication1.getType() == predication2.getType()
&& predication1.getParam() == predication2.getParam();
}
}
RuleEqualityToolTest.java
public void test_equal_action()
{
assertTrue(equalAction(toFizz(), toFizz()));
assertFalse(equalAction(toFizz(), toBuzz()));
}
RuleEqualityTool.java
static boolean equalAction(Action action1, Action action2)
{
return action1.getType() == action2.getType();
}
RuleEqualityToolTest.java
public void test_equal_atom()
{
assertTrue(equalAtom(new Atom(times(3), toFizz()), new Atom(times(3), toFizz())));
assertFalse(equalAtom(new Atom(times(3), toFizz()), new Atom(times(5), toFizz())));
assertFalse(equalAtom(new Atom(times(3), toFizz()), new Atom(times(3), toBuzz())));
}
RuleEqualityTool.java
static boolean equalAtom(Atom atom1, Atom atom2)
{
return equalPredication(atom1.getPredication(), atom2.getPredication())
&& equalAction(atom1.getAction(), atom2.getAction());
}
涉及到递归调用,除了实现equalOr(),还要实现equal()的atom和or分支。
RuleEqualityToolTest.java
public void test_equal_or()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Or or_35_1 = new Or(r1_3, r1_5);
Or or_35_2 = new Or(r1_3, r1_5);
Or or_37 = new Or(r1_3, r1_7);
Or or_357_1 = new Or(r1_3, or(r1_5, r1_7));
Or or_357_2 = new Or(or(r1_3, r1_5), r1_7);
assertTrue(equalOr(or_35_1, or_35_2));
assertFalse(equalOr(or_35_1, or_37));
assertFalse(equalOr(or_357_1, or_357_2));
}
RuleEqualityTool.java
public static boolean equal(Rule rule1, Rule rule2)
{
if(rule1.getType() == rule2.getType())
{
switch(rule1.getType())
{
case ATOM:
return equalAtom((Atom)rule1.getData(), (Atom)rule2.getData());
case OR:
return equalOr((Or)rule1.getData(), (Or)rule2.getData());
default:
return false;
}
}
return false;
}
static boolean equalOr(Or or1, Or or2)
{
return equal(or1.getRule1(), or2.getRule1())
&& equal(or1.getRule2(), or2.getRule2());
}
涉及到递归调用,除了实现equalAnd(),还要实现equal()的and分支。
RuleEqualityToolTest.java
public void test_equal_and()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
And and_35_1 = new And(r1_3, r1_5);
And and_35_2 = new And(r1_3, r1_5);
And and_37 = new And(r1_3, r1_7);
And and_357_1 = new And(r1_3, and(r1_5, r1_7));
And and_357_2 = new And(and(r1_3, r1_5), r1_7);
assertTrue(equalAnd(and_35_1, and_35_2));
assertFalse(equalAnd(and_35_1, and_37));
assertFalse(equalAnd(and_357_1, and_357_2));
}
RuleEqualityTool.java
public static boolean equal(Rule rule1, Rule rule2)
{
if(rule1.getType() == rule2.getType())
{
switch(rule1.getType())
{
case ATOM:
return equalAtom((Atom)rule1.getData(), (Atom)rule2.getData());
case OR:
return equalOr((Or)rule1.getData(), (Or)rule2.getData());
case AND:
return equalAnd((And)rule1.getData(), (And)rule2.getData());
default:
return false;
}
}
return false;
}
static boolean equalAnd(And and1, And and2)
{
return equal(and1.getRule1(), and2.getRule1())
&& equal(and1.getRule2(), and2.getRule2());
}
在已有的equal()基础上增加一些异常情况的处理。
RuleEqualityToolTest.java
public void test_equal()
{
Rule spec1 = spec();
Rule spec2 = spec();
assertTrue(equal(spec1, spec1));
assertTrue(equal(null, null));
assertFalse(equal(spec1, null));
assertFalse(equal(null, spec1));
assertTrue(equal(spec1, spec2));
}
RuleEqualityTool.java
public static boolean equal(Rule rule1, Rule rule2)
{
if(rule1 == rule2)
{
return true;
}
if(rule1 != null && rule2 != null && rule1.getType() == rule2.getType())
{
switch(rule1.getType())
{
case ATOM:
return equalAtom((Atom)rule1.getData(), (Atom)rule2.getData());
case OR:
return equalOr((Or)rule1.getData(), (Or)rule2.getData());
case AND:
return equalAnd((And)rule1.getData(), (And)rule2.getData());
default:
return false;
}
}
return false;
}
编写tokenize()方法。
从文件读入程序操作的输出是列表(List),列表的元素是一个规则的描述字符串,作为tokenize()方法的输入。tokenize()方法的输出是列表的列表(List<List>)。内层列表的元素是规格描述字段。外层列表的元素是每一个规则对应的描述字段列表。
tokenize: (List<String> ruleDescs) -> (List<List<String>> tokens)
tokenize操作分为以下几个步骤:
- 过滤空行;
- 规格化每一个规则描述(normalize):去掉前后空格,字段之间多于一个的空格替换为一个空格;
- 以一个空格为分隔符split每个规格化后的规则描述。
ParserTest.java
package fayelab.ddd.fbwreloaded.complete.interpreter;
public class ParserTest extends TestCase
{
private static final List<List<String>> SPEC_TOKENS = asList(
asList("r1_3", "atom", "times", "3", "to_fizz"),
asList("r1_5", "atom", "times", "5", "to_buzz"),
asList("r1_7", "atom", "times", "7", "to_whizz"),
asList("r1", "or", "r1_3", "r1_5", "r1_7"),
asList("r1_357", "and", "r1_3", "r1_5", "r1_7"),
asList("r1_35", "and", "r1_3", "r1_5"),
asList("r1_37", "and", "r1_3", "r1_7"),
asList("r1_57", "and", "r1_5", "r1_7"),
asList("r2", "or", "r1_357", "r1_35", "r1_37", "r1_57"),
asList("r3", "atom", "contains", "3", "to_fizz"),
asList("rd", "atom", "always_true", "to_str"),
asList("spec", "or", "r3", "r2", "r1", "rd"));
public void test_tokenize()
{
List<String> ruleDescs = asList(
"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 ");
assertEquals(SPEC_TOKENS, tokenize(ruleDescs));
}
}
Parser.java
package fayelab.ddd.fbwreloaded.complete.interpreter;
public class Parser
{
private static final String DELIMETER = " ";
static List<List<String>> tokenize(List<String> ruleDescs)
{
return ruleDescs.stream()
.filter(line -> isNotBlank(line))
.map(line -> asList(normalize(line).split(DELIMETER)))
.collect(toList());
}
static boolean isNotBlank(String line)
{
return !line.trim().isEmpty();
}
static String normalize(String line)
{
return line.trim().replaceAll(" +", DELIMETER);
}
}
编写parseTokens()方法。
tokenize()的输出(List<List> tokens)是parseTokens()方法的输入,parseTokens()方法的输出是最终的specRule。
parseTokens: (List<List<String>> tokens) -> (Rule specRule)
在parseTokens()中,遍历输入列表,列表的每个元素是一个规则的描述字段列表,通过parseRuleTokens()方法先将这个规则描述解析为Rule对象。由于规则之间存在引用,所以用一个Key为Rule名称、Value为Rule对象的Map<String, Rule>保存由每个规则描述解析出的Rule名称和Rule对象。在parseRuleTokens()中解析出的Rule对象要放入这个Map中。
parseRuleTokens: (List<String> ruleTokens, Map<String, Rule>) -> void
parseRuleTokens()内部要区分Rule的类型。
所以,parseTokens()的实现步骤为:
- parseAtom
- parseOr
- parseAnd
- parseRuleTokens
- parseTokens
其中,parseAtom()的输入是(List ruleTokens),但or和and因为引用了其他Rule数据,parseOr()和parseAnd()还需要Map作为第二个参数。它们的输出都是Rule数据,当然Rule数据的具体内容是不一样的。
parseAtom: (List<String> ruleTokens) -> (Rule atomRule)
parseOr: (List<String> ruleTokens, Map<String, Rule> ruleNameAndRuleMap) -> (Rule orRule)
parseAnd: (List<String> ruleTokens, Map<String, Rule> ruleNameAndRuleMap) -> (Rule andRule)
ParserTest.java
public void test_parseAtom()
{
assertTrue(equal(atom(times(3), toFizz()), parseAtom(asList("r1_3", "atom", "times", "3", "to_fizz"))));
assertTrue(equal(atom(contains(3), toFizz()), parseAtom(asList("r3", "atom", "contains", "3", "to_fizz"))));
assertTrue(equal(atom(alwaysTrue(), toStr()), parseAtom(asList("rd", "atom", "always_true", "to_str"))));
}
Parser.java
private static Map<String, Function<String, Predication>> predicationTypeAndFuncMap = null;
static
{
predicationTypeAndFuncMap = new HashMap<>();
predicationTypeAndFuncMap.put("times", param -> times(Integer.parseInt(param)));
predicationTypeAndFuncMap.put("contains", param -> contains(Integer.parseInt(param)));
predicationTypeAndFuncMap.put("always_true", param -> alwaysTrue());
}
private static Map<String, Supplier<Action>> actionTypeAndFuncMap = null;
static
{
actionTypeAndFuncMap = new HashMap<>();
actionTypeAndFuncMap.put("to_fizz", SpecTool::toFizz);
actionTypeAndFuncMap.put("to_buzz", SpecTool::toBuzz);
actionTypeAndFuncMap.put("to_whizz", SpecTool::toWhizz);
actionTypeAndFuncMap.put("to_str", SpecTool::toStr);
}
static Rule parseAtom(List<String> ruleTokens)
{
String predicationType = extractPredicationType(ruleTokens);
String predicationParam = extractPredicationParam(ruleTokens, predicationType);
String actionType = extractActionType(ruleTokens, predicationType);
return atom(parsePredication(predicationType, predicationParam), parseAction(actionType));
}
private static String extractPredicationType(List<String> ruleTokens)
{
return ruleTokens.get(2);
}
private static String extractPredicationParam(List<String> ruleTokens, String predicationType)
{
return "always_true".equals(predicationType) ? null : ruleTokens.get(3);
}
private static String extractActionType(List<String> ruleTokens, String predicationType)
{
return "always_true".equals(predicationType) ? ruleTokens.get(3) : ruleTokens.get(4);
}
private static Predication parsePredication(String predicationType, String predicationParam)
{
return predicationTypeAndFuncMap.get(predicationType).apply(predicationParam);
}
private static Action parseAction(String actionType)
{
return actionTypeAndFuncMap.get(actionType).get();
}
ParserTest.java
public void test_parseOr()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Map<String, Rule> ruleNameAndRuleMap = new HashMap<String, Rule>();
ruleNameAndRuleMap.put("r1_3", r1_3);
ruleNameAndRuleMap.put("r1_5", r1_5);
ruleNameAndRuleMap.put("r1_7", r1_7);
assertTrue(equal(or(r1_3, r1_5, r1_7),
parseOr(asList("or", "r1", "r1_3", "r1_5", "r1_7"), ruleNameAndRuleMap)));
}
Parser.java
static Rule parseOr(List<String> ruleTokens, Map<String, Rule> ruleNameAndRuleMap)
{
List<Rule> refRules = ruleTokens.subList(2, ruleTokens.size())
.stream()
.map(ruleNameAndRuleMap::get)
.collect(toList());
return or(refRules);
}
ParserTest.java
public void test_parseAnd()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Map<String, Rule> ruleNameAndRuleMap = new HashMap<String, Rule>();
ruleNameAndRuleMap.put("r1_3", r1_3);
ruleNameAndRuleMap.put("r1_5", r1_5);
ruleNameAndRuleMap.put("r1_7", r1_7);
assertTrue(equal(and(r1_3, r1_5, r1_7),
parseAnd(asList("and", "r1_357", "r1_3", "r1_5", "r1_7"), ruleNameAndRuleMap)));
}
Parser.java
static Rule parseAnd(List<String> ruleTokens, Map<String, Rule> ruleNameAndRuleMap)
{
List<Rule> refRules = ruleTokens.subList(2, ruleTokens.size())
.stream()
.map(ruleNameAndRuleMap::get)
.collect(toList());
return and(refRules);
}
测试代码和产品代码都有重复,重构一下。
ParserTest.java
private Rule r1_3;
private Rule r1_5;
private Rule r1_7;
private Map<String, Rule> ruleNameAndRuleMap;
@Override
protected void setUp()
{
r1_3 = atom(times(3), toFizz());
r1_5 = atom(times(5), toBuzz());
r1_7 = atom(times(7), toWhizz());
ruleNameAndRuleMap = new HashMap<String, Rule>();
ruleNameAndRuleMap.put("r1_3", r1_3);
ruleNameAndRuleMap.put("r1_5", r1_5);
ruleNameAndRuleMap.put("r1_7", r1_7);
}
public void test_parseOr()
{
assertTrue(equal(or(r1_3, r1_5, r1_7),
parseOr(asList("r1", "or", "r1_3", "r1_5", "r1_7"), ruleNameAndRuleMap)));
}
public void test_parseAnd()
{
assertTrue(equal(and(r1_3, r1_5, r1_7),
parseAnd(asList("r1_357", "and", "r1_3", "r1_5", "r1_7"), ruleNameAndRuleMap)));
}
Parser.java
static Rule parseOr(List<String> ruleTokens, Map<String, Rule> ruleNameAndRuleMap)
{
return or(parseRefRules(ruleTokens, ruleNameAndRuleMap));
}
static Rule parseAnd(List<String> ruleTokens, Map<String, Rule> ruleNameAndRuleMap)
{
return and(parseRefRules(ruleTokens, ruleNameAndRuleMap));
}
private static List<Rule> parseRefRules(List<String> ruleTokens, Map<String, Rule> ruleNameAndRuleMap)
{
List<Rule> refRules = ruleTokens.subList(2, ruleTokens.size())
.stream()
.map(ruleNameAndRuleMap::get)
.collect(toList());
return refRules;
}
先来解析atom rule tokens。
ParserTest.java
public void test_parseRuleTokens_atom()
{
Map<String, Rule> actual = new HashMap<>();
parseRuleTokens(asList("r1_3", "atom", "times", "3", "to_fizz"), actual);
Map<String, Rule> expected = new HashMap<>();
expected.put("r1_3", r1_3);
assertMap(expected, actual);
}
Parser.java
static void parseRuleTokens(List<String> ruleTokens, Map<String, Rule> ruleNameAndRuleMap)
{
String ruleName = extractRuleName(ruleTokens);
String ruleType = extractRuleType(ruleTokens);
Rule rule = null;
if("atom".equals(ruleType))
{
rule = parseAtom(ruleTokens);
}
ruleNameAndRuleMap.putIfAbsent(ruleName, rule);
}
private static String extractRuleName(List<String> ruleTokens)
{
return ruleTokens.get(0);
}
private static String extractRuleType(List<String> ruleTokens)
{
return ruleTokens.get(1);
}
再来解析or rule tokens。 ParserTest.java
public void test_parseRuleTokens_or()
{
Map<String, Rule> actual = new HashMap<>();
parseRuleTokens(asList("r1_3", "atom", "times", "3", "to_fizz"), actual);
parseRuleTokens(asList("r1_5", "atom", "times", "5", "to_buzz"), actual);
parseRuleTokens(asList("r1_7", "atom", "times", "7", "to_whizz"), actual);
parseRuleTokens(asList("r1", "or", "r1_3", "r1_5", "r1_7"), actual);
Map<String, Rule> expected = new HashMap<>();
expected.put("r1_3", r1_3);
expected.put("r1_5", r1_5);
expected.put("r1_7", r1_7);
expected.put("r1", or(r1_3, r1_5, r1_7));
assertMap(expected, actual);
}
Parser.java
static void parseRuleTokens(List<String> ruleTokens, Map<String, Rule> ruleNameAndRuleMap)
{
String ruleName = extractRuleName(ruleTokens);
String ruleType = extractRuleType(ruleTokens);
Rule rule = null;
if("atom".equals(ruleType))
{
rule = parseAtom(ruleTokens);
}
else if("or".equals(ruleType))
{
rule = parseOr(ruleTokens, ruleNameAndRuleMap);
}
ruleNameAndRuleMap.putIfAbsent(ruleName, rule);
}
再来解析and rule tokens。 ParserTest.java
public void test_parseRuleTokens_and()
{
Map<String, Rule> actual = new HashMap<>();
parseRuleTokens(asList("r1_3", "atom", "times", "3", "to_fizz"), actual);
parseRuleTokens(asList("r1_5", "atom", "times", "5", "to_buzz"), actual);
parseRuleTokens(asList("r1_7", "atom", "times", "7", "to_whizz"), actual);
parseRuleTokens(asList("r1_357", "and", "r1_3", "r1_5", "r1_7"), actual);
Map<String, Rule> expected = new HashMap<>();
expected.put("r1_3", r1_3);
expected.put("r1_5", r1_5);
expected.put("r1_7", r1_7);
expected.put("r1_357", and(r1_3, r1_5, r1_7));
assertMap(expected, actual);
}
Parser.java
static void parseRuleTokens(List<String> ruleTokens, Map<String, Rule> ruleNameAndRuleMap)
{
String ruleName = extractRuleName(ruleTokens);
String ruleType = extractRuleType(ruleTokens);
Rule rule = null;
if("atom".equals(ruleType))
{
rule = parseAtom(ruleTokens);
}
else if("or".equals(ruleType))
{
rule = parseOr(ruleTokens, ruleNameAndRuleMap);
}
else
{
rule = parseAnd(ruleTokens, ruleNameAndRuleMap);
}
ruleNameAndRuleMap.putIfAbsent(ruleName, rule);
}
最后解析spec,其实在这之前parseRuleTokens()的代码就已经写完了,这里只是用完整的spec测试一下,并做一下重构。
ParserTest.java
public void test_parseRuleTokens_spec()
{
Map<String, Rule> actual = new HashMap<>();
SPEC_TOKENS.stream().forEach(ruleTokens -> parseRuleTokens(ruleTokens, actual));
Map<String, Rule> expected = new HashMap<>();
expected.put("r1_3", r1_3);
expected.put("r1_5", r1_5);
expected.put("r1_7", r1_7);
Rule r1 = or(r1_3, r1_5, r1_7);
expected.put("r1", r1);
Rule r1_357 = and(r1_3, r1_5, r1_7);
expected.put("r1_357", r1_357);
Rule r1_35 = and(r1_3, r1_5);
expected.put("r1_35", r1_35);
Rule r1_37 = and(r1_3, r1_7);
expected.put("r1_37", r1_37);
Rule r1_57 = and(r1_5, r1_7);
expected.put("r1_57", r1_57);
Rule r2 = or(r1_357, r1_35, r1_37, r1_57);
expected.put("r2", r2);
Rule r3 = atom(contains(3), toFizz());
expected.put("r3", r3);
Rule rd = atom(alwaysTrue(), toStr());
expected.put("rd", rd);
Rule spec = or(r3, r2, r1, rd);
expected.put("spec", spec);
assertMap(expected, actual);
}
重构一下产品代码。
Parser.java
private static Map<String, BiFunction<List<String>, Map<String, Rule>, Rule>> ruleTypeAndFuncMap = null;
static
{
ruleTypeAndFuncMap = new HashMap<>();
ruleTypeAndFuncMap.put("atom", (ruleTokens, ruleNameAndRuleMap) -> parseAtom(ruleTokens));
ruleTypeAndFuncMap.put("or", Parser::parseOr);
ruleTypeAndFuncMap.put("and", Parser::parseAnd);
}
static void parseRuleTokens(List<String> ruleTokens, Map<String, Rule> ruleNameAndRuleMap)
{
String ruleName = extractRuleName(ruleTokens);
String ruleType = extractRuleType(ruleTokens);
Rule rule = ruleTypeAndFuncMap.get(ruleType).apply(ruleTokens, ruleNameAndRuleMap);
ruleNameAndRuleMap.putIfAbsent(ruleName, rule);
}
ParserTest.java
public void test_parseTokens()
{
assertTrue(equal(spec(), parseTokens(SPEC_TOKENS)));
}
Parser.java
static Rule parseTokens(List<List<String>> tokens)
{
Map<String, Rule> ruleNameAndRules = new HashMap<>();
tokens.stream().forEach(ruleTokens -> parseRuleTokens(ruleTokens, ruleNameAndRules));
return ruleNameAndRules.get("spec");
}
ParserTest.java
public void test_parse()
{
List<String> ruleDescs = asList(
"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 ");
assertTrue(equal(spec(), parse(ruleDescs)));
}
Parser.java
static Rule parse(List<String> ruleDescs)
{
return parseTokens(tokenize(ruleDescs));
}
ruleDescs定义有重复,重构一下测试代码。
ParserTest.java
private static final List<String> SPEC_DESC = asList(
"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 ");
public void test_tokenize()
{
assertEquals(SPEC_TOKENS, tokenize(SPEC_DESC));
}
public void test_parse()
{
assertTrue(equal(spec(), parse(SPEC_DESC)));
}
涉及到读取真实文件,不用单元测试保证,用main()方法集成测试一下。
Parser.java
public static Rule parse(String progFileName)
{
Rule rule = null;
try
{
Path progFilePath = FileSystems.getDefault().getPath("scripts", progFileName);
rule = parse(Files.readAllLines(progFilePath));
}
catch(IOException e)
{
e.printStackTrace();
}
return rule;
}
本来打算写main()方法测试一下,发现无法直观地看到解析出来的Rule的结构。所以实现RuleDescTool类提供Rule的字符串描述,后面会看到这其实也是一个解释器。
RuleDescToolTest.java
package fayelab.ddd.fbwreloaded.complete.interpreter;
public class RuleDescToolTest extends TestCase
{
public void test_desc_predication()
{
assertEquals("times_3", descPredication(times(3)));
assertEquals("contains_3", descPredication(contains(3)));
assertEquals("always_true", descPredication(alwaysTrue()));
}
}
RuleDescTool.java
package fayelab.ddd.fbwreloaded.complete.interpreter;
public class RuleDescTool
{
private static Map<Predication.Type, Function<Predication, String>> predicationTypeAndFuncMap;
static
{
predicationTypeAndFuncMap = new HashMap<>();
predicationTypeAndFuncMap.put(Predication.Type.TIMES,
predication -> joinPredication("times", predication.getParam()));
predicationTypeAndFuncMap.put(Predication.Type.CONTAINS,
predication -> joinPredication("contains", predication.getParam()));
predicationTypeAndFuncMap.put(Predication.Type.ALWAYSTRUE,
predication -> "always_true");
}
static String descPredication(Predication predication)
{
return predicationTypeAndFuncMap.get(predication.getType()).apply(predication);
}
private static String joinPredication(String type, int param)
{
return String.join("_", type, String.valueOf(param));
}
}
RuleDescToolTest.java
public void test_desc_action()
{
assertEquals("to_fizz", descAction(toFizz()));
assertEquals("to_buzz", descAction(toBuzz()));
assertEquals("to_whizz", descAction(toWhizz()));
assertEquals("to_str", descAction(toStr()));
}
RuleDescTool.java
private static Map<Action.Type, Supplier<String>> actionTypeAndFuncMap;
static
{
actionTypeAndFuncMap = new HashMap<>();
actionTypeAndFuncMap.put(Action.Type.TOFIZZ, () -> "to_fizz");
actionTypeAndFuncMap.put(Action.Type.TOBUZZ, () -> "to_buzz");
actionTypeAndFuncMap.put(Action.Type.TOWHIZZ, () -> "to_whizz");
actionTypeAndFuncMap.put(Action.Type.TOSTR, () -> "to_str");
}
static String descAction(Action action)
{
return actionTypeAndFuncMap.get(action.getType()).get();
}
RuleDescToolTest.java
public void test_desc_atom()
{
assertEquals("{atom, times_3, to_fizz}", descAtom(new Atom(times(3), toFizz())));
assertEquals("{atom, times_5, to_buzz}", descAtom(new Atom(times(5), toBuzz())));
assertEquals("{atom, times_7, to_whizz}", descAtom(new Atom(times(7), toWhizz())));
assertEquals("{atom, contains_3, to_fizz}", descAtom(new Atom(contains(3), toFizz())));
assertEquals("{atom, always_true, to_str}", descAtom(new Atom(alwaysTrue(), toStr())));
}
RuleDescTool.java
static String descAtom(Atom atom)
{
return wrapWithBraces(joinRule("atom", descPredication(atom.getPredication()), descAction(atom.getAction())));
}
private static String joinRule(String...ruleElementDescs)
{
return String.join(", ", ruleElementDescs);
}
private static String wrapWithBraces(String str)
{
return String.format("{%s}", str);
}
涉及到递归调用,除了实现descOr(),还要实现desc()的atom和or分支。
RuleDescToolTest.java
public void test_desc_or()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Or or_57 = new Or(r1_5, r1_7);
assertEquals("{or, {atom, times_5, to_buzz}, {atom, times_7, to_whizz}}", descOr(or_57));
Or or_357 = new Or(r1_3, or(r1_5, r1_7));
assertEquals("{or, {atom, times_3, to_fizz}, "
+ "{or, {atom, times_5, to_buzz}, "
+ "{atom, times_7, to_whizz}}}",
descOr(or_357));
}
RuleDescTool.java
private static Map<Rule.Type, Function<Rule, String>> ruleTypeAndFuncMap;
static
{
ruleTypeAndFuncMap = new HashMap<>();
ruleTypeAndFuncMap.put(Rule.Type.ATOM, rule -> descAtom((Atom)rule.getData()));
ruleTypeAndFuncMap.put(Rule.Type.OR, rule -> descOr((Or)rule.getData()));
}
static String desc(Rule rule)
{
return ruleTypeAndFuncMap.get(rule.getType()).apply(rule);
}
static String descOr(Or or)
{
return wrapWithBraces(joinRule("or", desc(or.getRule1()), desc(or.getRule2())));
}
涉及到递归调用,除了实现descAnd(),还要实现desc()的and分支。
RuleDescToolTest.java
public void test_desc_and()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
And and_57 = new And(r1_5, r1_7);
assertEquals("{and, {atom, times_5, to_buzz}, {atom, times_7, to_whizz}}", descAnd(and_57));
And and_357 = new And(r1_3, and(r1_5, r1_7));
assertEquals("{and, {atom, times_3, to_fizz}, "
+ "{and, {atom, times_5, to_buzz}, "
+ "{atom, times_7, to_whizz}}}",
descAnd(and_357));
}
RuleDescTool.java
private static Map<Rule.Type, Function<Rule, String>> ruleTypeAndFuncMap;
static
{
ruleTypeAndFuncMap = new HashMap<>();
ruleTypeAndFuncMap.put(Rule.Type.ATOM, rule -> descAtom((Atom)rule.getData()));
ruleTypeAndFuncMap.put(Rule.Type.OR, rule -> descOr((Or)rule.getData()));
ruleTypeAndFuncMap.put(Rule.Type.AND, rule -> descAnd((And)rule.getData()));
}
static String descAnd(And and)
{
return wrapWithBraces(joinRule("and", desc(and.getRule1()), desc(and.getRule2())));
}
desc()其实已经完成了,这里用spec测试用例验证一下。
RuleDescToolTest.java
public void test_desc_spec()
{
Rule spec = spec();
assertEquals("{or, {atom, contains_3, to_fizz}, "
+ "{or, {or, {and, {atom, times_3, to_fizz}, "
+ "{and, {atom, times_5, to_buzz}, "
+ "{atom, times_7, to_whizz}}}, "
+ "{or, {and, {atom, times_3, to_fizz}, "
+ "{atom, times_5, to_buzz}}, "
+ "{or, {and, {atom, times_3, to_fizz}, "
+ "{atom, times_7, to_whizz}}, "
+ "{and, {atom, times_5, to_buzz}, "
+ "{atom, times_7, to_whizz}}}}}, "
+ "{or, {or, {atom, times_3, to_fizz}, "
+ "{or, {atom, times_5, to_buzz}, "
+ "{atom, times_7, to_whizz}}}, "
+ "{atom, always_true, to_str}}}}",
desc(spec));
}
涉及到读取真实程序文件,不用单元测试保证,用main()方法集成测试一下。
Parser.java
public static Rule parse(String progFileName)
{
Rule rule = null;
try
{
Path progFilePath = FileSystems.getDefault().getPath("scripts", progFileName);
rule = parse(Files.readAllLines(progFilePath));
}
catch(IOException e)
{
e.printStackTrace();
}
return rule;
}
public static void main(String[] args)
{
String expected = "{or, {atom, contains_3, to_fizz}, "
+ "{or, {or, {and, {atom, times_3, to_fizz}, "
+ "{and, {atom, times_5, to_buzz}, "
+ "{atom, times_7, to_whizz}}}, "
+ "{or, {and, {atom, times_3, to_fizz}, "
+ "{atom, times_5, to_buzz}}, "
+ "{or, {and, {atom, times_3, to_fizz}, "
+ "{atom, times_7, to_whizz}}, "
+ "{and, {atom, times_5, to_buzz}, "
+ "{atom, times_7, to_whizz}}}}}, "
+ "{or, {or, {atom, times_3, to_fizz}, "
+ "{or, {atom, times_5, to_buzz}, "
+ "{atom, times_7, to_whizz}}}, "
+ "{atom, always_true, to_str}}}}";
String actual = RuleDescTool.desc(parse("prog_1.fbw"));
System.out.println(expected.equals(actual));
System.out.println(actual);
}
运行结果如下:
true
{or, {atom, contains_3, to_fizz}, {or, {or, {and, {atom, times_3, to_fizz}, {and, {atom, times_5, to_buzz}, {atom, times_7, to_whizz}}}, {or, {and, {atom, times_3, to_fizz}, {atom, times_5, to_buzz}}, {or, {and, {atom, times_3, to_fizz}, {atom, times_7, to_whizz}}, {and, {atom, times_5, to_buzz}, {atom, times_7, to_whizz}}}}}, {or, {or, {atom, times_3, to_fizz}, {or, {atom, times_5, to_buzz}, {atom, times_7, to_whizz}}}, {atom, always_true, to_str}}}}
package fayelab.ddd.fbwreloaded.complete.interpreter;
public class AllTests
{
public static Test suite()
{
TestSuite suite = new TestSuite(AllTests.class.getName());
//$JUnit-BEGIN$
suite.addTestSuite(InterpreterTest.class);
suite.addTestSuite(ParserTest.class);
suite.addTestSuite(RuleDescToolTest.class);
suite.addTestSuite(RuleEqualityToolTest.class);
//$JUnit-END$
return suite;
}
}
解释器用的就是之前实现的Interpreter类,没有变化。
为了支持读取程序文件,FizzBuzzWhizz类需要修改一下。
FizzBuzzWhizz.java
package fayelab.ddd.fbwreloaded.complete.interpreter;
public class FizzBuzzWhizz
{
public void run()
{
output(runSpec(spec()));
}
public void run(String progFileName)
{
output(runSpec(Parser.parse(progFileName)));
}
private String runSpec(Rule spec)
{
return IntStream.rangeClosed(1, 100)
.mapToObj(n -> asList(String.valueOf(n), interpret(spec, n).get()))
.map(pair -> String.join(" -> ", pair.get(0), pair.get(1)))
.collect(Collectors.joining("\n"));
}
private void output(String result)
{
System.out.println(result);
}
public static void main(String[] args)
{
new FizzBuzzWhizz().run("prog_1.fbw");
}
}
当prog_1.fbw内容如下时:
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
FizzBuzzWhizz运行结果如下图所示:
稍微修改一下prog_1.fbw程序,将r3由“contains 3 to_fizz”改为“contains 5 to_buzz”:
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 5 to_buzz
rd atom always_true to_str
spec or r3 r2 r1 rd
FizzBuzzWhizz运行结果如下图所示:
下面来编写一个从Program到Parser到Compiler的完整程序。
出于练习演示的目的,写了一个编译器,将AST核心数据编译成了函数式实现,以体现出编译器的作用———保持语义的等价语法变换。
函数式实现属于一种Embedded DSL,即利用Java 8语言新支持的函数式特性构造了和AST一一对应的函数式实现模型。
当然也可以将AST核心数据编译成OO实现。OO实现也属于Embedded DSL,即利用Java语言的OO特性构造了和AST一一对应的面向对象实现模型。
在函数式实现和OO实现中均包含了语义接口、语义的语法表现和语义实现。
一般不会实现这种编译器,一般会编译成更底层的语言,并会做一些语义上的优化。
代码包路径:
函数式实现:fayelab.ddd.fbwreloaded.complete.compiler.functional
OO式实现:fayelab.ddd.fbwreloaded.complete.compiler.oo
跟之前的prog_1.fbw一样,没有变化。
解析器用的就是之前实现的Parser类,没有变化。
CompilerTest.java
package fayelab.ddd.fbwreloaded.complete.compiler.functional;
public class CompilerTest extends TestCase
{
public void test_compile_predication()
{
assertTrue(compilePredication(times(3)).test(3));
assertFalse(compilePredication(times(3)).test(4));
assertTrue(compilePredication(contains(3)).test(3));
assertFalse(compilePredication(contains(3)).test(4));
assertTrue(compilePredication(alwaysTrue()).test(-1));
}
}
Compiler.java
package fayelab.ddd.fbwreloaded.complete.compiler.functional;
public class Compiler
{
private static Map<Predication.Type, Function<Integer, Predicate<Integer>>> predicationTypeAndFuncMap;
static
{
predicationTypeAndFuncMap = new HashMap<>();
predicationTypeAndFuncMap.put(Predication.Type.TIMES, Compiler::times);
predicationTypeAndFuncMap.put(Predication.Type.CONTAINS, Compiler::contains);
predicationTypeAndFuncMap.put(Predication.Type.ALWAYSTRUE, any -> alwaysTrue());
}
static Predicate<Integer> compilePredication(Predication predication)
{
return predicationTypeAndFuncMap.get(predication.getType()).apply(predication.getParam());
}
private static Predicate<Integer> times(int base)
{
return n -> n % base == 0;
}
private static Predicate<Integer> contains(int digit)
{
return n -> {
int p1 = n % 10;
int p2 = (n / 10) % 10;
int p3 = (n / 10 / 10) % 10;
return p1 == digit || p2 == digit || p3 == digit;
};
}
private static Predicate<Integer> alwaysTrue()
{
return n -> true;
}
}
CompilerTest.java
public void test_compile_action()
{
assertEquals("Fizz", compileAction(toFizz()).apply(3));
assertEquals("Buzz", compileAction(toBuzz()).apply(5));
assertEquals("Whizz", compileAction(toWhizz()).apply(7));
assertEquals("1", compileAction(toStr()).apply(1));
}
Compiler.java
private static Map<Action.Type, Supplier<Function<Integer, String>>> actionTypeAndFuncMap;
static
{
actionTypeAndFuncMap = new HashMap<>();
actionTypeAndFuncMap.put(Action.Type.TOFIZZ, Compiler::toFizz);
actionTypeAndFuncMap.put(Action.Type.TOBUZZ, Compiler::toBuzz);
actionTypeAndFuncMap.put(Action.Type.TOWHIZZ, Compiler::toWhizz);
actionTypeAndFuncMap.put(Action.Type.TOSTR, Compiler::toStr);
}
static Function<Integer, String> compileAction(Action action)
{
return actionTypeAndFuncMap.get(action.getType()).get();
}
CompilerTest.java
public void test_compile_atom()
{
Rule r1_3 = atom(times(3), toFizz());
Function<Integer, Optional<String>> c_r1_3 = compileAtom((Atom)r1_3.getData());
assertEquals("Fizz", c_r1_3.apply(6).get());
assertEquals(Optional.empty(), c_r1_3.apply(7));
Rule r1_5 = atom(times(5), toBuzz());
Function<Integer, Optional<String>> c_r1_5 = compileAtom((Atom)r1_5.getData());
assertEquals("Buzz", c_r1_5.apply(10).get());
assertEquals(Optional.empty(), c_r1_5.apply(11));
Rule r1_7 = atom(times(7), toWhizz());
Function<Integer, Optional<String>> c_r1_7 = compileAtom((Atom)r1_7.getData());
assertEquals("Whizz", c_r1_7.apply(14).get());
assertEquals(Optional.empty(), c_r1_7.apply(13));
Rule r3 = atom(contains(3), toFizz());
Function<Integer, Optional<String>> c_r3 = compileAtom((Atom)r3.getData());
assertEquals("Fizz", c_r3.apply(3).get());
assertEquals("Fizz", c_r3.apply(13).get());
assertEquals("Fizz", c_r3.apply(31).get());
assertEquals(Optional.empty(), c_r3.apply(24));
Rule rd = atom(alwaysTrue(), toStr());
Function<Integer, Optional<String>> c_rd = compileAtom((Atom)rd.getData());
assertEquals("6", c_rd.apply(6).get());
}
Compiler.java
static Function<Integer, Optional<String>> compileAtom(Atom atom)
{
return n -> {
Predicate<Integer> predication = compilePredication(atom.getPredication());
Function<Integer, String> action = compileAction(atom.getAction());
if(predication.test(n))
{
return Optional.of(action.apply(n));
}
return Optional.empty();
};
}
涉及到递归调用,除了实现compileOr(),还要实现compile()的atom和or分支。
CompilerTest.java
public void test_compile_or()
{
Rule r1_3 = atom(times(3), toFizz());
Rule r1_5 = atom(times(5), toBuzz());
Rule r1_7 = atom(times(7), toWhizz());
Rule or_35 = or(r1_3, r1_5);
Function<Integer, Optional<String>> c_or_35 = compileOr((Or)or_35.getData());
assertEquals("Fizz", c_or_35.apply(6).get());
assertEquals("Buzz", c_or_35.apply(10).get());
assertEquals("Fizz", c_or_35.apply(15).get());
assertEquals(Optional.empty(), c_or_35.apply(7));
Rule or_357 = or(r1_3, r1_5, r1_7);
Function<Integer, Optional<String>> c_or_357 = compileOr((Or)or_357.getData());
assertEquals("Fizz", c_or_357.apply(6).get());
assertEquals("Buzz", c_or_357.apply(10).get());
assertEquals("Whizz", c_or_357.apply(14).get());
assertEquals(Optional.empty(), c_or_357.apply(13));
}
Compiler.java
private static Map<Rule.Type, Function<Rule, Function<Integer, Optional<String>>>> ruleTypeAndFuncMap;
static
{
ruleTypeAndFuncMap = new HashMap<>();
ruleTypeAndFuncMap.put(Rule.Type.ATOM, rule -> compileAtom((Atom)rule.getData()));
ruleTypeAndFuncMap.put(Rule.Type.OR, rule -> compileOr((Or)rule.getData()));
}
public static Function<Integer, Optional<String>> compile(Rule rule)
{
return ruleTypeAndFuncMap.get(rule.getType()).apply(rule);
}
static Function<Integer, Optional<String>> compileOr(Or or)
{
return n -> {
Optional<String> result1 = compile(or.getRule1()).apply(n);
if(result1.isPresent())
{
return result1;
}
return compile(or.getRule2()).apply(n);
};
}
测试代码有重复,重构一下。
CompilerTest.java
private Rule r1_3;
private Rule r1_5;
private Rule r1_7;
private Rule r3;
private Rule rd;
@Override
protected void setUp()
{
r1_3 = atom(times(3), toFizz());
r1_5 = atom(times(5), toBuzz());
r1_7 = atom(times(7), toWhizz());
r3 = atom(contains(3), toFizz());
rd = atom(alwaysTrue(), toStr());
}
public void test_compile_atom()
{
Function<Integer, Optional<String>> c_r1_3 = compileAtom((Atom)r1_3.getData());
assertEquals("Fizz", c_r1_3.apply(6).get());
assertEquals(Optional.empty(), c_r1_3.apply(7));
Function<Integer, Optional<String>> c_r1_5 = compileAtom((Atom)r1_5.getData());
assertEquals("Buzz", c_r1_5.apply(10).get());
assertEquals(Optional.empty(), c_r1_5.apply(11));
Function<Integer, Optional<String>> c_r1_7 = compileAtom((Atom)r1_7.getData());
assertEquals("Whizz", c_r1_7.apply(14).get());
assertEquals(Optional.empty(), c_r1_7.apply(13));
Function<Integer, Optional<String>> c_r3 = compileAtom((Atom)r3.getData());
assertEquals("Fizz", c_r3.apply(3).get());
assertEquals("Fizz", c_r3.apply(13).get());
assertEquals("Fizz", c_r3.apply(31).get());
assertEquals(Optional.empty(), c_r3.apply(24));
Function<Integer, Optional<String>> c_rd = compileAtom((Atom)rd.getData());
assertEquals("6", c_rd.apply(6).get());
}
public void test_compile_or()
{
Rule or_35 = or(r1_3, r1_5);
Function<Integer, Optional<String>> c_or_35 = compileOr((Or)or_35.getData());
assertEquals("Fizz", c_or_35.apply(6).get());
assertEquals("Buzz", c_or_35.apply(10).get());
assertEquals(Optional.empty(), c_or_35.apply(7));
Rule or_357 = or(r1_3, r1_5, r1_7);
Function<Integer, Optional<String>> c_or_357 = compileOr((Or)or_357.getData());
assertEquals("Fizz", c_or_357.apply(6).get());
assertEquals("Buzz", c_or_357.apply(10).get());
assertEquals("Whizz", c_or_357.apply(14).get());
assertEquals(Optional.empty(), c_or_357.apply(13));
}
涉及到递归调用,除了实现compileAnd(),还要实现compile()的and分支。
CompilerTest.java
public void test_compile_and()
{
Rule and_35 = and(r1_3, r1_5);
Function<Integer, Optional<String>> c_and_35 = compileAnd((And)and_35.getData());
assertEquals(Optional.empty(), c_and_35.apply(3));
assertEquals(Optional.empty(), c_and_35.apply(5));
assertEquals("FizzBuzz", c_and_35.apply(15).get());
assertEquals(Optional.empty(), c_and_35.apply(16));
Rule and_357 = and(r1_3, r1_5, r1_7);
Function<Integer, Optional<String>> c_and_357 = compileAnd((And)and_357.getData());
assertEquals("FizzBuzzWhizz", c_and_357.apply(3*5*7).get());
assertEquals(Optional.empty(), c_and_357.apply(104));
}
Compiler.java
private static Map<Rule.Type, Function<Rule, Function<Integer, Optional<String>>>> ruleTypeAndFuncMap;
static
{
ruleTypeAndFuncMap = new HashMap<>();
ruleTypeAndFuncMap.put(Rule.Type.ATOM, rule -> compileAtom((Atom)rule.getData()));
ruleTypeAndFuncMap.put(Rule.Type.OR, rule -> compileOr((Or)rule.getData()));
ruleTypeAndFuncMap.put(Rule.Type.AND, rule -> compileAnd((And)rule.getData()));
}
static Function<Integer, Optional<String>> compileAnd(And and)
{
return n -> {
Optional<String> result1 = compile(and.getRule1()).apply(n);
if(!result1.isPresent())
{
return Optional.empty();
}
Optional<String> result2 = compile(and.getRule2()).apply(n);
if(!result2.isPresent())
{
return Optional.empty();
}
return Optional.of(result1.get() + result2.get());
};
}
compile()其实已经完成了,这里用Specification测试用例验证一下。
CompilerTest.java
public void test_compile_spec()
{
Rule spec = spec();
Function<Integer, Optional<String>> c_spec = compile(spec);
assertEquals("Fizz", c_spec.apply(35).get());
assertEquals("FizzWhizz", c_spec.apply(21).get());
assertEquals("BuzzWhizz", c_spec.apply(70).get());
assertEquals("Fizz", c_spec.apply(9).get());
assertEquals("1", c_spec.apply(1).get());
}
AllTests.java
package fayelab.ddd.fbwreloaded.complete.compiler.functional;
public class AllTests
{
public static Test suite()
{
TestSuite suite = new TestSuite(AllTests.class.getName());
//$JUnit-BEGIN$
suite.addTestSuite(CompilerTest.class);
suite.addTestSuite(ParserTest.class);
suite.addTestSuite(RuleDescToolTest.class);
suite.addTestSuite(RuleEqualityToolTest.class);
//$JUnit-END$
return suite;
}
}
因为要先编译,对比之前Interpreter的FizzBuzzWhizz,Compiler的FizzBuzzWhizz稍有不同。
FizzBuzzWhizz.java
package fayelab.ddd.fbwreloaded.complete.compiler.functional;
public class FizzBuzzWhizz
{
public void run()
{
output(runSpec(spec()));
}
public void run(String progFileName)
{
output(runSpec(Parser.parse(progFileName)));
}
private String runSpec(Rule spec)
{
return IntStream.rangeClosed(1, 100)
.mapToObj(n -> asList(String.valueOf(n), compile(spec).apply(n).get()))
.map(pair -> String.join(" -> ", pair.get(0), pair.get(1)))
.collect(Collectors.joining("\n"));
}
private void output(String result)
{
System.out.println(result);
}
public static void main(String[] args)
{
new FizzBuzzWhizz().run("prog_1.fbw");
}
}
测试过程和结果同Interpreter。
由于编译前后的Predication、Action、Atom、Or和And等类虽然包名不同但类名相同,所以将编译后的Predication、Action、Atom、Or和And等类的类名都加上OO的前缀,并放到fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled包路径下。
用Optional替换之前的Result类,还有其他一些小改造。
另外,这一层不需要SpecTool。
predication.AllTests.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.predication;
public class AllTests
{
public static Test suite()
{
TestSuite suite = new TestSuite(AllTests.class.getName());
//$JUnit-BEGIN$
suite.addTestSuite(OOTimesTest.class);
suite.addTestSuite(OOContainsTest.class);
suite.addTestSuite(OOAlwaysTrueTest.class);
//$JUnit-END$
return suite;
}
}
OOPredication.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.predication;
public interface OOPredication
{
boolean predicate(int n);
}
OOTimesTest.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.predication;
public class OOTimesTest extends TestCase
{
public void test_times_3()
{
OOPredication times3 = new OOTimes(3);
assertTrue(times3.predicate(6));
assertFalse(times3.predicate(5));
}
public void test_times_5()
{
OOPredication times5 = new OOTimes(5);
assertTrue(times5.predicate(10));
assertFalse(times5.predicate(11));
}
public void test_times_7()
{
OOPredication times7 = new OOTimes(7);
assertTrue(times7.predicate(14));
assertFalse(times7.predicate(15));
}
}
OOTimes.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.predication;
public class OOTimes implements OOPredication
{
private int base;
public OOTimes(int base)
{
this.base = base;
}
@Override
public boolean predicate(int n)
{
return n % base == 0;
}
}
OOContainsTest.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.predication;
public class OOContainsTest extends TestCase
{
public void test_contains()
{
OOPredication contains3 = new OOContains(3);
assertTrue(contains3.predicate(13));
assertTrue(contains3.predicate(35));
assertTrue(contains3.predicate(300));
assertFalse(contains3.predicate(24));
}
}
OOContains.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.predication;
public class OOContains implements OOPredication
{
private int digit;
public OOContains(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;
}
}
OOAlwaysTrueTest.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.predication;
public class OOAlwaysTrueTest extends TestCase
{
public void test_alwaysTrue()
{
OOPredication alwaysTrue = new OOAlwaysTrue();
assertTrue(alwaysTrue.predicate(1));
assertTrue(alwaysTrue.predicate(3));
assertTrue(alwaysTrue.predicate(5));
}
}
OOAlwaysTrue.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.predication;
public class OOAlwaysTrue implements OOPredication
{
@Override
public boolean predicate(int n)
{
return true;
}
}
action.AllTests.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.action;
public class AllOOActionTests
{
public static Test suite()
{
TestSuite suite = new TestSuite(AllTests.class.getName());
//$JUnit-BEGIN$
suite.addTestSuite(OOToFizzTest.class);
suite.addTestSuite(OOToBuzzTest.class);
suite.addTestSuite(OOToWhizzTest.class);
suite.addTestSuite(OOToStrTest.class);
//$JUnit-END$
return suite;
}
}
OOAction.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.action;
public interface OOAction
{
String act(int n);
}
OOToFizzTest.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.action;
public class OOToFizzTest extends TestCase
{
public void test_toFizz()
{
OOAction toFizz = new OOToFizz();
assertEquals("Fizz", toFizz.act(3));
}
}
OOToFizz.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.action;
public class OOToFizz implements OOAction
{
@Override
public String act(int n)
{
return "Fizz";
}
}
OOToBuzzTest.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.action;
public class OOToBuzzTest extends TestCase
{
public void test_toBuzz()
{
OOAction toBuzz = new OOToBuzz();
assertEquals("Buzz", toBuzz.act(5));
}
}
OOToBuzz.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.action;
public class OOToBuzz implements OOAction
{
@Override
public String act(int n)
{
return "Buzz";
}
}
OOToWhizzTest.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.action;
public class OOToWhizzTest extends TestCase
{
public void test_toWhizz()
{
OOAction toWhizz = new OOToWhizz();
assertEquals("Whizz", toWhizz.act(7));
}
}
OOToWhizz.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.action;
public class OOToWhizz implements OOAction
{
@Override
public String act(int n)
{
return "Whizz";
}
}
OOToStrTest.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.action;
public class OOToStrTest extends TestCase
{
public void test_toStr()
{
OOAction toStr = new OOToStr();
assertEquals("1", toStr.act(1));
assertEquals("10", toStr.act(10));
}
}
OOToStr.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.action;
public class OOToStr implements OOAction
{
@Override
public String act(int n)
{
return String.valueOf(n);
}
}
rule.AllTests.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.rule;
public class AllTests
{
public static Test suite()
{
TestSuite suite = new TestSuite(AllTests.class.getName());
//$JUnit-BEGIN$
suite.addTestSuite(OORuleTest.class);
//$JUnit-END$
return suite;
}
}
OORuleTest.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.rule;
public class OORuleTest extends TestCase
{
public void test_atom_rule_1_3()
{
OORule r1_3 = new OOAtom(new OOTimes(3), new OOToFizz());
assertEquals(Optional.of("Fizz"), r1_3.apply(3));
assertEquals(Optional.empty(), r1_3.apply(4));
}
public void test_atom_rule_1_5()
{
OORule r1_5 = new OOAtom(new OOTimes(5), new OOToBuzz());
assertEquals(Optional.of("Buzz"), r1_5.apply(10));
assertEquals(Optional.empty(), r1_5.apply(11));
}
public void test_atom_rule_1_7()
{
OORule r1_7 = new OOAtom(new OOTimes(7), new OOToWhizz());
assertEquals(Optional.of("Whizz"), r1_7.apply(14));
assertEquals(Optional.empty(), r1_7.apply(13));
}
public void test_or_rule()
{
OORule r1_3 = new OOAtom(new OOTimes(3), new OOToFizz());
OORule r1_5 = new OOAtom(new OOTimes(5), new OOToBuzz());
OORule or_35 = new OOOr(r1_3, r1_5);
assertEquals(Optional.of("Fizz"), or_35.apply(6));
assertEquals(Optional.of("Buzz"), or_35.apply(10));
assertEquals(Optional.of("Fizz"), or_35.apply(15));
assertEquals(Optional.empty(), or_35.apply(7));
}
public void test_rule_1()
{
OORule r1_3 = new OOAtom(new OOTimes(3), new OOToFizz());
OORule r1_5 = new OOAtom(new OOTimes(5), new OOToBuzz());
OORule r1_7 = new OOAtom(new OOTimes(7), new OOToWhizz());
OORule r1 = new OOOr(r1_3, new OOOr(r1_5, r1_7));
assertEquals(Optional.of("Fizz"), r1.apply(6));
assertEquals(Optional.of("Buzz"), r1.apply(10));
assertEquals(Optional.of("Whizz"), r1.apply(14));
assertEquals(Optional.empty(), r1.apply(13));
}
public void test_and_rule()
{
OORule r1_3 = new OOAtom(new OOTimes(3), new OOToFizz());
OORule r1_5 = new OOAtom(new OOTimes(5), new OOToBuzz());
OORule and_35 = new OOAnd(r1_3, r1_5);
assertEquals(Optional.empty(), and_35.apply(3));
assertEquals(Optional.empty(), and_35.apply(5));
assertEquals(Optional.of("FizzBuzz"), and_35.apply(15));
assertEquals(Optional.empty(), and_35.apply(16));
}
public void test_rule_2()
{
OORule r1_3 = new OOAtom(new OOTimes(3), new OOToFizz());
OORule r1_5 = new OOAtom(new OOTimes(5), new OOToBuzz());
OORule r1_7 = new OOAtom(new OOTimes(7), new OOToWhizz());
OORule r2 = new OOOr(new OOAnd(r1_3, new OOAnd(r1_5, r1_7)),
new OOOr(new OOAnd(r1_3, r1_5),
new OOOr(new OOAnd(r1_3, r1_7),
new OOAnd(r1_5, r1_7))));
assertEquals(Optional.empty(), r2.apply(3));
assertEquals(Optional.empty(), r2.apply(5));
assertEquals(Optional.empty(), r2.apply(7));
assertEquals(Optional.of("FizzBuzzWhizz"), r2.apply(3*5*7));
assertEquals(Optional.empty(), r2.apply(104));
assertEquals(Optional.of("FizzBuzz"), r2.apply(15));
assertEquals(Optional.empty(), r2.apply(14));
assertEquals(Optional.of("FizzWhizz"), r2.apply(21));
assertEquals(Optional.empty(), r2.apply(22));
assertEquals(Optional.of("BuzzWhizz"), r2.apply(35));
assertEquals(Optional.empty(), r2.apply(34));
}
public void test_rule_3()
{
OORule r3 = new OOAtom(new OOContains(3), new OOToFizz());
assertEquals(Optional.of("Fizz"), r3.apply(3));
assertEquals(Optional.of("Fizz"), r3.apply(13));
assertEquals(Optional.of("Fizz"), r3.apply(31));
assertEquals(Optional.empty(), r3.apply(24));
}
public void test_default_rule()
{
OORule rd = new OOAtom(new OOAlwaysTrue(), new OOToStr());
assertEquals(Optional.of("1"), rd.apply(1));
assertEquals(Optional.of("3"), rd.apply(3));
}
}
OORule.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.rule;
public interface OORule
{
Optional<String> apply(int n);
}
OOAtom.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.rule;
public class OOAtom implements OORule
{
private OOPredication predication;
private OOAction action;
public OOAtom(OOPredication predication, OOAction action)
{
this.predication = predication;
this.action = action;
}
@Override
public Optional<String> apply(int n)
{
if(predication.predicate(n))
{
return Optional.of(action.act(n));
}
return Optional.empty();
}
}
OOOr.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.rule;
public class OOOr implements OORule
{
private OORule rule1;
private OORule rule2;
public OOOr(OORule rule1, OORule rule2)
{
this.rule1 = rule1;
this.rule2 = rule2;
}
@Override
public Optional<String> apply(int n)
{
Optional<String> result1 = rule1.apply(n);
if(result1.isPresent())
{
return result1;
}
return rule2.apply(n);
}
}
OOAnd.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.rule;
public class OOAnd implements OORule
{
private OORule rule1;
private OORule rule2;
public OOAnd(OORule rule1, OORule rule2)
{
this.rule1 = rule1;
this.rule2 = rule2;
}
@Override
public Optional<String> apply(int n)
{
Optional<String> result1 = rule1.apply(n);
if(!result1.isPresent())
{
return Optional.empty();
}
Optional<String> result2 = rule2.apply(n);
if(!result2.isPresent())
{
return Optional.empty();
}
return Optional.of(result1.get() + result2.get());
}
}
跟之前的prog_1.fbw一样,没有变化。
解析器用的就是之前实现的Parser类,没有变化。
CompilerTest.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo;
public class CompilerTest extends TestCase
{
public void test_compile_predication()
{
assertTrue(compilePredication(times(3)).predicate(3));
assertFalse(compilePredication(times(3)).predicate(4));
assertTrue(compilePredication(contains(3)).predicate(3));
assertFalse(compilePredication(contains(3)).predicate(4));
assertTrue(compilePredication(alwaysTrue()).predicate(-1));
}
}
Compiler.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo;
public class Compiler
{
private static Map<Predication.Type, Function<Integer, OOPredication>> predicationTypeAndFuncMap;
static
{
predicationTypeAndFuncMap = new HashMap<>();
predicationTypeAndFuncMap.put(Predication.Type.TIMES, Compiler::times);
predicationTypeAndFuncMap.put(Predication.Type.CONTAINS, Compiler::contains);
predicationTypeAndFuncMap.put(Predication.Type.ALWAYSTRUE, any -> alwaysTrue());
}
static OOPredication compilePredication(Predication predication)
{
return predicationTypeAndFuncMap.get(predication.getType()).apply(predication.getParam());
}
private static OOPredication times(int base)
{
return new OOTimes(base);
}
private static OOPredication contains(int digit)
{
return new OOContains(digit);
}
private static OOPredication alwaysTrue()
{
return new OOAlwaysTrue();
}
}
CompilerTest.java
public void test_compile_action()
{
assertEquals("Fizz", compileAction(toFizz()).act(3));
assertEquals("Buzz", compileAction(toBuzz()).act(5));
assertEquals("Whizz", compileAction(toWhizz()).act(7));
assertEquals("1", compileAction(toStr()).act(1));
}
Compiler.java
private static Map<Action.Type, Supplier<OOAction>> actionTypeAndFuncMap;
static
{
actionTypeAndFuncMap = new HashMap<>();
actionTypeAndFuncMap.put(Action.Type.TOFIZZ, Compiler::toFizz);
actionTypeAndFuncMap.put(Action.Type.TOBUZZ, Compiler::toBuzz);
actionTypeAndFuncMap.put(Action.Type.TOWHIZZ, Compiler::toWhizz);
actionTypeAndFuncMap.put(Action.Type.TOSTR, Compiler::toStr);
}
static OOAction compileAction(Action action)
{
return actionTypeAndFuncMap.get(action.getType()).get();
}
CompilerTest.java
private Rule r1_3;
private Rule r1_5;
private Rule r1_7;
private Rule r3;
private Rule rd;
@Override
protected void setUp()
{
r1_3 = atom(times(3), toFizz());
r1_5 = atom(times(5), toBuzz());
r1_7 = atom(times(7), toWhizz());
r3 = atom(contains(3), toFizz());
rd = atom(alwaysTrue(), toStr());
}
public void test_compile_atom()
{
OOAtom o_r1_3 = compileAtom((Atom)r1_3.getData());
assertEquals("Fizz", o_r1_3.apply(6).get());
assertEquals(Optional.empty(), o_r1_3.apply(7));
OOAtom o_r1_5 = compileAtom((Atom)r1_5.getData());
assertEquals("Buzz", o_r1_5.apply(10).get());
assertEquals(Optional.empty(), o_r1_5.apply(11));
OOAtom o_r1_7 = compileAtom((Atom)r1_7.getData());
assertEquals("Whizz", o_r1_7.apply(14).get());
assertEquals(Optional.empty(), o_r1_7.apply(13));
OOAtom o_r3 = compileAtom((Atom)r3.getData());
assertEquals("Fizz", o_r3.apply(3).get());
assertEquals("Fizz", o_r3.apply(13).get());
assertEquals("Fizz", o_r3.apply(31).get());
assertEquals(Optional.empty(), o_r3.apply(24));
OOAtom o_rd = compileAtom((Atom)rd.getData());
assertEquals("6", o_rd.apply(6).get());
}
Compiler.java
static OOAtom compileAtom(Atom atom)
{
return new OOAtom(compilePredication(atom.getPredication()), compileAction(atom.getAction()));
}
涉及到递归调用,除了实现compileOr(),还要实现compile()的atom和or分支。
CompilerTest.java
public void test_compile_or()
{
Rule or_35 = or(r1_3, r1_5);
OORule o_or_35 = compileOr((Or)or_35.getData());
assertEquals("Fizz", o_or_35.apply(6).get());
assertEquals("Buzz", o_or_35.apply(10).get());
assertEquals("Fizz", o_or_35.apply(15).get());
assertEquals(Optional.empty(), o_or_35.apply(7));
Rule or_357 = or(r1_3, r1_5, r1_7);
OORule o_or_357 = compileOr((Or)or_357.getData());
assertEquals("Fizz", o_or_357.apply(6).get());
assertEquals("Buzz", o_or_357.apply(10).get());
assertEquals("Whizz", o_or_357.apply(14).get());
assertEquals(Optional.empty(), o_or_357.apply(13));
}
Compiler.java
private static Map<Rule.Type, Function<Rule, OORule>> ruleTypeAndFuncMap;
static
{
ruleTypeAndFuncMap = new HashMap<>();
ruleTypeAndFuncMap.put(Rule.Type.ATOM, rule -> compileAtom((Atom)rule.getData()));
ruleTypeAndFuncMap.put(Rule.Type.OR, rule -> compileOr((Or)rule.getData()));
}
public static OORule compile(Rule rule)
{
return ruleTypeAndFuncMap.get(rule.getType()).apply(rule);
}
static OOOr compileOr(Or or)
{
return new OOOr(compile(or.getRule1()), compile(or.getRule2()));
}
涉及到递归调用,除了实现compileAnd(),还要实现compile()的and分支。
CompilerTest.java
public void test_compile_and()
{
Rule and_35 = and(r1_3, r1_5);
OORule o_and_35 = compileAnd((And)and_35.getData());
assertEquals(Optional.empty(), o_and_35.apply(3));
assertEquals(Optional.empty(), o_and_35.apply(5));
assertEquals("FizzBuzz", o_and_35.apply(15).get());
assertEquals(Optional.empty(), o_and_35.apply(16));
Rule and_357 = and(r1_3, r1_5, r1_7);
OORule o_and_357 = compileAnd((And)and_357.getData());
assertEquals("FizzBuzzWhizz", o_and_357.apply(3*5*7).get());
assertEquals(Optional.empty(), o_and_357.apply(104));
}
Compiler.java
private static Map<Rule.Type, Function<Rule, OORule>> ruleTypeAndFuncMap;
static
{
ruleTypeAndFuncMap = new HashMap<>();
ruleTypeAndFuncMap.put(Rule.Type.ATOM, rule -> compileAtom((Atom)rule.getData()));
ruleTypeAndFuncMap.put(Rule.Type.OR, rule -> compileOr((Or)rule.getData()));
ruleTypeAndFuncMap.put(Rule.Type.AND, rule -> compileAnd((And)rule.getData()));
}
static OORule compileAnd(And and)
{
return new OOAnd(compile(and.getRule1()), compile(and.getRule2()));
}
compile()其实已经完成了,这里用spec测试用例验证一下。
CompilerTest.java
public void test_compile_spec()
{
Rule spec = spec();
OORule o_spec = compile(spec);
assertEquals("Fizz", o_spec.apply(35).get());
assertEquals("FizzWhizz", o_spec.apply(21).get());
assertEquals("BuzzWhizz", o_spec.apply(70).get());
assertEquals("Fizz", o_spec.apply(9).get());
assertEquals("1", o_spec.apply(1).get());
}
AllTests.java
package fayelab.ddd.fbwreloaded.complete.compiler.oo;
public class AllTests
{
public static Test suite()
{
TestSuite suite = new TestSuite(AllTests.class.getName());
//$JUnit-BEGIN$
suite.addTest(fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled.AllTests.suite());
suite.addTestSuite(CompilerTest.class);
suite.addTestSuite(ParserTest.class);
suite.addTestSuite(RuleDescToolTest.class);
suite.addTestSuite(RuleEqualityToolTest.class);
//$JUnit-END$
return suite;
}
}
跟Compiler Functional的FizzBuzzWhizz一样。
测试过程和结果同Interpreter和Compiler Functional。
- 对问题领域进行深入分析,发现问题领域的核心需求;
- 通过核心需求驱动出计算模型和语义;
- 再围绕这个计算模型提供一套语言,给外面的人使用这个计算模型提供一个接口,这个接口可以是API、可以是数据表达、也可以是语言;
- 最终要实现这个计算模型,实现的方法有解释器和编译器两种。
这就是DDD!这才是DDD!
这就是DSL!这才是DSL!
“编程”不过是在某个计算模型上用某种语言去表达计算。 | 用DSL编程不过是在问题领域的计算模型上用DSL来表达计算。 |
计算模型是相应领域中的“通用机器”。 | 问题领域的计算模型是问题领域的通用机器。 |
编程语言不过是描述计算机器的一种方法。 | DSL描述的是DSL语言的计算机器。 |
程序是对特定机器的描述,这个特定机器可以被通用机器仿真。 | 用DSL程序实现了问题领域中的某个功能,这个DSL程序就是对这个功能(特定机器)的描述。 |
- 从问题领域导出核心需求,得到领域的计算模型(通用计算机器),在上面可以包装一个DSL语言或者数据或者API,基于这些开发程序和应用。
- 领域的计算模型和通用语言的计算模型之间存在鸿沟,可以用解释器或者编译器来填补。
- 解释器和编译器听起来很复杂,其实思想很简单,而且我们没有必要实现一个工业级别的、非常全面的解释器和编译器,只要借鉴这个思想实现我们的计算模型就够了。
Common Languages(Java/C) UML对应Java通用语言。
Common Languages(Java/C) UM对应Java通用语言提供的计算模型(通用计算机器)。
- 【必选】写一个解释器,把FizzBuzzWhizz中的整个Rule的关系树打印出来,测试用例之一如下:
public void test_desc_spec()
{
Rule spec = spec();
assertEquals("{or, {atom, contains_3, to_fizz}, "
+ "{or, {or, {and, {atom, times_3, to_fizz}, "
+ "{and, {atom, times_5, to_buzz}, "
+ "{atom, times_7, to_whizz}}}, "
+ "{or, {and, {atom, times_3, to_fizz}, "
+ "{atom, times_5, to_buzz}}, "
+ "{or, {and, {atom, times_3, to_fizz}, "
+ "{atom, times_7, to_whizz}}, "
+ "{and, {atom, times_5, to_buzz}, "
+ "{atom, times_7, to_whizz}}}}}, "
+ "{or, {or, {atom, times_3, to_fizz}, "
+ "{or, {atom, times_5, to_buzz}, "
+ "{atom, times_7, to_whizz}}}, "
+ "{atom, always_true, to_str}}}}",
desc(spec));
}
可参考:fayelab.ddd.fbwreloaded.complete.interpreter.RuleDescTool
- 【可选】用函数式编程实现FizzBuzzWhizz。
可参考:fayelab.ddd.fbwreloaded.functional
- 【可选】写一个编译器,将FizzBuzzWhizz的AST核心数据编译为OO或函数式实现。
可参考:fayelab.ddd.fbwreloaded.complete.compiler.oo 或 fayelab.ddd.fbwreloaded.complete.compiler.functional