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

1 Problem

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

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

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

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

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

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

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

2 Showcase & Discuss

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

3 Analysis

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

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

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

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

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

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

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

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

4 Design

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

4.1 Core Concepts

design core concepts

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

4.2 What does a Rule mean?

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

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

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

一个Rule的工作流程是什么样的?
接受一个int输入,判断int是否满足条件,满足的话就做动作,输出{true, String},不满足就输出{false, ""}。

When an integer is inputted, predicate it. If predicated true, the relative action will be acted, {true, String} is outputted, otherwise, {false, ""} is outputted.

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

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

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

design atom rule

4.3 What does a Relation mean?

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

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

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

4.3.1 How to express Relations explicitly?

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

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

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

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

Rule 1: or(r1_3, r1_5, r1_7) 

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

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

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

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

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

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

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

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

4.3.2 What does the Or / And mean?

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

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

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

design combination rule

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

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

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

4.3.3 Mapping Problem Domain to Algebra Semantic Domain

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

design domain mapping

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

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

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

4.4 Representation

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

4.4.1 Atom Rules

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

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

4.4.2 Rule 1

r1 or r1_3 r1_5 r1_7

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

4.4.3 Rule 2

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

r2 or r1_357 r1_35 r1_37 r1_57

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

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

4.4.4 Rule 3

r3 atom contains 3 to_fizz

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

4.4.5 Default Rule

rd atom always_true to_str

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

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

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

spec or r3 r2 r1 rd

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

4.4.7 Specification

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

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

r1 or r1_3 r1_5 r1_7

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

r2 or r1_357 r1_35 r1_37 r1_57

r3 atom contains 3 to_fizz

rd atom always_true to_str

spec or r3 r2 r1 rd

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

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

我们也可以提供一套API,不过就和实现语言相关了。以Java语言为例,atom、and、or、times、contains、alwaysTrue、toFizz、toBuzz、toWhizz、toStr都是API,用这些API编程写出的Specification如下:

public static Rule spec()
{
    Rule r1_3 = atom(times(3), toFizz());
    Rule r1_5 = atom(times(5), toBuzz());
    Rule r1_7 = atom(times(7), toWhizz());

    Rule r1 = or3(r1_3, r1_5, r1_7);
    Rule r2 = or4(and3(r1_3, r1_5, r1_7),
                  and(r1_3, r1_5),
                  and(r1_3, r1_7),
                  and(r1_5, r1_7));
    Rule r3 = atom(contains(3), toFizz());
    Rule rd = atom(alwaysTrue(), toStr());
    
    return or4(r3, r2, r1, rd);
}

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

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

4.5 Design Summary

做一个设计小结。

4.5.1 Mapping to Isomorphic Domain

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

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

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

4.5.2 Core Concepts & Semantic Domain

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

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

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

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

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

4.5.3 Type-based Design

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

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

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

5 Implementation

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

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

    public static Rule spec()
    {
        Rule r1_3 = atom(times(3), toFizz());
        Rule r1_5 = atom(times(5), toBuzz());
        Rule r1_7 = atom(times(7), toWhizz());
    
        Rule r1 = or3(r1_3, r1_5, r1_7);
        Rule r2 = or4(and3(r1_3, r1_5, r1_7),
                      and(r1_3, r1_5),
                      and(r1_3, r1_7),
                      and(r1_5, r1_7));
        Rule r3 = atom(contains(3), toFizz());
        Rule rd = atom(alwaysTrue(), toStr());
        
        return or4(r3, r2, r1, rd);
    }

实现步骤(Steps):

  1. Atom Rules 实现r1_3、r1_5、r1_7这三个Atom Rule。构造Atom Rule需要Predication和Action,所以要先实现times这个Predication以及toFizz、toBuzz和toWhizz这三个Action。

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

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

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

  5. Default Rule
    实现alwaysTrue Predication和toStr Action,构造出Default Rule。

  6. Specification
    用API编写spec。

  7. FizzBuzzWhizz
    最终实现FizzBuzzWhizz。

代码包路径:fayelab.ddd.fbw.original

5.1 Atom Rules: r1_3 r1_5 r1_7

实现r1_3、r1_5、r1_7这三个Atom Rule。构造Atom Rule需要Predication和Action,所以要先实现times这个Predication以及toFizz、toBuzz和toWhizz这三个Action。

5.1.1 Predication: times

TimesTest.java

package fayelab.ddd.fbw.original.predication;

public class TimesTest extends TestCase
{
    public void test_times_3()
    {
        Predication times3 = new Times(3);
        assertTrue(times3.predicate(6));
        assertFalse(times3.predicate(5));
    }
    
    public void test_times_5()
    {
        Predication times5 = new Times(5);
        assertTrue(times5.predicate(10));
        assertFalse(times5.predicate(11));
    }
    
    public void test_times_7()
    {
        Predication times7 = new Times(7);
        assertTrue(times7.predicate(14));
        assertFalse(times7.predicate(15));
    }    
}

