報數遊戲-實戰簡單設計

注:手機推薦橫屏觀看:-)

幼兒園老師在給一羣小朋友玩報數遊戲,遊戲規則如下:

  • 老師給定任意三個特殊個位數:3,5,7
  • 總共120位小朋友排成一排順序報數

需求1:
a. 所報數字是第一個特殊數的倍數(本例爲3),不能說該數字,說“石頭”;
b. 所報數字是第二個特殊數的倍數(本例爲5),不能說該數字,說“剪刀”;
c. 所報數字是第三個特殊數的倍數(本例爲7),不能說該數字,說“布”;
d. 如果所報數字同時是兩個特殊數的倍數情況下,也要特殊處理,比如是3和5的倍數,那麼不能說該數字,而是要說“石頭剪刀”, 以此類推。
e. 如果同時是三個特殊數的倍數,那麼要說“石頭剪刀布”

Sprint 1

快速瀏覽題目,從中識別出關鍵字“報數遊戲”,“特殊數:3,5,7”,“120,順序報數”。採用TDD方式,先驅動出接口。
第一個測試用例:

#include "gtest/gtest.h"
#include "CountOffGame.h"

struct GameTest : testing::Test
{
protected:
    CountOffGame game;
};

TEST_F(GameTest, should_count_off_given_special_num_3_5_7)
{
    ASSERT_EQ("1", game.shout(1));
}

從解決編譯問題開始,快速通過測試:

//CountOffGame.h
#include <string>

struct CountOffGame
{
    std::string shout(int n) const;
};
//CountOffGame.cpp
#include "CountOffGame.h"

std::string CountOffGame::shout(int n) const
{
    return "1";
}

至此,我們已經驅動出用戶接口,通過第一個用例。

接下來,繼續增加第二個用例:

注:簡單起見,本例不對測試用例進行拆分,請大家按照 F.I.R.S.T. 原則及 given-when-then方式自行編寫測試用例 :-)

TEST_F(GameTest, should_count_off_given_special_num_3_5_7)
{
    ASSERT_EQ("1", game.shout(1));
    ASSERT_EQ("2", game.shout(2));
}

修改實現,老老實實把數字轉換爲字符串,通過第二個用例。

//CountOffGame.cpp
#include "CountOffGame.h"

std::string CountOffGame::shout(int n) const
{
    return std::to_string(n);
}

接下來我們該處理需求1-a了:

所報數字是第一個特殊數的倍數(本例爲3),不能說該數字,說“石頭”

TEST_F(GameTest, should_count_off_given_special_num_3_5_7)
{
    ASSERT_EQ("1", game.shout(1));
    ASSERT_EQ("2", game.shout(2));
    ASSERT_EQ("石頭", game.shout(3));
}

運行測試,校驗失敗,繼續完成需求1-a。這個難不倒我們,求某個數的倍數,用%即可。

std::string CountOffGame::shout(int n) const
{
    if(n % 3 == 0) return "石頭";
    return std::to_string(n);
}

繼續完成需求1-b:

TEST_F(GameTest, should_count_off_given_special_num_3_5_7)
{
    ASSERT_EQ("1", game.shout(1));
    ASSERT_EQ("2", game.shout(2));
    ASSERT_EQ("石頭", game.shout(3));
    ASSERT_EQ("剪刀", game.shout(5));
}
std::string CountOffGame::shout(int n) const
{
    if(n % 3 == 0) return "石頭";
    if(n % 5 == 0) return "剪刀";
    return std::to_string(n);
}

至此,我們已經完成了需求1-a,需求1-b,但敏銳的你一定發現了不少壞味道:

  1. 業務代碼中需求1-a,需求1-b的實現存在明顯重複
  2. 測試用例中,game.shout(...)存在明顯重複
  3. 用戶APIshout()命名也不太合理

我們先忍忍,繼續完成需求1-c,因爲它並不會帶來新的變化方向

