序言
控制复杂性是计算机编程的本质。—— Brian Kernighan
前几天有幸参加了刘光聪同学组织的Code Retreat活动,收获比较大,其中印象最深刻的是结对编程实践。通过和几位高手的结对,开了眼界,他们不光设计能力超强,而且是键盘侠,两手一抹,数行高质量代码就跃然眼前,非常过瘾。
在Code Retreat活动后,笔者打算写篇文章梳理一下学到的知识,同时分享给大家。
FizzBuzzWhizz问题
FizzBuzzWhizz问题是某公司的面试题目,具体如下所示:
你是一名体育老师,在某次课距离下课还有五分钟时,你决定搞一个游戏。此时有100名学生在上课。游戏的规则是:
- 你首先说出三个不同的特殊数,要求必须是个位数,比如3、5、7。
- 让所有学生拍成一队,然后按顺序报数。
- 学生报数时,如果所报数字是第一个特殊数(3)的倍数,那么不能说该数字,而要说Fizz;如果所报数字是第二个特殊数(5)的倍数,那么要说Buzz;如果所报数字是第三个特殊数(7)的倍数,那么要说Whizz。
- 学生报数时,如果所报数字同时是两个特殊数的倍数情况下,也要特殊处理,比如第一个特殊数和第二个特殊数的倍数,那么不能说该数字,而是要说FizzBuzz, 以此类推。如果同时是三个特殊数的倍数,那么要说FizzBuzzWhizz。
- 学生报数时,如果所报数字包含了第一个特殊数,那么也不能说该数字,而是要说相应的单词,比如本例中第一个特殊数是3,那么要报13的同学应该说Fizz。如果数字中包含了第一个特殊数,那么忽略规则3和规则4,比如要报35的同学只报Fizz,不报BuzzWhizz。
- 否则,直接说出要报的数字。
DDD建模
该问题域只涉及一个BC(Bounded Context,限界上下文),我们先找UL(Ubiquitous language,通用语言)。
通用语言
- 题目中有三个数,我们假定为(n1, n2, n3),(3, 5, 7)是这三个数的一个例子。
- 原子操作记作atom,题目中有三个原子操作,分别为倍数times、包含contains和默认default。
- 一个数如果是ni(i=1,2,3)的倍数,我们记作times_ni,如果包含ni,我们记作contains_ni。
- 每个atom包含两部分,即匹配器和执行器二元组,记作(matcher, Action),那么针对三个原子操作,就有(matcher_times_ni, action_times_ni)、(matcher_contains_ni, action_contains_ni)和(matcher_default, action_default)
- 题目中有多个规则rule,atom是基本的rule,rule可以组合成新rule,组合可以是“与”的关系allof,也可以是“或”的关系anyof。
语义模型
我们将rule简写为r,使用UL形式化表达一下问题域:
r1_n1 = atom(matcher_times_n1, action_times_n1) -> (true, "Fizz") | (false, "")
r1_n2 = atom(matcher_times_n2, action_times_n2) -> (true, "Buzz") | (false, "")
r1_n3 = atom(matcher_times_n2, action_times_n2) -> (true, "Whizz") | (false, "")
r1 = allof(r1_n1, r1_n2, r1_n3)
r2 = atom(matcher_contains_n1, action_contains_n1) -> (true, "Fizz") | (false, "")
rd = atom(matcher_default, action_default) -> "num"
spec = anyof(r2, r1, rd)
从上面的形式化描述,可以很容易地得到FizzBuzzWhizz问题的语义模型:
rule: int -> string
matcher: int -> bool
action: int -> string
其中rule存在三种基本类型:
rule: atom | allof | anyof
三者之间构成了树型结构:
atom: (matcher, action) -> string
allof: rule1 && rule2 && ... && rulen
anyof: rule1 || rule2 || ... || rulen
领域模型
先看core domain的模型图:
然后是matcher domain的模型图:
最后是action domain的模型图:
代码实现
测试用例
笔者的xUnit工具使用的是刘光聪同学的作品cut,感兴趣的同学可以从github上直接下载 :)
FIXTURE(FizzBuzzWhizzSpec)
{
Game* game;
SETUP()
{
game = new Game(3, 5, 7);
}
TEARDOWN()
{
delete game;
}
void rule(int num, const std::string& expect)
{
ASSERT_THAT(game->saying(num), eq(expect));
}
TEST("fizz buzz whizz")
{
rule(3, "Fizz");
rule(5, "Buzz");
rule(7, "Whizz");
rule(3 * 5, "FizzBuzz");
rule(3 * 7, "FizzWhizz");
rule(5 * 7 /* 35 */, "Fizz");
rule(5 * 7 * 2, "BuzzWhizz");
rule(3 * 5 * 7, "FizzBuzzWhizz");
rule(13,"Fizz");
rule(11, "11");
}
DSL
r1_n1 = new Atom(matcher_times_n1, action_times_n1);
r1_n2 = new Atom(matcher_times_n2, action_times_n2);
r1_n3 = new Atom(matcher_times_n3, action_times_n3);
r1 = new AllOf({r1_n1, r1_n2, r1_n3});
r2 = new Atom(matcher_contains_n1, action_contains_n1);
rd = new Atom(matcher_default, action_default);
spec = new AnyOf({r2, r1, rd});
细心的读者会发现,DSL和语义模型一节中的形式化表达完全一致 :)
Rule
我们先看Interface:
struct Rule
{
virtual std::string apply(int num) = 0;
virtual ~Rule() = default;
};
Atom的实现:
//Atom.h
struct Atom : Rule
{
Atom(Matcher* matcher, Action* action);
virtual std::string apply(int num) override;
private:
Matcher* matcher;
Action* action;
};
//Atom.cpp
Atom::Atom(Matcher* matcher, Action* action)
: matcher(matcher), action(action)
{
}
std::string Atom::apply(int num)
{
if (matcher->match(num)) return action->exec(num);
return "";
}
AllOf的实现:
//AllOf.h
struct AllOf : Rule
{
AllOf(const std::vector& group);
virtual std::string apply(int num) override;
private:
std::vector group;
};
//AllOf.cpp
AllOf::AllOf(const std::vector& group)
: group(group)
{
}
std::string AllOf::apply(int num)
{
std::string output;
for (Rule* p : group)
{
output += p->apply(num);
}
return output;
}
AnyOf的实现和AllOf类似,不同的是apply函数实现时会短路,不再赘述。
Mather
我们先看Interface:
struct Matcher
{
virtual bool match(int num) = 0;
virtual ~Matcher() = default;
};
Matcher子类的实现都很简单,我们以TimersMatcher为例介绍一下:
//TimesMatcher.h
struct TimesMatcher : Matcher
{
TimesMatcher(int base);
virtual bool match(int num) override;
private:
int base;
};
//TimesMatcher.cpp
TimesMatcher::TimesMatcher(int base)
: base(base)
{
}
bool TimesMatcher::match(int num)
{
return num % base == 0;
}
Action
我们先看Interface:
struct Action
{
virtual std::string exec(int num) = 0;
virtual ~Action() = default;
};
Action子类的实现都很简单,我们以DefaultAction为例介绍一下:
//DefaultAction.h
struct DefaultAction : Action
{
virtual std::string exec(int num) override;
};
//DefaultAction.cpp
std::string DefaultAction::exec(int num)
{
return toString(num);
}
其中toString接口由基础实施提供。
小结
FizzBuzzWhizz问题本身并不复杂,本文给出了一种解决思路,即通过寻找通用语言、分析语义模型和建立领域模型等三个步骤来完成DDD建模,然后使用基本的C++语法对语义模型和领域模型进行了实现,其中语义模型对应DSL层,领域模型对应Domain层,希望DDD建模的步骤和代码实现的思路对读者有一定的价值。