Predication.java

package fayelab.ddd.fbw.original.predication;

public interface Predication
{
    boolean predicate(int n);
}

Times.java

package fayelab.ddd.fbw.original.predication;

public class Times implements Predication
{
    private int base;

    public Times(int base)
    {
        this.base = base;
    }

    @Override
    public boolean predicate(int n)
    {
        return n % base == 0;
    }
}

5.1.2 Action: toFizz toBuzz toWhizz

ToFizzTest.java

package fayelab.ddd.fbw.original.action;

public class ToFizzTest extends TestCase
{
    public void test_toFizz()
    {
        Action toFizz = new ToFizz();
        assertEquals("Fizz", toFizz.act(3));
    }
}

Action.java

package fayelab.ddd.fbw.original.action;

public interface Action
{
    String act(int n);
}

ToFizz.java

package fayelab.ddd.fbw.original.action;

public class ToFizz implements Action
{
    @Override
    public String act(int n)
    {
        return "Fizz";
    }
}

ToBuzzTest.java

package fayelab.ddd.fbw.original.action;

public class ToBuzzTest extends TestCase
{
    public void test_toBuzz()
    {
        Action toBuzz = new ToBuzz();
        assertEquals("Buzz", toBuzz.act(5));
    }
}

ToBuzz.java

package fayelab.ddd.fbw.original.action;

public class ToBuzz implements Action
{
    @Override
    public String act(int n)
    {
        return "Buzz";
    }
}

ToWhizzTest.java

package fayelab.ddd.fbw.original.action;

public class ToWhizzTest extends TestCase
{
    public void test_toWhizz()
    {
        Action toWhizz = new ToWhizz();
        assertEquals("Whizz", toWhizz.act(7));
    }
}

ToWhizz.java

package fayelab.ddd.fbw.original.action;

public class ToWhizz implements Action
{
    @Override
    public String act(int n)
    {
        return "Whizz";
    }
}

5.1.3 Atom: r1_3 r1_5 r1_7

5.1.3.1 r1_3

5.1.3.1.1 Coding

这里之所以没有写专门的AtomTest、OrTest和AndTest等测试类,是因为后面还会有Rule 1、Rule 2和Specification等业务规则的测试,放在AtomTest、OrTest和AndTest都不合适,所以就都放在RuleTest测试类中了。这样做还有一个好处,重构后的checkResult不用重复出现在AtomTest、OrTest和AndTest中。

RuleTest.java

package fayelab.ddd.fbw.original.rule;

public class RuleTest extends TestCase
{
    public void test_atom_rule_1_3()
    {
        Rule r1_3 = new Atom(new Times(3), new ToFizz());
        
        Result actual1 = r1_3.apply(3);
        assertTrue(actual1.isSucceeded());
        assertEquals("Fizz", actual1.getStr());
        
        Result actual2 = r1_3.apply(4);
        assertFalse(actual2.isSucceeded());
        assertEquals("", actual2.getStr());
    }
}

Rule.java

public interface Rule
{
    Result apply(int n);
}

Result.java

package fayelab.ddd.fbw.original.rule;

public class Result
{
    private boolean isSucceeded;
    private String str;

    public Result(boolean isSucceeded, String str)
    {
        this.isSucceeded = isSucceeded;
        this.str = str;
    }

    public boolean isSucceeded()
    {
        return isSucceeded;
    }

    public String getStr()
    {
        return str;
    }
}

Atom.java

package fayelab.ddd.fbw.original.rule;

public class Atom implements Rule
{
    private Predication predication;
    private Action action;

    public Atom(Predication predication, Action action)
    {
        this.predication = predication;
        this.action = action;
    }

    @Override
    public Result apply(int n)
    {
        if(predication.predicate(n))
        {
            return new Result(true, action.act(n));
        }
        
        return new Result(false, "");
    }
}
5.1.3.1.2 Refactoring

重构要点:

  1. RuleTest
    • 抽取checkResult()方法;
    • 新增SpecTool类,并在其中增加atom()、times()、toFizz()等静态工具方法。
  2. Result类中提供succeededResult()和failedResult()等静态工具方法,且构造函数的访问属性改为private。
  3. Atom类改为调用Result类提供的静态方法。

RuleTest.java

public class RuleTest extends TestCase
{
    public void test_atom_rule_1_3()
    {
        Rule r1_3 = new Atom(new Times(3), new ToFizz());
        checkResult(true, "Fizz", r1_3.apply(3));
        checkResult(false, "", r1_3.apply(4));
        
        Rule r1_3_t = atom(times(3), toFizz());
        checkResult(true, "Fizz", r1_3_t.apply(3));
        checkResult(false, "", r1_3_t.apply(4));
    }

    private void checkResult(boolean expectedSucceeded, String expectedStr, Result actual)
    {
        assertEquals(expectedSucceeded, actual.isSucceeded());
        assertEquals(expectedStr, actual.getStr());
    }
}

SpecTool.java