TEST_F(GameTest, should_count_off_given_special_num_3_5_7)
{
    ASSERT_EQ("1", game.shout(1));
    ASSERT_EQ("2", game.shout(2));
    ASSERT_EQ("石頭", game.shout(3));
    ASSERT_EQ("剪刀", game.shout(5));
    ASSERT_EQ("布", game.shout(7));
}
std::string CountOffGame::shout(int n) const
{
    if(n % 3 == 0) return "石頭";
    if(n % 5 == 0) return "剪刀";
    if(n % 7 == 0) return "布";
    return std::to_string(n);
}

至此,我們已經完成需求1-a,b,c,接下來要開啓Refactor模式了

  • 重命名shout(int n)接口
//CountOffGame.h
struct CountOffGame
{
    std::string countOff(int n) const;
};

//CountOffGame.cpp
std::string CountOffGame::countOff(int n) const
{
   ...
}
  • 消除測試用例接口重複
struct GameTest : testing::Test
{
    void ASSERT_COUNTOFF_GAME(int n, const std::string& words)
    {
        ASSERT_EQ(words, game.countOff(n));
    }

protected:
    CountOffGame game;
};

TEST_F(GameTest, should_count_off_given_special_num_3_5_7)
{
    ASSERT_COUNTOFF_GAME(1, "1");
    ASSERT_COUNTOFF_GAME(2, "2");
    ASSERT_COUNTOFF_GAME(3, "石頭");
    ASSERT_COUNTOFF_GAME(5, "剪刀");
    ASSERT_COUNTOFF_GAME(7, "布");
}
  • 消除業務代碼中重複,提煉出倍數概念
namespace
{
    bool times(int n, int times)
    {
        return n % times == 0;
    }
}

std::string CountOffGame::countOff(int n) const
{
    if(times(n, 3)) return "石頭";
    if(times(n, 5)) return "剪刀";
    if(times(n, 7)) return "布";
    return std::to_string(n);
}
  • 消除報數“石頭,剪刀,布”硬編碼
namespace
{
    ...

    std::string shout(int n, const std::string& words = "")
    {
        if(!words.empty()) return words;
        return std::to_string(n);
    }
}

std::string CountOffGame::countOff(int n) const
{
    if(times(n, 3)) return shout(n, "石頭");
    if(times(n, 5)) return shout(n, "剪刀");
    if(times(n, 7)) return shout(n, "布");
    return shout(n);
}

至此,我們已經消除了明顯的重複,但是依然存在結構性重複。即每個語句都是報數時遵循的一個規則,並且都是if(謂詞) return 動作結構。
我們對其進行抽象,進一步分析發現滿足如下形式化定義:

Rule: (int) -> string
Predicate: (int) -> bool
Action: (int) -> string

我們從武器庫中快速搜尋着各種解決方案,比較着他們的利弊。

  1. 抽象出PredicateAction接口類,分別提供bool isMatch(int n) conststd::string do(int n) const虛方法;抽象出Rule類,注入PredicateAction接口
  2. 定義類模板Rule,綁定謂詞與動作,要求謂詞滿足isMatch(int n),動作滿足do(int n)約束
  3. 定義兩個函數指針,用於抽象約束關係,使用函數模板rule將其綁定

綜合考慮後,發現方案3在簡單性和擴展性方面是最合適的

//CountOffGame.cpp
namespace
{
    ...
    typedef bool (*Predicate)(int, int);
    typedef std::string (*Action)(int, const std::string& );

    template<Predicate isMatch, Action do>
    std::string rule(int n, int times, const std::string& words)
    {
        if(isMatch(n, times))
        {
            return do(n, words);
        }

        return std::string("");
    }
}

std::string CountOffGame::countOff(int n) const
{
    const std::string& r1_1 = rule<times, shout>(n, 3, "石頭");
    if( ! r1_1.empty()) return r1_1;
    const std::string& r1_2 = rule<times, shout>(n, 5, "剪刀");
    if( ! r1_2.empty()) return r1_2;
    const std::string& r1_3 = rule<times, shout>(n, 7, "布");
    if( ! r1_3.empty()) return r1_3;

    return shout(n);
}

舊仇剛報,又添新恨,此時又出現了新的結構性重複,抽象一下就是:報數規則有一個滿足,就執行該規則並返回,否則繼續執行下一個規則,語義爲爲anyof(...)。我們暫且忍受一下,讓子彈飛一會,繼續完成下面需求。
增加需求1-d,需求1-e測試用例:

TEST_F(GameTest, should_count_off_given_special_num_3_5_7)
{
    ASSERT_COUNTOFF_GAME(1,   "1");
    ASSERT_COUNTOFF_GAME(2,   "2");
    ASSERT_COUNTOFF_GAME(3,   "石頭");
    ASSERT_COUNTOFF_GAME(5,   "剪刀");
    ASSERT_COUNTOFF_GAME(7,   "布");
    ASSERT_COUNTOFF_GAME(15,  "石頭剪刀");
    ASSERT_COUNTOFF_GAME(21,  "石頭布");
    ASSERT_COUNTOFF_GAME(35,  "剪刀布");
    ASSERT_COUNTOFF_GAME(105, "石頭剪刀布");
}

我們仔細分析發現需求1-d,需求1-e是對上面需求的組合,即如果規則滿足則執行該規則,完成後繼續執行下一個規則,若不滿足則直接執行下一個規則。語義爲allof(...)。本例中表現爲把所有規則結果串起來。

std::string CountOffGame::countOff(int n) const
{
    const std::string& r1 = rule<times, shout>(n, 3, "石頭") ;
                          + rule<times, shout>(n, 5, "剪刀");
                          + rule<times, shout>(n, 7, "布");
    if( ! r1.empty()) return r1;

    return shout(n);
}

至此,我們全部完成了需求1所有規則,由於需求1-d,e比需求1-a,b,c優先級高,allof()隱式滿足了anyof()需求,所以結構性重複暫時不存在了。


Sprint 2

需求 2:
a. 如果所報數字中包含了第一個特殊數,那麼也不能說該數字,而是要說相應的詞,比如本例中第一個特殊數是3,那麼要報13的同學應該說“石頭”。
b. 如果所報數字中包含了第一個特殊數,那麼忽略規則1,比如要報35的同學只報“石頭”,不報“剪刀布”

增加需求2測試用例:

TEST_F(GameTest, should_count_off_given_special_num_3_5_7)
{
    ...

    ASSERT_COUNTOFF_GAME(13,   "石頭");
    ASSERT_COUNTOFF_GAME(35,   "石頭");
}

有了需求1的抽象,需求2就比較簡單了,僅需要增加一個謂詞即可

namespace
{
    ...
    bool contains(int n, int contained)
    {
        int units = 0;
        while(n > 0)
        {
            units = n % 10;
            n = n /10;
            if(units == contained) return true;
        }

        return false;
    }
    ...
}

std::string CountOffGame::countOff(int n) const
{
    const std::string& r2 = rule<contains, shout>(n, 3, "石頭");
    if(!r2.empty()) return r2;

    const std::string& r1 = rule<times, shout>(n, 3, "石頭");
                          + rule<times, shout>(n, 5, "剪刀");
                          + rule<times, shout>(n, 7, "布");
    if( ! r1.empty()) return r1;

    return shout(n);
}

return shout(n);是什麼鬼,我們進一步分析,將它抽象成一個規則

namespace
{
    ...
    bool always_true(int,int)
    {
        return true;
    }

    std::string shout(int, const std::string& words)
    {
        return words;
    }

    std::string nop(int n, const std::string&)
    {
        return std::to_string(n);
    }
    ...
}

std::string CountOffGame::countOff(int n) const
{
    const std::string& r2 = rule<contains, shout>(n, 3, "石頭");
    if(!r2.empty()) return r2;

    const std::string& r1 = rule<times, shout>(n, 3, "石頭");
                          + rule<times, shout>(n, 5, "剪刀");
                          + rule<times, shout>(n, 7, "布");
    if( ! r1.empty()) return r1;

    return rule<always_true, nop>(n, 0, "");
}

但是我們討厭的結構性重複又來了,這不就是前面抽象的anyof(...)麼,看來不得不收拾他們了。
解決這個問題之前,我們先把物理耦合處理下,CountOffGame.cpp文件的職責已經太多了

//Predicate.h
bool times(int n, int times);
bool contains(int n, int contained);
bool always_true(int,int);

//Predicate.cpp
#include "Predicate.h"

bool times(int n, int times)
{
    return n % times == 0;
}

