201710 DDD DSL Design War FizzBuzzWhizz Reloaded - xiaoxianfaye/Courses GitHub Wiki

1 Review

1.1 Problem

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

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

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

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

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

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

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

1.2 Design

1.2.1 Step1: Semantics

设计的第一步:找到合适的语义。

What is Semantics?

  • 在解决一个问题的时候,经常问“What does this mean?”;
  • 既能够被精确清晰地表达出来,又能够教给计算机去做。

How to find an appropriate Semantics?

  • 首先考虑是否能将问题领域映射到一个熟悉的同构领域。如果能,就可以借用那个领域的机制来表达问题领域的概念,而不是重新发明。
  • 最好是映射到数学领域。一般来说,最终一定能在数学层面上找到一个同构领域,有可能只是没找到而已。

design domain mapping

把问题领域映射到了数学上的布尔代数的语义领域。

1.2.2 Step 2: Formalization

设计的第二步:形式化地表达语义。

语义(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是精确无二义的,而且是可计算的,因为布尔代数本身就是精确、可计算的。

1.2.3 Design Summary

以上都是语义(Semantics)层面的。不考虑实现,只考虑语义。

找到一个跟FizzBuzzWhizz问题领域完全同构的布尔代数领域,把问题领域映射到布尔 代数领域,用布尔代数领域的语言(AND、OR)形式化地表达问题领域的整个语义模型。

设计的两个步骤:

  1. 找到合适的同构语义领域;
  2. 用同构语义领域的语言精确地形式化地表达语义。

这就是“建模”的本质。

1.3 Implementations

在语义层面设计好以后,接下来就是选择合适的编程语言实现。

1.3.1 OO Implementation —— Interfaces & Objects

在实现层面,用一系列“接口”、“对象”来实现语义。

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

1.3.1.2 Interfaces & Objects

在实现层面,用一系列“接口”和“对象”来实现语义。

代码包路径:fayelab.ddd.fbwreloaded.oo

summary global view

1.3.2 Functional Implementation - High-order Functions

在实现层面,用一系列“函数”来实现语义。

代码包路径:fayelab.ddd.fbwreloaded.functional

1.3.2.1 Functions

1.3.2.1.1 Predication Functions

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)是一个函数,这个函数的输入是一个整数,输出是一个布尔值。从语义理解更重要、也更容易。

1.3.2.1.2 Action Functions

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()是一个函数,这个函数的输入是一个整数,输出是一个字符串。

1.3.2.1.3 Rule Functions
1.3.2.1.3.1 atom

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。

1.3.2.1.3.2 or

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。

1.3.2.1.3.3 and

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。

1.3.2.2.3.4 Closure

对于and和or来说,参与计算的rule是原子rule还是组合rule都无所谓,只要实现同样的语义即可。这个语义就是一个函数,函数的输入是一个整数,输出是一个Optional。

and和or是组合子。两个rule用and或or组合以后还是一个rule,组合后的rule跟原子rule一样还可以和其它的rule再次组合,也就是说,and和or组合子在rule的世界里是封闭的,我们称这种性质为闭包性(Closure)。

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

在OO实现方式中,通常用“接口”表示“是什么”,接口方法的输入、输出决定了“什么”的特征。用“接口”实现组合子的封闭性,对象只要满足了接口特征就是“什么”。 在Functional实现方式中,通常用“函数”表示“是什么”,函数的输入、输出决定了“什么”的特征。用“函数”实现组合子的封闭性,函数只要满足了输入输出特征就是“什么”。

1.3.2.1.4 Specification

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几乎是一样的。

1.3.2.1.5 Other Functions

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

2 Stop to Think

2.1 Double-edged Sword

语言能力是一把双刃剑。我们在享受编程语言的强大特性带来的好处的同时,也要警惕它们带来的阴暗面。正面影响是利用语言的强大特性实现起来很快,代码可能看上去会很“酷炫”。但其实负面影响更大,而且往往不容易察觉。语言能力太强大,在设计上就会偷懒,结果就是极大地损害了代码的可理解性,增加了不必要的复杂性。语言能力比较弱,其实是件好事情,会强迫你从问题出发、做出更好的设计,因为就那么点语言特性。