package fayelab.ddd.fbw.original;

public class SpecTool
{
    public static Predication times(int n)
    {
        return new Times(n);
    }
    
    public static Action toFizz()
    {
        return new ToFizz();
    }
    
    public static Rule atom(Predication predication, Action action)
    {
        return new Atom(predication, action);
    }
}

Result.java

    private Result(boolean isSucceeded, String str)
    {
        this.isSucceeded = isSucceeded;
        this.str = str;
    }

    public static Result succeededResult(String str)
    {
        return new Result(true, str);
    }

    public static Result failedResult()
    {
        return new Result(false, "");
    }

Atom.java

    @Override
    public Result apply(int n)
    {
        if(predication.predicate(n))
        {
            return Result.succeededResult(action.act(n));
        }
        
        return Result.failedResult();
    }

5.1.3.2 r1_5 r1_7

在SpecTool中补充toBuzz()、toWhizz()工具方法。调用SpecTool中的工具方法,继续补充r1_5和r1_7的测试用例。

RuleTest.java

    public void test_atom_rule_1_5()
    {
        Rule r1_5 = atom(times(5), toBuzz());
        checkResult(true, "Buzz", r1_5.apply(10));
        checkResult(false, "", r1_5.apply(11));
    }
    
    public void test_atom_rule_1_7()
    {
        Rule r1_7 = atom(times(7), toWhizz());
        checkResult(true, "Whizz", r1_7.apply(14));
        checkResult(false, "", r1_7.apply(13));
    }

SpecTool.java

    public static Action toBuzz()
    {
        return new ToBuzz();
    }
    
    public static Action toWhizz()
    {
        return new ToWhizz();
    }

5.2 Or Rule & Rule 1

实现Or Rule,构造出Rule1。

5.2.1 Or Rule

RuleTest.java

    public void test_or_rule()
    {
        Rule r1_3 = atom(times(3), toFizz());
        Rule r1_5 = atom(times(5), toBuzz());
        
        Rule or_35 = or(r1_3, r1_5);
        checkResult(true, "Fizz", or_35.apply(6));
        checkResult(true, "Buzz", or_35.apply(10));
        checkResult(true, "Fizz", or_35.apply(15));
        checkResult(false, "", or_35.apply(7));
    }

SpecTool.java

    public static Rule or(Rule rule1, Rule rule2)
    {
        return new Or(rule1, rule2);
    }

Or.java

package fayelab.ddd.fbw.original.rule;

public class Or implements Rule
{
    private Rule rule1;
    private Rule rule2;

    public Or(Rule rule1, Rule rule2)
    {
        this.rule1 = rule1;
        this.rule2 = rule2;
    }

    @Override
    public Result apply(int n)
    {
        Result result1 = rule1.apply(n);
        if(result1.isSucceeded())
        {
            return result1;
        }
        
        return rule2.apply(n);
    }
}

5.2.2 Rule 1

RuleTest.java

    public void test_rule_1()
    {
        Rule r1_3 = atom(times(3), toFizz());
        Rule r1_5 = atom(times(5), toBuzz());
        Rule r1_7 = atom(times(7), toWhizz());
        
        Rule r1 = or3(r1_3, r1_5, r1_7);
        checkResult(true, "Fizz", r1.apply(6));
        checkResult(true, "Buzz", r1.apply(10));
        checkResult(true, "Whizz", r1.apply(14));
        checkResult(false, "", r1.apply(13));
    }

SpecTool.java

    public static Rule or3(Rule rule1, Rule rule2, Rule rule3)
    {
        return or(rule1, or(rule2, rule3));
    }

5.3 And Rule & Rule 2

实现And Rule,构造出Rule2。

5.3.1 And Rule

RuleTest.java

    public void test_and_rule()
    {
        Rule r1_3 = atom(times(3), toFizz());
        Rule r1_5 = atom(times(5), toBuzz());
        
        Rule and_35 = and(r1_3, r1_5);
        checkResult(false, "", and_35.apply(3));
        checkResult(false, "", and_35.apply(5));
        checkResult(true, "FizzBuzz", and_35.apply(15));
        checkResult(false, "", and_35.apply(16));
    }

SpecTool.java

    public static Rule and(Rule rule1, Rule rule2)
    {
        return new And(rule1, rule2);
    }

And.java

package fayelab.ddd.fbw.original.rule;

public class And implements Rule
{
    private Rule rule1;
    private Rule rule2;

    public And(Rule rule1, Rule rule2)
    {
        this.rule1 = rule1;
        this.rule2 = rule2;
    }

    @Override
    public Result apply(int n)
    {
        Result result1 = rule1.apply(n);
        if(!result1.isSucceeded())
        {
            return Result.failedResult();
        }
        
        Result result2 = rule2.apply(n);
        if(!result2.isSucceeded())
        {
            return Result.failedResult();
        }
        
        return Result.succeededResult(result1.getStr() + result2.getStr());
    }
}

5.3.2 Rule 2