bool contains(int n, int contained)
{
    int units = 0;
    while(n > 0)
    {
        units = n % 10;
        n = n /10;
        if(units == contained) return true;
    }

    return false;
}

bool always_true(int,int)
{
    return true;
}
//Action.h
#include <string>

std::string shout(int n, const std::string& words);
std::string nop(int n, const std::string&);

//Action.cpp
#include "Action.h"

std::string shout(int, const std::string& words)
{
    return words;
}

std::string nop(int n, const std::string&)
{
    return std::to_string(n);
}
//Rule.h
#include <string>

typedef bool (*Predicate)(int, int);
typedef std::string (*Action)(int, const std::string& );

template<Predicate isMatch, Action do>
std::string rule(int n, int times, const std::string& words)
{
    if(isMatch(n, times))
    {
        return do(n, words);
    }

    return std::string("");
}

CountOffGame.cpp文件中僅剩下組合遊戲規則,完成遊戲了

std::string CountOffGame::countOff(int n) const
{
    const std::string& r2 = rule<contains, shout>(n, 3, "石頭");
    if(!r2.empty()) return r2;

    const std::string& r1 = rule<times, shout>(n, 3, "石頭");
                          + rule<times, shout>(n, 5, "剪刀");
                          + rule<times, shout>(n, 7, "布");
    if( ! r1.empty()) return r1;

    return shout(n);
}

注:有同學不知道CountOffGame類與Rule的區別,這裏解釋一下,規則僅完成每個規則自己的形式化與定義,遊戲中對規則進行組合,完成遊戲

完成文件物理解耦後,世界一下子清靜多了,需要特別指出的是,在使用c語言進行編程時,由於沒有類的模塊化手段,在解耦方面,文件是留給我們的唯一利器了。

我們回過頭來,再看看前面抽象的anyof(...),allof(...)概念,形式化一下可以表示爲:

Rule: (int) -> string
allof: rule1 && rule2 ...
anyof: rule1 || rule2 ...

這顯然是規則的組合關係管理,我們又陷入了深深的思索,使用什麼方法解決這個問題呢?
繼續搜索武器庫:

  1. 使用面向對象方法,抽象出rule對應接口,使用組合模式解決該問題
  2. 使用類模板,將不同rule通過模板參數注入,需要使用變參模板
  3. 使用函數模板,將不同rule通過模板參數注入,需要使用變參函數模板
  4. 使用函數式編程,可以使用std::function定義每個rule

綜合比較以上方案,

  • 面向對象設計,組合模式中集合類要求每個成員rule不可以異構,並且僅能存放指向rule的指針,即要求每個rule都是一個對象。我們就需要管理每個對象的生命週期,需要使用shared_ptr,或者使用c++11 std::move語義,延長臨時對象生命週期。越想越複雜,打住!
  • 模板元編程,需要使用c++11變參模板,或者使用repeate宏了(參看boost庫),
  • 函數模板,需要使用c++11變參函數模板,使用尾遞歸完成組合,直接注入規則即可
  • 函數式編程,形式化表示規則,天生具有優勢,又加之函數的無狀態,組合起來很方便,可以結合右值引用及std::move完成,考慮熟悉同學較少,暫不考慮

綜合分析後,我們選方案3

//Rule.h
...
template<typename Head>
Head allof(Head head)
{
    return head;
}
template<typename Head, typename... Tail>
Head allof(Head head, Tail... tail)
{
    return head + allof<Head>(tail...);
}

template<typename Head>
Head anyof(Head head)
{
    if(!head.empty()) return head;
    return std::string("");
}

template<typename Head, typename... Tail>
Head anyof(Head head, Tail... tail)
{
    if(!head.empty()) return head;
    return anyof<Head>(tail...);
}
//CountOffGame.cpp

#include "CountOffGame.h"
#include "Rule.h"
#include "Predicate.h"
#include "Action.h"

std::string CountOffGame::countOff(int n) const
{
    auto r1_1 = rule<times, shout>(n, 3, "石頭");
    auto r1_2 = rule<times, shout>(n, 5, "剪刀");
    auto r1_3 = rule<times, shout>(n, 7, "布");
    auto r1 = allof(r1_1, r1_2, r1_3);

    auto r2 = rule<contains, shout>(n, 3, "石頭");
    auto rd = rule<always_true, nop>(n, 0, "");

    return anyof(r2, r1, rd);
}