为了避免引起歧义,特别说明两点:

  • 这里的“设计”是指课程中一直反复强调的“以语义为核心”的设计过程,不是指使用了接口、继承、OO设计模式或者高阶函数的那些所谓的“设计”。现在大家应该可以理解了,那些都只是语义实现手段。

  • 那是不是说我们就不用学习、不使用语言新特性了呢?当然不是。在做好“以语义为核心”的设计的前提下,我们可以尽情享受语言的强大特性快速实现我们的设计。这就更需要我们不断提升自己的思考能力和把控能力。

如果没有接口,没有High-order Function,还能实现吗?如何实现呢?

2.2 Imperceptible Coupling in Implementations

在OO实现方式和函数式实现方式中,我们将语义的表达和执行解耦了,还存在耦合吗?解耦还不够彻底,还存在不易察觉的耦合。执行还可以再细分为执行方式和执行时机。在OO实现方式和函数式实现方式中,语义的表达和执行时机确实解耦了,Specification只是语义表达,定义Specification的时候并未执行,apply的时候才执行。但语义的表达和执行方式并未解耦。在OO实现方式中,语义的表达和执行方式均定义在了Atom、And、Or等对象中。在函数式实现方式中,语义的表达和执行方式均定义在了高阶函数中。这也是一种耦合,只是不那么容易被察觉。

举个例子,可以说得更清楚。现在要再实现一个功能——把FizzBuzzWhizz中的整个Rule的关系树打印出来。在OO实现方式和函数式实现方式中,要怎么修改代码呢?

需求变化了,代码就要修改。如果需求变化的复杂性和代码修改的复杂性是线性关系,这个设计就是一个非常好的设计,大部分情况下需求变化。代码修改一团糟,是完全非线性的。

能不能将语义的表达、执行时机和执行方式这三者彻底解耦呢?

3 Interpreter

代码包路径:fayelab.ddd.fbwreloaded.interpreter

3.1 Review Specification

    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都表达了一个个的概念和语义。它们可以是对象,也可以是函数,也可以既不是对象也不是函数,那是什么呢?它们可以只是“数据”,表达语义即可。语义什么时候执行、如何执行是另外一件事情。

通过**“数据”**来表达领域的核心概念、逻辑和关系。

3.2 Data Definition

在Java语言中,所有自定义数据类型都需要用Class来定义。

3.2.1 Predication

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所需要的参数。

3.2.2 Action

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的类型。

3.2.3 Rule

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的世界里是封闭的。

3.2.4 SpecTool

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中不再提供构造对象的工具方法,而是提供一系列构造数据的工具方法。

3.2.5 Summary

Specification看上去没变,但其实已经完全数据化了。OO实现方式和函数式实现方式中的Specification既有数据又有代码(行为)。这里的Specification只有数据,更抽象。

数据是声明性的(Declarative),告诉我们做什么(What)。代码是命令性的(Imperative),告诉我们如何做(How to)。声明性的语义表达力更强。

这里有个前提:在定义数据的时候,要从语义层面声明性地、精确地表达领域的核心概念、逻辑和关系(What),而不是从实现层面上定义数据结构(How to)。

这里的Specification其实只是一堆符号,它的语义在于如何去解释。

核心是数据,用数据去捕获领域的核心概念、逻辑和关系。

3.3 How to Run Specification?

Specification定义好了,都是纯数据,怎么让它跑起来呢?

对于类似Java的静态语言,写好程序(.java)以后,需要先编译(Compile)成字节码(.class),再由虚拟机加载执行。

对于类似JavaScript的动态语言,写好程序以后,不需要编译(Compile),可以直接解释(Interpret)执行。

这些编译器(Compiler)和解释器(Interpreter)都是别人已经写好的,我们直接拿过来用就好。

同理,Specification是用我们自己设计的DSL写的程序,写好程序以后,可以直接解释执行,也可以先编译再执行。