RuleTest.java

    public void test_rule_2()
    {
        Rule r1_3 = atom(times(3), toFizz());
        Rule r1_5 = atom(times(5), toBuzz());
        Rule r1_7 = atom(times(7), toWhizz());
        
        Rule r2 = or4(and3(r1_3, r1_5, r1_7),
                      and(r1_3, r1_5),
                      and(r1_3, r1_7),
                      and(r1_5, r1_7));
        checkResult(false, "", r2.apply(3));
        checkResult(false, "", r2.apply(5));
        checkResult(false, "", r2.apply(7));
        checkResult(true, "FizzBuzzWhizz", r2.apply(3*5*7));
        checkResult(false, "", r2.apply(104));
        checkResult(true, "FizzBuzz", r2.apply(15));
        checkResult(false, "", r2.apply(14));
        checkResult(true, "FizzWhizz", r2.apply(21));
        checkResult(false, "", r2.apply(22));
        checkResult(true, "BuzzWhizz", r2.apply(35));
        checkResult(false, "", r2.apply(34));
    }

SpecTool.java

    public static Rule or4(Rule rule1, Rule rule2, Rule rule3, Rule rule4)
    {
        return or(rule1, or3(rule2, rule3, rule4));
    }

    public static Rule and3(Rule rule1, Rule rule2, Rule rule3)
    {
        return and(rule1, and(rule2, rule3));
    }

5.4 Atom Rule: Rule 3

实现contains Predication,构造出Rule3。

5.4.1 Predication: contains

ContainsTest.java

package fayelab.ddd.fbw.original.predication;

public class ContainsTest extends TestCase
{
    public void test_contains()
    {
        Predication contains3 = new Contains(3);
        assertTrue(contains3.predicate(13));
        assertTrue(contains3.predicate(35));
        assertTrue(contains3.predicate(300));
        assertFalse(contains3.predicate(24));
        
        Predication contains3_t = contains(3);
        assertTrue(contains3_t.predicate(13));
    }
}

SpecTool.java

    public static Predication contains(int digit)
    {
        return new Contains(digit);
    }

Contains.java

package fayelab.ddd.fbw.original.predication;

public class Contains implements Predication
{
    private int digit;

    public Contains(int digit)
    {
        this.digit = digit;
    }

    @Override
    public boolean predicate(int n)
    {
        int p1 = n % 10;
        int p2 = (n / 10) % 10;
        int p3 = (n / 100) % 10;
        
        return p1 == digit || p2 == digit || p3 == digit;
    }
}

5.4.2 Atom: Rule 3

RuleTest.java

    public void test_rule_3()
    {
        Rule r3 = atom(contains(3), toFizz());
        checkResult(true, "Fizz", r3.apply(3));
        checkResult(true, "Fizz", r3.apply(13));
        checkResult(true, "Fizz", r3.apply(31));
        checkResult(false, "", r3.apply(24));
    }

5.5 Atom Rule: Default Rule

实现alwaysTrue Predication和toStr Action,构造出Default Rule。

5.5.1 Predication: alwaysTrue

AlwaysTrueTest.java

package fayelab.ddd.fbw.original.predication;

public class AlwaysTrueTest extends TestCase
{
    public void test_alwaysTrue()
    {
        Predication alwaysTrue = new AlwaysTrue();
        assertTrue(alwaysTrue.predicate(1));
        assertTrue(alwaysTrue.predicate(3));
        assertTrue(alwaysTrue.predicate(5));
        
        Predication alwaysTrue_t = alwaysTrue();
        assertTrue(alwaysTrue_t.predicate(1));
    }
}

SpecTool.java

    public static Predication alwaysTrue()
    {
        return new AlwaysTrue();
    }

AlwaysTrue.java

package fayelab.ddd.fbw.original.predication;

public class AlwaysTrue implements Predication
{
    @Override
    public boolean predicate(int n)
    {
        return true;
    }
}

5.5.2 Action: toStr

ToStrTest.java

package fayelab.ddd.fbw.original.action;

public class ToStrTest extends TestCase
{
    public void test_toStr()
    {
        Action toStr = new ToStr();
        assertEquals("1", toStr.act(1));
        assertEquals("10", toStr.act(10));
        
        Action toStr_t = toStr();
        assertEquals("1", toStr_t.act(1));
    }
}

SpecTool.java

    public static Action toStr()
    {
        return new ToStr();
    }

ToStr.java

package fayelab.ddd.fbw.original.action;

public class ToStr implements Action
{
    @Override
    public String act(int n)
    {
        return String.valueOf(n);
    }
}

5.5.4 Atom: Default Rule

RuleTest.java

    public void test_default_rule()
    {
        Rule rd = atom(alwaysTrue(), toStr());
        checkResult(true, "1", rd.apply(1));
        checkResult(true, "3", rd.apply(3));
    }

5.6 Spec

用API编写spec。

