序言
控制復(fù)雜性是計(jì)算機(jī)編程的本質(zhì)?!?Brian Kernighan
前幾天有幸參加了劉光聰同學(xué)組織的Code Retreat活動(dòng),收獲比較大,其中印象最深刻的是結(jié)對(duì)編程實(shí)踐。通過和幾位高手的結(jié)對(duì),開了眼界,他們不光設(shè)計(jì)能力超強(qiáng),而且是鍵盤俠,兩手一抹,數(shù)行高質(zhì)量代碼就躍然眼前,非常過癮。
在Code Retreat活動(dòng)后,筆者打算寫篇文章梳理一下學(xué)到的知識(shí),同時(shí)分享給大家。
FizzBuzzWhizz問題
FizzBuzzWhizz問題是某公司的面試題目,具體如下所示:
你是一名體育老師,在某次課距離下課還有五分鐘時(shí),你決定搞一個(gè)游戲。此時(shí)有100名學(xué)生在上課。游戲的規(guī)則是:
- 你首先說出三個(gè)不同的特殊數(shù),要求必須是個(gè)位數(shù),比如3、5、7。
- 讓所有學(xué)生拍成一隊(duì),然后按順序報(bào)數(shù)。
- 學(xué)生報(bào)數(shù)時(shí),如果所報(bào)數(shù)字是第一個(gè)特殊數(shù)(3)的倍數(shù),那么不能說該數(shù)字,而要說Fizz;如果所報(bào)數(shù)字是第二個(gè)特殊數(shù)(5)的倍數(shù),那么要說Buzz;如果所報(bào)數(shù)字是第三個(gè)特殊數(shù)(7)的倍數(shù),那么要說Whizz。
- 學(xué)生報(bào)數(shù)時(shí),如果所報(bào)數(shù)字同時(shí)是兩個(gè)特殊數(shù)的倍數(shù)情況下,也要特殊處理,比如第一個(gè)特殊數(shù)和第二個(gè)特殊數(shù)的倍數(shù),那么不能說該數(shù)字,而是要說FizzBuzz, 以此類推。如果同時(shí)是三個(gè)特殊數(shù)的倍數(shù),那么要說FizzBuzzWhizz。
- 學(xué)生報(bào)數(shù)時(shí),如果所報(bào)數(shù)字包含了第一個(gè)特殊數(shù),那么也不能說該數(shù)字,而是要說相應(yīng)的單詞,比如本例中第一個(gè)特殊數(shù)是3,那么要報(bào)13的同學(xué)應(yīng)該說Fizz。如果數(shù)字中包含了第一個(gè)特殊數(shù),那么忽略規(guī)則3和規(guī)則4,比如要報(bào)35的同學(xué)只報(bào)Fizz,不報(bào)BuzzWhizz。
- 否則,直接說出要報(bào)的數(shù)字。
DDD建模
該問題域只涉及一個(gè)BC(Bounded Context,限界上下文),我們先找UL(Ubiquitous language,通用語言)。
通用語言
- 題目中有三個(gè)數(shù),我們假定為(n1, n2, n3),(3, 5, 7)是這三個(gè)數(shù)的一個(gè)例子。
- 原子操作記作atom,題目中有三個(gè)原子操作,分別為倍數(shù)times、包含contains和默認(rèn)default。
- 一個(gè)數(shù)如果是ni(i=1,2,3)的倍數(shù),我們記作times_ni,如果包含ni,我們記作contains_ni。
- 每個(gè)atom包含兩部分,即匹配器和執(zhí)行器二元組,記作(matcher, Action),那么針對(duì)三個(gè)原子操作,就有(matcher_times_ni, action_times_ni)、(matcher_contains_ni, action_contains_ni)和(matcher_default, action_default)
- 題目中有多個(gè)規(guī)則rule,atom是基本的rule,rule可以組合成新rule,組合可以是“與”的關(guān)系allof,也可以是“或”的關(guān)系anyof。
語義模型
我們將rule簡(jiǎn)寫為r,使用UL形式化表達(dá)一下問題域:
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
三者之間構(gòu)成了樹型結(jié)構(gòu):
atom: (matcher, action) -> string
allof: rule1 && rule2 && ... && rulen
anyof: rule1 || rule2 || ... || rulen
領(lǐng)域模型
先看core domain的模型圖:

然后是matcher domain的模型圖:

最后是action domain的模型圖:

代碼實(shí)現(xiàn)
測(cè)試用例
筆者的xUnit工具使用的是劉光聰同學(xué)的作品cut,感興趣的同學(xué)可以從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});
細(xì)心的讀者會(huì)發(fā)現(xiàn),DSL和語義模型一節(jié)中的形式化表達(dá)完全一致 :)
Rule
我們先看Interface:
struct Rule
{
virtual std::string apply(int num) = 0;
virtual ~Rule() = default;
};
Atom的實(shí)現(xiàn):
//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的實(shí)現(xiàn):
//AllOf.h
struct AllOf : Rule
{
AllOf(const std::vector<Rule*>& group);
virtual std::string apply(int num) override;
private:
std::vector<Rule*> group;
};
//AllOf.cpp
AllOf::AllOf(const std::vector<Rule*>& group)
: group(group)
{
}
std::string AllOf::apply(int num)
{
std::string output;
for (Rule* p : group)
{
output += p->apply(num);
}
return output;
}
AnyOf的實(shí)現(xiàn)和AllOf類似,不同的是apply函數(shù)實(shí)現(xiàn)時(shí)會(huì)短路,不再贅述。
Mather
我們先看Interface:
struct Matcher
{
virtual bool match(int num) = 0;
virtual ~Matcher() = default;
};
Matcher子類的實(shí)現(xiàn)都很簡(jiǎn)單,我們以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子類的實(shí)現(xiàn)都很簡(jiǎn)單,我們以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接口由基礎(chǔ)實(shí)施提供。
小結(jié)
FizzBuzzWhizz問題本身并不復(fù)雜,本文給出了一種解決思路,即通過尋找通用語言、分析語義模型和建立領(lǐng)域模型等三個(gè)步驟來完成DDD建模,然后使用基本的C++語法對(duì)語義模型和領(lǐng)域模型進(jìn)行了實(shí)現(xiàn),其中語義模型對(duì)應(yīng)DSL層,領(lǐng)域模型對(duì)應(yīng)Domain層,希望DDD建模的步驟和代碼實(shí)現(xiàn)的思路對(duì)讀者有一定的價(jià)值。