这里的解释器和编译器需要我们自己写。不用害怕,不是那么困难。因为这里的解释器和编译器同样是面向特定领域的。

本次课程里,我们重点讲一下解释器,编译器以后有机会再讲。

3.4 Interpreter Implementation

3.4.1 The Top of the Interpreter

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

3.4.2 Atom

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

3.4.3 Or

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

3.4.4 And

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

3.4.5 FizzBuzzWhizz

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

3.5 Summary

3.5.1 The Interpreter Recipe

  1. Look at a piece of data.
  2. Decide what kind of data it represents.
  3. Extract the components of the datum and do the right thing with them.

3.5.2 Solving Problem with Design, Not with the implementation language construction.

在前面的语义实现里,Specification是用我们自己设计的DSL写的程序,Interpreter是我们自己写的解释器,解释执行Specification。没有用到Java语言的高级特性,例如接口、对象、高阶函数等,只用了Java语言最基本的语言特性。

通过设计来解决问题,而不是用你选择的实现语言的构造来解决问题。先有设计,再选择合适的语言构造来表达,一定要强迫自己使用最简单的语言构造解决问题,除非实在没有办法,再选择高级的语言特性解决问题。要记住:语言越强,复杂性越高,而控制复杂性是软件工程里要解决的唯一问题。

3.5.3 Separation of Computational Description and Execution

在前面的实现里,语义的表达、执行方式、执行时机三者彻底解耦,实现了计算描述与执行的分离。

计算描述与执行分离这种设计思想非常强大。它带来的好处是:一方面在描述层面可以很自由,可以任意组装,不受限于执行层面;另一方面在执行层面可以做很多优化,比如并发,或者解释成其他的结果,比如把FizzBuzzWhizz中的整个Rule的关系树打印出来,只要再写一个解释器即可,需求变化的复杂性和代码修改的复杂性是线性关系,重要的是还不影响描述层面。

前提是先要有语义,解耦到什么程度,看具体问题和你的选择,但作为设计者要知道可能存在的耦合。

设计模式里最后一个模式是Interpreter模式,但这个模式很少有人提,但其实是最重要的。

3.5.4 Switch/case is not always a devil.

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。

4 Interpreter & Compiler

虽然在这次课程里重点讲解了Interpreter,但还是想从整体上将Interpreter和Compiler再给大家介绍一下。

4.1 Overview

下面从整体来介绍一下Interpreter和Compiler。

interpreter and compiler

4.1.1 Computational Description

对问题领域进行深入分析,设计出其语义与问题领域根本需求相匹配的计算模型,围绕这个计算模型设计出贴合问题领域的DSL。

基于这个DSL设计一套小语言,用这套小语言编写程序(Program),作为解析器(Parser)的输入。解析器一般包括两个步骤:Tokenizer/Lexical Analysis和Syntax/Grammar。Tokenizer/Lexical Analysis会根据DSL的语法定义将分隔符分隔的每个字段取出,并进行词法分析。Syntax/Grammar会根据语法定义检查语法的合法性。这两个步骤的区分并不是那么严格,有时Tokenizer/Lexical Analysis也可以做一些语法检查。解析器一般不用自己实现,现在有很多开源的第三方工具帮我们做这件事情。

解析器的输出是生成抽象语法树(AST:Abstract Syntax Tree)。这是我们的设计中最核心的部分。这部分只有数据,数据是核心,通过数据和数据结构声明性地表达了领域的核心概念、逻辑和关系。

FizzBuzzWhizz的AST如下:
fizzbuzzwhizz ast

以上均属于描述层面。

4.1.2 Computational Execution

执行层面有两种方式:解释器(Interpreter)和编译器(Compiler)。

  • 可以将解释器的作用理解为一台语义执行机器,实现了当前的语义。Interpreter一般都包含switch/case和递归调用。
  • 可以将编译器的作用理解为保持语义的等价语法变换。变换成了另外一种语言(比如字节码、机器指令等),最终都要由解释器(比如虚拟机、硬件等)解释执行。

4.1.3 Interpreter vs. Compiler