RuleTest.java

    public void test_spec()
    {
        Rule spec = spec();
        checkResult(true, "Fizz", spec.apply(35));
        checkResult(true, "FizzBuzz", spec.apply(15));
        checkResult(true, "FizzWhizz", spec.apply(21));
        checkResult(true, "BuzzWhizz", spec.apply(70));
        checkResult(true, "Fizz", spec.apply(9));
        checkResult(true, "1", spec.apply(1));
    }

SpecTool.java

    public static Rule spec()
    {
        Rule r1_3 = atom(times(3), toFizz());
        Rule r1_5 = atom(times(5), toBuzz());
        Rule r1_7 = atom(times(7), toWhizz());
    
        Rule r1 = or3(r1_3, r1_5, r1_7);
        Rule r2 = or4(and3(r1_3, r1_5, r1_7),
                      and(r1_3, r1_5),
                      and(r1_3, r1_7),
                      and(r1_5, r1_7));
        Rule r3 = atom(contains(3), toFizz());
        Rule rd = atom(alwaysTrue(), toStr());
        
        return or4(r3, r2, r1, rd);
    }

5.7 Test Suite

action、predication、rule包各自增加自己的Test Suite,最后再加一个总的Test Suite。

action.AllTests.java

package fayelab.ddd.fbw.original.action;

public class AllTests
{
    public static Test suite()
    {
        TestSuite suite = new TestSuite(AllTests.class.getName());
        //$JUnit-BEGIN$
        suite.addTestSuite(ToFizzTest.class);
        suite.addTestSuite(ToBuzzTest.class);
        suite.addTestSuite(ToWhizzTest.class);
        suite.addTestSuite(ToStrTest.class);
        //$JUnit-END$
        return suite;
    }
}

predication.AllTests.java

package fayelab.ddd.fbw.original.predication;

public class AllTests
{
    public static Test suite()
    {
        TestSuite suite = new TestSuite(AllTests.class.getName());
        //$JUnit-BEGIN$
        suite.addTestSuite(TimesTest.class);
        suite.addTestSuite(ContainsTest.class);
        suite.addTestSuite(AlwaysTrueTest.class);
        //$JUnit-END$
        return suite;
    }
}

rule.AllTests.java

public class AllTests
{
    public static Test suite()
    {
        TestSuite suite = new TestSuite(AllTests.class.getName());
        //$JUnit-BEGIN$
        suite.addTestSuite(RuleTest.class);
        //$JUnit-END$
        return suite;
    }
}

fbw.AllTests.java

package fayelab.ddd.fbw.original;

public class AllTests
{
    public static Test suite()
    {
        TestSuite suite = new TestSuite(AllTests.class.getName());
        //$JUnit-BEGIN$
        suite.addTest(fayelab.ddd.fbw.original.action.AllTests.suite());
        suite.addTest(fayelab.ddd.fbw.original.predication.AllTests.suite());
        suite.addTest(fayelab.ddd.fbw.original.rule.AllTests.suite());
        //$JUnit-END$
        return suite;
    }
}

5.8 FizzBuzzWhizz

最终实现FizzBuzzWhizz。

package fayelab.ddd.fbw.original;

public class FizzBuzzWhizz
{
    public void run()
    {
        Rule spec = spec();
        List<Result> results = IntStream.rangeClosed(1, 100)
                                        .mapToObj(spec::apply)
                                        .collect(Collectors.toList());
        output(results);
    }
    
    private void output(List<Result> results)
    {
        results.stream().map(Result::getStr).forEach(System.out::println);
    }

    public static void main(String[] args)
    {
        new FizzBuzzWhizz().run();
    }
}

6 Summary

6.1 Global View

summary global view

6.2 What is Design?

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

design computable

6.3 What is Good Design?

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

good design

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

6.4 How to do Design?

how to do design

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

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

6.5 What is Programming?

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

6.6 Design & Programming

design and programming

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

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

6.7 How to improve Design Capability?

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

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

7 Homework

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

8 My Homework

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

第2题参见FizzBuzzWhizz Reloaded,包路径:faylab.ddd.fbwreloaded.functional。

8.1 straightforward

8.1.1 ToHazz

ToHazzTest.java

package fayelab.ddd.fbw.eight.action;

public class ToHazzTest extends TestCase
{
    public void test_toHazz()
    {
        Action toHazz = new ToHazz();
        assertEquals("Hazz", toHazz.act(8));
        
        Action toHazz_t = toHazz();
        assertEquals("Hazz", toHazz_t.act(8));
    }
}

action.AllTests.java

package fayelab.ddd.fbw.eight.action;

public class AllActionTests
{
    public static Test suite()
    {
        TestSuite suite = new TestSuite(AllTests.class.getName());
        //$JUnit-BEGIN$
        suite.addTestSuite(ToFizzTest.class);
        suite.addTestSuite(ToBuzzTest.class);
        suite.addTestSuite(ToWhizzTest.class);
        suite.addTestSuite(ToStrTest.class);
        suite.addTestSuite(ToHazzTest.class);
        //$JUnit-END$
        return suite;
    }
}