現在我們徹底解決了規則管理問題,你可以任意調整優先級了,也可以任意忽略規則了。在整個設計過程中,我們發現重複總是我們識別壞味道最好的引子。當變化方向來的時候,也就是我們被第二顆子彈擊中的時候(好吧,有時候是第三顆子彈),我們需要把該方向的所有問題解決,而不是僅解決該問題。

注: 坦白講,這裏的CountOffGame類使用的完全沒有必要,你完全可以使用一個簡單的 const char* count_off(int n)函數代替,再把std::string使用char*取代,這樣就完全是一份c的代碼了:-)


Sprint 3

需求 3:
a. 第二天,幼兒園換了新老師,新老師對遊戲進行了修改
三個特殊的個位數變更爲5,7,9。規則1,規則2依然有效,例如:
遇到 5 的倍數說“石頭”,7的倍數說“剪刀”,9的倍數說“布”;
遇到 63 說“剪刀布”
遇到 53 說“石頭”,遇到35說“石頭”
b. 需求1,2 測試用例斷言不允許修改,僅允許修改前置條件

需求3已經說的很明顯了,三個特殊數可以發生變化,並且需要通過遊戲注入,而不需要修改遊戲規則

簡單重構測試用例斷言,增加需求3用例

#include "gtest/gtest.h"
#include "CountOffGame.h"

struct GameTest : testing::Test
{
    GameTest() : game_sprint_1_2(3, 5, 7)
               , game_sprint_3(5, 7, 9)
    {
    }

    void ASSERT_COUNTOFF_3_5_7(int n, const std::string& words)
    {
        ASSERT_EQ(words, game_sprint_1_2.countOff(n));
    }

    void ASSERT_COUNTOFF_5_7_9(int n, const std::string& words)
    {
        ASSERT_EQ(words, game_sprint_3.countOff(n));
    }

protected:
    CountOffGame game_sprint_1_2;
    CountOffGame game_sprint_3;
};

TEST_F(GameTest, should_count_off_given_special_num_3_5_7)
{
    ASSERT_COUNTOFF_3_5_7(1,   "1");
    ASSERT_COUNTOFF_3_5_7(2,   "2");
    ASSERT_COUNTOFF_3_5_7(3,   "石頭");
    ASSERT_COUNTOFF_3_5_7(5,   "剪刀");
    ASSERT_COUNTOFF_3_5_7(7,   "布");
    ASSERT_COUNTOFF_3_5_7(15,  "石頭剪刀");
    ASSERT_COUNTOFF_3_5_7(21,  "石頭布");
    ASSERT_COUNTOFF_3_5_7(105, "石頭剪刀布");

    ASSERT_COUNTOFF_3_5_7(13,   "石頭");
    ASSERT_COUNTOFF_3_5_7(35,   "石頭");
}

TEST_F(GameTest, should_count_off_given_special_num_5_7_9)
{
    ASSERT_COUNTOFF_5_7_9(1,   "1");
    ASSERT_COUNTOFF_5_7_9(2,   "2");
    ASSERT_COUNTOFF_5_7_9(5,   "石頭");
    ASSERT_COUNTOFF_5_7_9(7,   "剪刀");
    ASSERT_COUNTOFF_5_7_9(9,   "布");
    ASSERT_COUNTOFF_5_7_9(63,  "剪刀布");

    ASSERT_COUNTOFF_5_7_9(15,  "石頭");
    ASSERT_COUNTOFF_5_7_9(35,  "石頭");
}

快速完成需求3,僅是規則中Predicate匹配內容發生了變化,修改CountOffGame即可

//CountOffGame.h
#include <string>

struct CountOffGame
{
    CountOffGame(int n1, int n2, int n3);
    std::string countOff(int n) const;

private:
    int n1;
    int n2;
    int n3;
};

//CountOffGame.cpp
CountOffGame::CountOffGame(int n1, int n2, int n3) : n1(n1), n2(n2), n3(n3)
{
}