解释器是一边解释、一边执行。这样的实现方式导致:一方面执行的中间结果没法重用的,另一方面无法做语义上的优化,所以效率相对比较低下,但也更为通用。

编译器是编译一次、可以反复执行。编译时可以针对特定语义进行语义上的优化,所以效率相对比较高,但比较特定。

4.2 Program + Parser + Interpreter

下面来编写一个从Program到Parser到Interpreter的完整程序。

代码包路径:fayelab.ddd.fbwreloaded.complete.interpreter

4.2.1 Program

为了完整的展示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  
 

这套小语言的语法非常简单:

  1. 每行只描述一个规则(Rule);
  2. 每个Rule的字段用空格分隔。第1个字段含义固定是Rule的名称,第2个字段含义固定是Rule的类型,根据不同的Rule类型,后续字段的含义会有所不同:
    a) 当Rule类型是“atom”时,后续字段的含义依次是Predication类型、Predication参数(可能没有,例如always_true就不需要这个字段)、Action类型; b) 当Rule类型是“or”或“and”时,后续字段的含义是参与or或者and运算的Rule名称。
  3. 被引用的Rule必须是提前定义好的;
  4. 全小写;
  5. 为了能够清晰表述,可以用空行分段,不小心多敲了空格也没关系。

4.2.2 Parser

实现解析器,包括以下步骤:

  1. 不想在Parser中区分“or2|3|4”和“and2|3”,所以在SpecTool类中增加or(...)、or(List)、and(...)和and(List)静态工具方法,并删除or3、or4和and3等静态工具方法。
  2. 在单元测试代码中需要比较期望Rule和实际解析出来的Rule的相等性,不想在Rule的各种数据对象中覆写equals()方法,所以实现RuleEqualityTool类提供Rule的相等性比较,后面会看到,从某种角度来说这其实也是一个解释器。
  3. 最终实现解析器Parser。

另外,由于只是练习演示需要,这里的Parser没有实现语法检查功能,只实现了简单的解析功能。

4.2.2.1 SpecTool

不想在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);
    }

4.2.2.2 RuleEqualityTool

在单元测试代码中需要比较期望Rule和实际解析出来的Rule的相等性,不想在Rule的各种数据对象中覆写equals()方法,所以实现RuleEqualityTool类提供Rule的相等性比较。后面会看到,从某种角度来说这其实也是一个解释器。

4.2.2.2.1 equal predication

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();
    }
}
4.2.2.2.2 equal action

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();
    }
4.2.2.2.3 equal atom

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());
    }
4.2.2.2.4 equal or

涉及到递归调用,除了实现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());
    }
4.2.2.2.5 equal and

涉及到递归调用,除了实现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());
    }
4.2.2.2.6 equal

在已有的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;
    }

4.2.2.3 Parser

4.2.2.3.1 tokenize

编写tokenize()方法。

从文件读入程序操作的输出是列表(List),列表的元素是一个规则的描述字符串,作为tokenize()方法的输入。tokenize()方法的输出是列表的列表(List<List>)。内层列表的元素是规格描述字段。外层列表的元素是每一个规则对应的描述字段列表。

tokenize: (List<String> ruleDescs) -> (List<List<String>> tokens)

tokenize操作分为以下几个步骤:

  1. 过滤空行;
  2. 规格化每一个规则描述(normalize):去掉前后空格,字段之间多于一个的空格替换为一个空格;
  3. 以一个空格为分隔符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);
    }
}
4.2.2.3.2 parse tokens

编写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()的实现步骤为:

  1. parseAtom
  2. parseOr
  3. parseAnd
  4. parseRuleTokens
  5. 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)
4.2.2.3.2.1 parse atom

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();
    }
4.2.2.3.2.2 parse or

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);
    }
4.2.2.3.2.3 parse and

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;
    }
4.2.2.3.2.4 parseRuleTokens

先来解析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);
    }
4.2.2.3.2.5 parseTokens

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");
    }
4.2.2.3.3 parse

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)));
    }
4.2.2.3.4 parse from prog file

涉及到读取真实文件,不用单元测试保证,用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的字符串描述,后面会看到这其实也是一个解释器。