SpecTool.java

    public static Action toHazz()
    {
        return new ToHazz();
    }

ToHazz.java

package fayelab.ddd.fbw.eight.action;

public class ToHazz implements Action
{
    @Override
    public String act(int n)
    {
        return "Hazz";
    }
}

8.1.2 Rule 1_8

RuleTest.java

    public void test_atom_rule_1_8()
    {
        Rule r1_8 = atom(times(8), toHazz());
        checkResult(true, "Hazz", r1_8.apply(16));
        checkResult(false, "", r1_8.apply(13));
    }

8.1.3 Rule 1

RuleTest.java

    public void test_rule_1()
    {
        Rule r1_3 = atom(times(3), toFizz());
        Rule r1_5 = atom(times(5), toBuzz());
        Rule r1_7 = atom(times(7), toWhizz());
        Rule r1_8 = atom(times(8), toHazz());
        
        Rule r1 = or4(r1_3, r1_5, r1_7, r1_8);
        checkResult(true, "Fizz", r1.apply(6));
        checkResult(true, "Buzz", r1.apply(10));
        checkResult(true, "Whizz", r1.apply(14));
        checkResult(true, "Hazz", r1.apply(16));
        checkResult(false, "", r1.apply(13));
    }

8.1.4 Rule 2

RuleTest.java

    public void test_rule_2()
    {
        Rule r1_3 = atom(times(3), toFizz());
        Rule r1_5 = atom(times(5), toBuzz());
        Rule r1_7 = atom(times(7), toWhizz());
        Rule r1_8 = atom(times(8), toHazz());
        
        Rule r2 = or(and4(r1_3, r1_5, r1_7, r1_8),
                     and3(r1_3, r1_5, r1_7),
                     and3(r1_3, r1_5, r1_8),
                     and3(r1_3, r1_7, r1_8),
                     and3(r1_5, r1_7, r1_8),
                     and(r1_3, r1_5),
                     and(r1_3, r1_7),
                     and(r1_3, r1_8),
                     and(r1_5, r1_7),
                     and(r1_5, r1_8),
                     and(r1_7, r1_8));
        
        checkResult(false, "", r2.apply(3));
        checkResult(false, "", r2.apply(5));
        checkResult(false, "", r2.apply(7));
        checkResult(false, "", r2.apply(8));
        checkResult(true, "FizzBuzzWhizzHazz", r2.apply(3*5*7*8));
        checkResult(false, "", r2.apply(841));
        checkResult(true, "FizzBuzzWhizz", r2.apply(3*5*7));
        checkResult(false, "", r2.apply(104));
        checkResult(true, "FizzBuzzHazz", r2.apply(3*5*8));
        checkResult(false, "", r2.apply(121));
        checkResult(true, "FizzWhizzHazz", r2.apply(3*7*8));
        checkResult(false, "", r2.apply(167));
        checkResult(true, "BuzzWhizzHazz", r2.apply(5*7*8));
        checkResult(false, "", r2.apply(281));
        checkResult(true, "FizzBuzz", r2.apply(15));
        checkResult(false, "", r2.apply(14));
        checkResult(true, "FizzWhizz", r2.apply(21));
        checkResult(false, "", r2.apply(22));
        checkResult(true, "FizzHazz", r2.apply(24));
        checkResult(false, "", r2.apply(23));
        checkResult(true, "BuzzWhizz", r2.apply(35));
        checkResult(false, "", r2.apply(34));
        checkResult(true, "BuzzHazz", r2.apply(40));
        checkResult(false, "", r2.apply(41));
        checkResult(true, "WhizzHazz", r2.apply(56));
        checkResult(false, "", r2.apply(55));
    }

SpecTool类增加and4()、or(...)、or(List)方法。

    public static Rule or(Rule...rules)
    {
        return or(asList(rules));
    }
    
    private static Rule or(List<Rule> rules)
    {
        return rules.stream().reduce(Or::new).get();
    }

    public static Rule and4(Rule rule1, Rule rule2, Rule rule3, Rule rule4)
    {
        return and(rule1, and3(rule2, rule3, rule4));
    }

SpecTool类增加or(...)、or(List)方法后,or3()和or4()方法就可以删掉了。

RuleTest.java中引用or4()的地方全部改为引用or(...)。

    public void test_rule_1()
    {
        Rule r1_3 = atom(times(3), toFizz());
        Rule r1_5 = atom(times(5), toBuzz());
        Rule r1_7 = atom(times(7), toWhizz());
        Rule r1_8 = atom(times(8), toHazz());
        
        Rule r1 = or(r1_3, r1_5, r1_7, r1_8);
        checkResult(true, "Fizz", r1.apply(6));
        checkResult(true, "Buzz", r1.apply(10));
        checkResult(true, "Whizz", r1.apply(14));
        checkResult(true, "Hazz", r1.apply(16));
        checkResult(false, "", r1.apply(13));
    }

8.1.5 Spec