std::string CountOffGame::countOff(int n) const
{
    auto r1_1 = rule<times, shout>(n, n1, "石頭");
    auto r1_2 = rule<times, shout>(n, n2, "剪刀");
    auto r1_3 = rule<times, shout>(n, n3, "布");
    auto r1 = allof(r1_1, r1_2, r1_3);

    auto r2 = rule<contains, shout>(n, n1, "石頭");
    auto rd = rule<always_true, nop>(n, 0, "");

    return anyof(r2, r1, rd);
}

繼續...


Sprint 4

需求 4:
第三天,原來老師又回來了,對遊戲又做了如下修改
a. 老師又把三個特殊的個位數變回爲3,5,7
b. 規則1與規則2中“石頭、剪刀、布”變更爲 “老虎,棒子,雞”。
c. 打印120位小朋友報數的結果到文件(count_off.txt),並提交到 document 文件夾下。可以不處理IO,打印出來拷貝到 count_off.txt

這壓根沒引入什麼新的變化方向,僅是規則中Action的內容發生了變化,修改CountOffGame即可,我們愛死這個老師了!

增加需求4測試用例:

struct GameTest : testing::Test
{
    GameTest() : game_sprint_1_2(3, 5, 7, "石頭", "剪刀", "布")
               , game_sprint_3(5, 7, 9, "石頭", "剪刀", "布")
               , game_sprint_4(3, 5, 7, "老虎", "棒子", "雞")
    {
    }

    void ASSERT_COUNTOFF_3_5_7(int n, const std::string& words)
    {
        ASSERT_COUNTOFF(game_sprint_1_2, n, words);
    }

    void ASSERT_COUNTOFF_5_7_9(int n, const std::string& words)
    {
        ASSERT_COUNTOFF(game_sprint_3, n, words);
    }

    void ASSERT_COUNTOFF_3_5_7_EX(int n, const std::string& words)
    {
        ASSERT_COUNTOFF(game_sprint_4, n, words);
    }

private:
    void ASSERT_COUNTOFF(const CountOffGame& game, int n, const std::string& words)
    {
        ASSERT_EQ(words, game.countOff(n));
    }

protected:
    CountOffGame game_sprint_1_2;
    CountOffGame game_sprint_3;
    CountOffGame game_sprint_4;
};

TEST_F(GameTest, should_count_off_given_special_num_3_5_7)
{
    ASSERT_COUNTOFF_3_5_7(1,   "1");
    ASSERT_COUNTOFF_3_5_7(2,   "2");
    ASSERT_COUNTOFF_3_5_7(3,   "石頭");
    ASSERT_COUNTOFF_3_5_7(5,   "剪刀");
    ASSERT_COUNTOFF_3_5_7(7,   "布");
    ASSERT_COUNTOFF_3_5_7(15,  "石頭剪刀");
    ASSERT_COUNTOFF_3_5_7(21,  "石頭布");
    ASSERT_COUNTOFF_3_5_7(105, "石頭剪刀布");

    ASSERT_COUNTOFF_3_5_7(13,   "石頭");
    ASSERT_COUNTOFF_3_5_7(35,   "石頭");
}

TEST_F(GameTest, should_count_off_given_special_num_5_7_9)
{
    ASSERT_COUNTOFF_5_7_9(1,   "1");
    ASSERT_COUNTOFF_5_7_9(2,   "2");
    ASSERT_COUNTOFF_5_7_9(5,   "石頭");
    ASSERT_COUNTOFF_5_7_9(7,   "剪刀");
    ASSERT_COUNTOFF_5_7_9(9,   "布");
    ASSERT_COUNTOFF_5_7_9(63,  "剪刀布");

    ASSERT_COUNTOFF_5_7_9(15,  "石頭");
    ASSERT_COUNTOFF_5_7_9(35,  "石頭");
}
TEST_F(GameTest, should_count_off_given_special_num_3_5_7_countof_other)
{
    ASSERT_COUNTOFF_3_5_7_EX(1,   "1");
    ASSERT_COUNTOFF_3_5_7_EX(2,   "2");
    ASSERT_COUNTOFF_3_5_7_EX(3,   "老虎");
    ASSERT_COUNTOFF_3_5_7_EX(5,   "棒子");
    ASSERT_COUNTOFF_3_5_7_EX(7,   "雞");
    ASSERT_COUNTOFF_3_5_7_EX(15,  "老虎棒子");
    ASSERT_COUNTOFF_3_5_7_EX(21,  "老虎雞");
    ASSERT_COUNTOFF_3_5_7_EX(105, "老虎棒子雞");

    ASSERT_COUNTOFF_3_5_7_EX(13,  "老虎");
    ASSERT_COUNTOFF_3_5_7_EX(35,  "老虎");
}