4.2.2.4 RuleDescTool

4.2.2.4.1 desc predication

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));
    }
}
4.2.2.4.2 desc action

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();
    }
4.2.2.4.3 desc atom

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);
    }
4.2.2.4.4 desc or

涉及到递归调用,除了实现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())));
    }
4.2.2.4.5 desc and

涉及到递归调用,除了实现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())));
    }
4.2.2.4.6 desc

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

4.2.2.5 Back to Parser

涉及到读取真实程序文件,不用单元测试保证,用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}}}}

4.2.2.6 Test Suite

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

4.2.3 Interpreter

解释器用的就是之前实现的Interpreter类,没有变化。

4.2.4 FizzBuzzWhizz

为了支持读取程序文件,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");
    }
}

4.2.5 Test

当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运行结果如下图所示:
test prog result 1

稍微修改一下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运行结果如下图所示:
test prog result 2 1

test prog result 2 2

test prog result 2 3

4.3 Program + Parser + Compiler

下面来编写一个从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

4.3.1 Compiler Functional

4.3.1.1 Program

跟之前的prog_1.fbw一样,没有变化。

4.3.1.2 Parser

解析器用的就是之前实现的Parser类,没有变化。

4.3.1.3 Compiler

4.3.1.3.1 Compile Predication

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;
    }
}
4.3.1.3.2 Compile Action

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();
    }
4.3.1.3.3 Compile Atom

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();
        };
    }
4.3.1.3.4 Compile Or

涉及到递归调用,除了实现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));
    }
4.3.1.3.5 Compile And

涉及到递归调用,除了实现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());
        };
    }
4.3.1.3.6 Compile

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

4.3.1.4 Test Suite

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

4.3.1.5 FizzBuzzWhizz

因为要先编译,对比之前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");
    }
}

4.3.1.6 Test

测试过程和结果同Interpreter。

4.3.2 Compiler OO

4.3.2.1 Compiled OO

由于编译前后的Predication、Action、Atom、Or和And等类虽然包名不同但类名相同,所以将编译后的Predication、Action、Atom、Or和And等类的类名都加上OO的前缀,并放到fayelab.ddd.fbwreloaded.complete.compiler.oo.compiled包路径下。

用Optional替换之前的Result类,还有其他一些小改造。

另外,这一层不需要SpecTool。

4.3.2.1.1 Compiled OO Predication

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;
    }
}
4.3.2.1.2 Compiled OO Action

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);
    }
}
4.3.2.1.3 Compiled OO Rule

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

4.3.2.2 Program

跟之前的prog_1.fbw一样,没有变化。

4.3.2.3 Parser

解析器用的就是之前实现的Parser类,没有变化。

4.3.2.4 Compiler

4.3.2.4.1 Compile Predication

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();
    }
}
4.3.2.4.2 Compile Action

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();
    }
4.3.2.4.3 Compile Atom

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()));
    }
4.3.2.4.4 Compile Or

涉及到递归调用,除了实现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()));
    }
4.3.2.4.5 Compile And

涉及到递归调用,除了实现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()));
    }
4.3.2.4.6 Compile

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

4.3.2.5 Test Suite

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

4.3.2.6 FizzBuzzWhizz

跟Compiler Functional的FizzBuzzWhizz一样。

4.3.2.7 Test

测试过程和结果同Interpreter和Compiler Functional。

5 Summary

5.1 How to do Design?

how to do design

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

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

5.2 What is Programming?

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

5.3 Design & Programming

design and programming

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

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

6 Homework

  1. 【必选】写一个解释器,把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

  1. 【可选】用函数式编程实现FizzBuzzWhizz。

可参考:fayelab.ddd.fbwreloaded.functional

  1. 【可选】写一个编译器,将FizzBuzzWhizz的AST核心数据编译为OO或函数式实现。

可参考:fayelab.ddd.fbwreloaded.complete.compiler.oo 或 fayelab.ddd.fbwreloaded.complete.compiler.functional

⚠️ **GitHub.com Fallback** ⚠️