RuleTest.java

    public void test_spec()
    {
        Rule spec = spec();
                
        checkResult(true, "Fizz", spec.apply(35));
        checkResult(true, "FizzBuzzWhizzHazz", spec.apply(3*5*7*8));
        checkResult(true, "FizzBuzzWhizz", spec.apply(3*5*7));
        checkResult(true, "FizzBuzzHazz", spec.apply(3*5*8));
        checkResult(true, "FizzWhizzHazz", spec.apply(3*7*8));
        checkResult(true, "BuzzWhizzHazz", spec.apply(5*7*8));
        checkResult(true, "FizzBuzz", spec.apply(15));
        checkResult(true, "FizzWhizz", spec.apply(21));
        checkResult(true, "FizzHazz", spec.apply(24));
        checkResult(true, "BuzzWhizz", spec.apply(70));
        checkResult(true, "BuzzHazz", spec.apply(40));
        checkResult(true, "WhizzHazz", spec.apply(56));
        checkResult(true, "Fizz", spec.apply(9));
        checkResult(true, "Buzz", spec.apply(5));
        checkResult(true, "Whizz", spec.apply(7));
        checkResult(true, "Hazz", spec.apply(8));
        checkResult(true, "1", spec.apply(1));
    }

SpecTool.java

    public static Rule spec()
    {
        Rule r1_3 = atom(times(3), toFizz());
        Rule r1_5 = atom(times(5), toBuzz());
        Rule r1_7 = atom(times(7), toWhizz());
        Rule r1_8 = atom(times(8), toHazz());
    
        Rule r1 = or(r1_3, r1_5, r1_7, r1_8);
        Rule r2 = or(and4(r1_3, r1_5, r1_7, r1_8),
                     and3(r1_3, r1_5, r1_7),
                     and3(r1_3, r1_5, r1_8),
                     and3(r1_3, r1_7, r1_8),
                     and3(r1_5, r1_7, r1_8),
                     and(r1_3, r1_5),
                     and(r1_3, r1_7),
                     and(r1_3, r1_8),
                     and(r1_5, r1_7),
                     and(r1_5, r1_8),
                     and(r1_7, r1_8));
        Rule r3 = atom(contains(3), toFizz());
        Rule rd = atom(alwaysTrue(), toStr());
        
        return or(r3, r2, r1, rd);
    }

8.2 combination

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

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

8.2.1 ListTool

在ListTool列表工具类中实现两个方法:

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

ListToolTest.java

package fayelab.ddd.fbw.eight.combination;

public class ListToolTest extends TestCase
{
    public void test_combinate()
    {
        List<Integer> lst = asList(1, 2, 3, 4);
        
        assertEquals(asList(), combinate(lst, 0));
        assertEquals(asList(), combinate(lst, 5));
        
        assertEquals(asList(asList(1), asList(2), asList(3), asList(4)), 
                     combinate(lst, 1));
        
        assertEquals(asList(asList(1, 2), asList(1, 3), asList(1, 4), 
                            asList(2, 3), asList(2, 4),
                            asList(3, 4)), 
                     combinate(lst, 2));
        
        assertEquals(asList(asList(1, 2, 3), asList(1, 2, 4), asList(1, 3, 4), 
                            asList(2, 3, 4)), 
                     combinate(lst, 3));
        
        assertEquals(asList(asList(1, 2, 3, 4)), 
                     combinate(lst, 4));
    }
    
    public void test_flatten()
    {
        List<List<Integer>> lstOfLst = asList(asList(1, 2), asList(3, 4), asList(5));
        assertEquals(asList(1, 2, 3, 4, 5), flatten(lstOfLst));
        
        List<List<List<Integer>>> lstOfLst2 = asList(asList(asList(1, 2), asList(3, 4)), 
                                                     asList(asList(5), asList(6)));
        assertEquals(asList(asList(1, 2), asList(3, 4), asList(5), asList(6)), 
                     flatten(lstOfLst2));
    }
}

fbw.AllTests.java

    public static Test suite()
    {
        TestSuite suite = new TestSuite(AllTests.class.getName());
        //$JUnit-BEGIN$
        suite.addTest(fayelab.ddd.fbw.eight.combination.action.AllTests.suite());
        suite.addTest(fayelab.ddd.fbw.eight.combination.predication.AllTests.suite());
        suite.addTest(fayelab.ddd.fbw.eight.combination.rule.AllTests.suite());
        suite.addTestSuite(ListToolTest.class);
        //$JUnit-END$
        return suite;
    }

ListTool.java

package fayelab.ddd.fbw.eight.combination;

public class ListTool
{
    public static <T> List<List<T>> combinate(List<T> lst, int n)
    {
        if(n < 1 || n > lst.size())
        {
            return asList();
        }
        
        if(n == 1)
        {
            return lst.stream().map(ele -> asList(ele)).collect(toList());
        }
        
        if(n == lst.size())
        {
            return asList(lst);
        }
        
        T head = lst.get(0);
        List<T> rest = lst.subList(1, lst.size());
        List<List<T>> comb1 = combinate(rest, n - 1).stream()
                                                    .map(ele -> addHead(head, ele))
                                                    .collect(toList());
        List<List<T>> comb2 = combinate(rest, n);
        return concat(comb1, comb2);
    }
    