快速實現需求

//CountOffGame.h
#include <string>

struct CountOffGame
{
    CountOffGame(int n1, int n2, int n3, const std::string&, const std::string&, const std::string&);
    std::string countOff(int n) const;

private:
    int n1;
    int n2;
    int n3;
    const std::string w1;
    const std::string w2;
    const std::string w3;
};

//CountOffGame.cpp
CountOffGame::CountOffGame(int n1, int n2, int n3, const std::string& w1, const std::string& w2, const std::string& w3)
    : n1(n1), n2(n2), n3(n3), w1(w1), w2(w2), w3(w3)
{
}

std::string CountOffGame::countOff(int n) const
{
    auto r1_1 = rule<times, shout>(n, n1, w1);
    auto r1_2 = rule<times, shout>(n, n2, w2);
    auto r1_3 = rule<times, shout>(n, n3, w3);
    auto r1 = allof(r1_1, r1_2, r1_3);

    auto r2 = rule<contains, shout>(n, n1, w1);
    auto rd = rule<always_true, nop>(n, 0, "");

    return anyof(r2, r1, rd);
}

雖然實現了需求,但這一長串初始化參數,搞的人都快吐了,
仔細分析一下,報數遊戲中規則條件及動作結果是一個一一映射關係,我們可以定義一個RuleMap進行化簡

//CountOffGame.h
#include <string>
#include <initializer_list>
#include <vector>

struct RuleMap
{
    int n;
    std::string words;
};

struct CountOffGame
{
    CountOffGame(const std::initializer_list<RuleMap>&);
    std::string countOff(int n) const;

private:
    std::vector<RuleMap> rules;
};

//CountOffGame.cpp
#include "CountOffGame.h"
#include "Rule.h"
#include "Predicate.h"
#include "Action.h"

CountOffGame::CountOffGame(const std::initializer_list<RuleMap>& rules) : rules(rules)
{
}

std::string CountOffGame::countOff(int n) const
{
    auto r1_1 = rule<times, shout>(n, rules[0].n, rules[0].words);
    auto r1_2 = rule<times, shout>(n, rules[1].n, rules[1].words);
    auto r1_3 = rule<times, shout>(n, rules[2].n, rules[2].words);
    auto r1 = allof(r1_1, r1_2, r1_3);

    auto r2 = rule<contains, shout>(n, rules[0].n, rules[0].words);
    auto rd = rule<always_true, nop>(n, 0, "");

    return anyof(r2, r1, rd);
}

每個規則中rules[...]又出現了重複,繼續消除

//rule.h
#include <string>

struct RuleMap
{
    int n;
    std::string words;
};

typedef bool (*Predicate)(int, int);
typedef std::string (*Action)(int, const std::string& );

template<Predicate isMatch, Action do>
std::string rule(int n, const RuleMap& map = {})
{
    if(isMatch(n, map.n))
    {
        return do(n, map.words);
    }

    return std::string("");
}
...
//CountOffGame.cpp
...
std::string CountOffGame::countOff(int n) const
{
    auto r1_1 = rule<times, shout>(n, rules[0]);
    auto r1_2 = rule<times, shout>(n, rules[1]);
    auto r1_3 = rule<times, shout>(n, rules[2]);
    auto r1 = allof(r1_1, r1_2, r1_3);

    auto r2 = rule<contains, shout>(n, rules[0]);
    auto rd = rule<always_true, nop>(n);

    return anyof(r2, r1, rd);
}

代碼地址:https://github.com/liyongshun/count-off

[email protected] @ November 26, 2017 11:31 PM

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章