    public static <T> List<T> flatten(List<List<T>> lstOfLst)
    {
        return lstOfLst.stream().collect(ArrayList::new, 
                                         (result, lst) -> result.addAll(lst), 
                                         ArrayList::addAll);
    }
    
    private static <T> List<T> addHead(T head, List<T> lst)
    {
        List<T> result = new ArrayList<>();
        result.add(head);
        result.addAll(lst);
        return result;
    }

    private static <T> List<List<T>> concat(List<List<T>> lstOfLst1, List<List<T>> lstOfLst2)
    {
        return Stream.concat(lstOfLst1.stream(), lstOfLst2.stream()).collect(toList());
    }
}

8.2.2 Spec

为了不区分and3、and4和and,SpecTool还要提供and(list)方法,为了跟or保持一致,也提供and(...)方法,并删除 and3()和and4()方法。

SpecTool.java

    public static Rule and(Rule...rules)
    {
        return and(asList(rules));
    }
    
    public static Rule and(List<Rule> rules)
    {
        return rules.stream().reduce(And::new).get();
    }

RuleTest.java

    public void test_rule_2()
    {
        Rule r1_3 = atom(times(3), toFizz());
        Rule r1_5 = atom(times(5), toBuzz());
        Rule r1_7 = atom(times(7), toWhizz());
        Rule r1_8 = atom(times(8), toHazz());
        
        List<Rule> atomRules = asList(r1_3, r1_5, r1_7, r1_8);
        
        List<List<Rule>> lstOfRules = flatten(asList(combinate(atomRules, 4),
                                                     combinate(atomRules, 3),
                                                     combinate(atomRules, 2)));

        Rule r2 = or(lstOfRules.stream().map(rules -> and(rules)).collect(toList()));
        
        checkResult(false, "", r2.apply(3));
        checkResult(false, "", r2.apply(5));
        checkResult(false, "", r2.apply(7));
        checkResult(false, "", r2.apply(8));
        checkResult(true, "FizzBuzzWhizzHazz", r2.apply(3*5*7*8));
        checkResult(false, "", r2.apply(841));
        checkResult(true, "FizzBuzzWhizz", r2.apply(3*5*7));
        checkResult(false, "", r2.apply(104));
        checkResult(true, "FizzBuzzHazz", r2.apply(3*5*8));
        checkResult(false, "", r2.apply(121));
        checkResult(true, "FizzWhizzHazz", r2.apply(3*7*8));
        checkResult(false, "", r2.apply(167));
        checkResult(true, "BuzzWhizzHazz", r2.apply(5*7*8));
        checkResult(false, "", r2.apply(281));
        checkResult(true, "FizzBuzz", r2.apply(15));
        checkResult(false, "", r2.apply(14));
        checkResult(true, "FizzWhizz", r2.apply(21));
        checkResult(false, "", r2.apply(22));
        checkResult(true, "FizzHazz", r2.apply(24));
        checkResult(false, "", r2.apply(23));
        checkResult(true, "BuzzWhizz", r2.apply(35));
        checkResult(false, "", r2.apply(34));
        checkResult(true, "BuzzHazz", r2.apply(40));
        checkResult(false, "", r2.apply(41));
        checkResult(true, "WhizzHazz", r2.apply(56));
        checkResult(false, "", r2.apply(55));
    }

SpecTool.java

    public static Rule spec()
    {
        Rule r1_3 = atom(times(3), toFizz());
        Rule r1_5 = atom(times(5), toBuzz());
        Rule r1_7 = atom(times(7), toWhizz());
        Rule r1_8 = atom(times(8), toHazz());
        
        List<Rule> atomRules = asList(r1_3, r1_5, r1_7, r1_8);
    
        Rule r1 = or(atomRules);
        
        List<List<Rule>> lstOfRules = flatten(asList(combinate(atomRules, 4),
                                                     combinate(atomRules, 3),
                                                     combinate(atomRules, 2)));
        Rule r2 = or(lstOfRules.stream().map(rules -> and(rules)).collect(toList()));

        Rule r3 = atom(contains(3), toFizz());
        Rule rd = atom(alwaysTrue(), toStr());
        
        return or(r3, r2, r1, rd);
    }

SpecTool中or(List)和and(List)有重复,通过函数式编程范式消除重复。

    public static Rule or(List<Rule> rules)
    {
        return combine(rules, Or::new);
    }

    public static Rule and(List<Rule> rules)
    {
        return combine(rules, And::new);
    }

    private static Rule combine(List<Rule> rules, BinaryOperator<Rule> biCombine)
    {
        return rules.stream().reduce(biCombine).get();
    }
⚠️ **GitHub.com Fallback** ⚠️