C++11 模板元編程 - 構(gòu)建DSL


C++是一門非常適合用來構(gòu)建DSL(Domain Specific Language)的語言,它的多范式特點為它提供了豐富的工具,尤其是C++提供了:

  • 一個靜態(tài)類型系統(tǒng);
  • 近似于零抽象懲罰的能力(包括強大的優(yōu)化器);
  • 預(yù)處理宏,能夠以文本替換的方式操縱源代碼;
  • 一套豐富的內(nèi)建符號運算符,它們可以被重載,且對重載的語義幾乎沒有任何限制;
  • 一套圖靈完備的模板計算系統(tǒng)(模板元編程),可以用于:
    • 生成新的類型和函數(shù);
    • 在編譯期執(zhí)行任何計算;
    • 提供靜態(tài)反射的能力;

結(jié)合這些武器,使得通過C++構(gòu)建兼顧語法表達力及運行時效率的DSL成為了可能。可以說,模板元編程是上述所有武器中最為重要的,接下來我們使用模板元編程設(shè)計一個用于描述有限狀態(tài)機(FSM)的DSL。該設(shè)計最初來自于《C++模板元編程》一書,由于作者在書中所舉狀態(tài)機的例子比較晦澀,而且實現(xiàn)使用了更晦澀的boost mpl庫,為了讓這個設(shè)計更加易懂并且代碼更加清晰,我對例子和代碼進行了重新設(shè)計。

有限狀態(tài)機(FSM)是計算機編程中非常有用的工具,它通過抽象將紊亂的程序邏輯轉(zhuǎn)換成更易于理解的形式化的表達形式。

有限狀態(tài)機的領(lǐng)域模型由三個簡單的元素構(gòu)成:

  • 狀態(tài)(state):FSM某一時刻總是處于一個狀態(tài)中,不同狀態(tài)決定了FSM可響應(yīng)的事件類型以及響應(yīng)的方式。

  • 事件(event):事件觸發(fā)FSM狀態(tài)的改變,事件可以攜帶具體信息。

  • 轉(zhuǎn)換(transition):一個轉(zhuǎn)換標(biāo)記了在某個事件的激勵下FSM從一個狀態(tài)到另一個狀態(tài)的躍遷。通常轉(zhuǎn)換還會有一個關(guān)聯(lián)動作(action),表示在狀態(tài)躍遷時進行的操作。將所有的轉(zhuǎn)換放在一起可以構(gòu)成一個狀態(tài)轉(zhuǎn)換表(State Transition Table,STT)。

我們假設(shè)有一個跳舞機器人,它的狀態(tài)轉(zhuǎn)換關(guān)系如下圖:

圖可能是表示FSM最直觀的工具了,但是如果圖中出現(xiàn)太多的細節(jié)就會導(dǎo)致很凌亂,例如上圖為了簡潔就沒有標(biāo)示每個轉(zhuǎn)換對應(yīng)的action。為了讓FSM的表示更加形式化,我們將其裝換成如下表格的形式:

Current State Event Next State Action
closed open opened sayReady
opened close closed sayClosed
opened play dancing doDance
dancing stop opened sayStoped
dancing close closed sayClosed

如上,對于跳舞機器人,它有三種狀態(tài):closed,opened,dancing;它可以接收四種事件:close,open,play,stop;它有四個action:sayReady,sayClosed,doDance,sayStoped。上表中的每一行表示了一種可以進行的轉(zhuǎn)換關(guān)系。用表格來表示FSM同樣易于理解,而且這種表示是相對形式化的,且容易通過代碼來描述。

對于這樣一個由FSM表示的跳舞機器人,最常見的實現(xiàn)如下:

// Events
struct Close {};
struct Open {};
struct Play
{
    std::string name;
};
struct Stop {};

// FSM
struct DanceRobot
{
    void processEvent(const Open& event)
    {
        if(state == closed)
        {
            sayReady(event);
            state = opened;
        }
        else
        {
            reportError(event);
        }
    }

    void processEvent(const Close& event)
    {
        if(state == opened)
        {
            sayClosed(event);
            state = closed;
        }
        else if(state == dancing)
        {
            sayClosed(event);
            state = closed;
        }
        else
        {
            reportError(event);
        }
    }

    void processEvent(const Play& event)
    {
        if(state == opened)
        {
            doDance(event);
            state = dancing;
        }
        else
        {
            reportError(event);
        }
    }

    void processEvent(const Stop& event)
    {
        if(state == dancing)
        {
            sayStoped(event);
            state = opened;
        }
        else
        {
            reportError(event);
        }
    }

private:
    // Actions
    void sayReady(const Open&)
    {
        std::cout << "Robot is ready for play!" << std::endl;
    }

    void sayClosed(const Close&)
    {
        std::cout << "Robot is closed!" << std::endl;
    }

    void sayStoped(const Stop&)
    {
        std::cout << "Robot stops playing!" << std::endl;
    }

    void doDance(const Play& playInfo)
    {
        std::cout << "Robot is dancing (" << playInfo.name << ") now!" << std::endl;
    }

    template<typename Event>
    void reportError(Event& event)
    {
        std::cout << "Error: robot on state(" << state
                  << ") receives unknown event( " 
                  << typeid(event).name() << " )" << std::endl;
    }

private:
    // States
    enum
    {
        closed, opened, dancing, initial = closed

    }state{initial};
};
int main()
{
    DanceRobot robot;

    robot.processEvent(Open());
    robot.processEvent(Close());
    robot.processEvent(Open());
    robot.processEvent(Play{.name = "hip-hop"});
    robot.processEvent(Stop());
    robot.processEvent(Close());

    robot.processEvent(Stop()); // Invoke error

    return 0;
}

上面的代碼中為了簡化只有Play事件攜帶了消息內(nèi)容。Robot通過函數(shù)重載實現(xiàn)了processEvent方法,用于處理不同的消息。reportError用于在某狀態(tài)下收到不能處理的消息時調(diào)用,它會打印出當(dāng)前狀態(tài)以及調(diào)用運行時RTTI技術(shù)打印出消息類名稱。

通過代碼可以看到,上面的實現(xiàn)將整個有限狀態(tài)機的狀態(tài)關(guān)系散落在每個消息處理函數(shù)的if-else語句中,我們必須通過仔細分析代碼邏輯關(guān)系才能再還原出狀態(tài)機的全貌。當(dāng)狀態(tài)機的狀態(tài)或者轉(zhuǎn)換關(guān)系發(fā)生變化時,我們必須非常小心地審查每個消息處理函數(shù),以保證修改不出錯。而且當(dāng)狀態(tài)和事件變多的時候,每個函數(shù)的if-else層數(shù)將會變得更深。

如果你精通設(shè)計模式,可能會采用狀態(tài)模式改寫上面的代碼。狀態(tài)模式為每個狀態(tài)建立一個子類,將不同狀態(tài)下的消息處理函數(shù)分開。這樣當(dāng)我們修改某一狀態(tài)的實現(xiàn)細節(jié)時就不會干擾到別的狀態(tài)的實現(xiàn)。狀態(tài)模式讓每個狀態(tài)的處理內(nèi)聚在自己的狀態(tài)類里面,讓修改變得隔離,減少了出錯的可能。但是狀態(tài)模式的問題在于將狀態(tài)拆分到多個類中,導(dǎo)致一個完整的FSM的實現(xiàn)被分割到多處,難以看到一個狀態(tài)機的全貌。我們必須在多個狀態(tài)類之間跳轉(zhuǎn)才能搞明白整個FSM的狀態(tài)關(guān)系。而且由于采用了虛函數(shù),這阻止了一定可能上的編譯期優(yōu)化,會造成一定的性能損失。

有經(jīng)驗的C程序員說可以采用表驅(qū)動法來實現(xiàn),這樣就可以避免那么多的if-else或者子類。表可以將狀態(tài)機的關(guān)系內(nèi)聚在一起,從而展示整個FSM的全貌。表是用代碼表示FSM非常好的一個工具,可惜C語言的表驅(qū)動需要借助函數(shù)指針,它和虛函數(shù)本質(zhì)上一樣,都會導(dǎo)致編譯器放棄很多優(yōu)化,性能都沒有第一種實現(xiàn)高。

那么有沒有一種方法,讓我們可以以表來表示整個FSM,但是運行時效率又能和第一種實現(xiàn)相當(dāng)?前面我們說了,可以利用模板元編程的代碼生成能力。我們利用模板元編程創(chuàng)建一種描述FSM的DSL,讓用戶可以以表的形式描述一個FSM,然后在C++編譯期將其生成類似第一種實現(xiàn)的代碼。這樣我們即得到了吻合于領(lǐng)域的表達力,又沒有造成任何運行時的性能損失!

接下來我們看看如何實現(xiàn)。

既然提到了使用表來表達,那么我們已經(jīng)有了一種熟識的編譯期表數(shù)據(jù)結(jié)構(gòu)了,沒錯,就是TypeList。TypeList的每個元素表示一個轉(zhuǎn)換(transition),代表表的一行。按照前面給出的DanceRobot的表格表示,每行應(yīng)該可以讓用戶定義:當(dāng)前狀態(tài),事件,目標(biāo)狀態(tài),以及對應(yīng)的action。所以我們定義一個模板Row,它的參數(shù)是:int CurrentState, typename EventType, int NextState, void(Fsm::*action)(const EventType&),一旦它具現(xiàn)化后將表示一個transition,作為狀態(tài)轉(zhuǎn)換表的一行。

除了表之外,用戶還應(yīng)該負責(zé)給出表中元素的定義,包括每個狀態(tài)、事件和action的定義。我們希望整個DSL框架和用戶的代碼分離開,用戶在自己的類中定義state,event,action以及轉(zhuǎn)換表,然后DSL框架負責(zé)為用戶生成所有的事件處理函數(shù)processEvent

有了上面的思考后,我們通過DanceRobot展示我們構(gòu)思的DSL的用法:

// Events
struct Close {};
struct Open {};
struct Play
{
    std::string name;
};
struct Stop {};

// FSM
struct DanceRobot : fsm::StateMachine<DanceRobot>
{
private:
    friend struct StateMachine<DanceRobot>;

    enum States
    {
        closed, opened, dancing, initial = closed
    };

    // actions
    void sayReady(const Open&)
    {
        std::cout << "Robot is ready for play!" << std::endl;
    }

    void sayClosed(const Close&)
    {
        std::cout << "Robot is closed!" << std::endl;
    }

    void sayStoped(const Stop&)
    {
        std::cout << "Robot stops playing!" << std::endl;
    }

    void doDance(const Play& playInfo)
    {
        std::cout << "Robot is dancing (" << playInfo.name << ") now!" << std::endl;
    }

    // table
    using R = DanceRobot;

    using TransitionTable = __type_list(
        //  +----------+----------+----------+----------------+
        //  |  current |   event  |  target  |  action        |
        //  +----------+----------+----------+----------------+
        Row <  closed  ,   Open   ,  opened  ,  &R::sayReady  >,
        //  +----------+----------+----------+----------------+
        Row <  opened  ,   Close  ,  closed  ,  &R::sayClosed >,
        Row <  opened  ,   Play   ,  dancing ,  &R::doDance   >,
        //  +----------+----------+----------+----------------+
        Row <  dancing ,   Stop   ,  opened  ,  &R::sayStoped >,
        Row <  dancing ,   Close  ,  closed  ,  &R::sayClosed >
        //  +----------+----------+----------+----------------+
    );
};

如上,我們希望客戶只用定義好Event,State,Action以及按照DSL的語法定義TransitionTable。最終所有消息處理函數(shù)的生成全部交給DSL背后的fsm::StateMachine框架,它負責(zé)根據(jù)TransitionTable生成所有類似前面第一種實現(xiàn)中的processEvent函數(shù),并且要求性能和它相當(dāng)。fsm::StateMachine框架是和用戶代碼解耦的,它獨立可復(fù)用的,用戶類通過我們之前介紹過的CRTP技術(shù)和它進行組合。

通過例子可以看到,TransitionTable的描述已經(jīng)非常接近手工描述一張狀態(tài)表了,我們基本沒有給用戶帶來太多偶發(fā)復(fù)雜度,更重要的是我們完全通過編譯時代碼生成技術(shù)來實現(xiàn),沒有為用戶帶來任何運行時效率損失。

接下來我們具體看看StateMachine的實現(xiàn)。

template<typename Derived>
struct StateMachine
{
    template<typename Event>
    int processEvent(const Event& e)
    {
        using Dispatcher = typename details::DispatcherGenerator<typename Derived::TransitionTable, Event>::Result;

        this->state = Dispatcher::dispatch(*static_cast<Derived*>(this), this->state, e);

        return this->state;
    }

    template<typename Event>
    int onUndefined(int state, const Event& e)
    {
        std::cout << "Error: no transition on state(" << state 
                  << ") handle event( " << typeid(e).name() 
                  << " )" << std::endl;
        return state;
    }

protected:
    template< int CurrentState,
              typename EventType,
              int NextState,
              void (Derived::*action)(const EventType&) >
    struct Row
    {
        enum
        {
            current = CurrentState,
            next = NextState
        };

        using Fsm = Derived;
        using Event = EventType;

        static void execute(Fsm& fsm, const Event& e)
        {
            (fsm.*action)(e);
        }
    };

protected:
    StateMachine() : state(Derived::initial)
    {
    }

private:
    int state;
};

上面是StateMachine的代碼實現(xiàn),不要被這么一大坨代碼嚇住,我們一步步分析它的實現(xiàn)。

先來看它的構(gòu)造函數(shù):

StateMachine() : state(Derived::initial)
{
}

int state;

它的內(nèi)部有一個私有成員state,用來存儲當(dāng)前的狀態(tài)。它的構(gòu)造函數(shù)把state初始化為Derived::initial。得益于CRTP模式,我們在父類模板中可以使用子類中的定義。StateMachine要求其子類中必須定義initial,用來指明初始狀態(tài)值。

接來下`onUndefined函數(shù)定義了當(dāng)收到未定義的消息時的默認處理方式。可以在子類中重定義這個方法,如果子類中沒有重定義則采用此默認版本。

template<typename Event>
int onUndefined(int state, const Event& e)
{
    std::cout << "Error: no transition on state(" << state 
              << ") handle event( " << typeid(e).name() 
              << " )" << std::endl;
    return state;
}

接下來內(nèi)部的嵌套模板Row用于子類在表中定義一行transition。它的四個模板參數(shù)分別代表當(dāng)前狀態(tài)、事件類型、目標(biāo)狀態(tài)以及對應(yīng)action的函數(shù)指針。注意由于采用了CRTP模式,這里我們直接使用了子類的類型Derived來聲明函數(shù)指針類型void (Derived::*action)(const EventType&)

template< int CurrentState,
          typename EventType,
          int NextState,
          void (Derived::*action)(const EventType&) >
struct Row
{
    enum
    {
        current = CurrentState,
        next = NextState
    };

    using Fsm = Derived;
    using Event = EventType;

    static void execute(Fsm& fsm, const Event& e)
    {
        (fsm.*action)(e);
    }
};

上面在Row中通過定義execute方法,對action的調(diào)用進行了封裝,統(tǒng)一了所有action的調(diào)用形式。原有的每個action名稱不同,例如sayReadysayStoped...,后續(xù)可以統(tǒng)一通過調(diào)用Row::execute的方式進行使用了。借助封裝層來統(tǒng)一不同方法的調(diào)用形式是一種非常有用的設(shè)計技巧。

最后我們來看StateMachineprocessEvent函數(shù)實現(xiàn)。

template<typename Event>
int processEvent(const Event& e)
{
    using Dispatcher = typename DispatcherGenerator<typename Derived::TransitionTable, Event>::Result;

    this->state = Dispatcher::dispatch(*static_cast<Derived*>(this), this->state, e);

    return this->state;
}

該函數(shù)是一個模板方法,它的入?yún)⑹谴幚淼南ⅲ瑸榱酥С置糠N消息,將消息的類型定義為泛型。為了方便客戶獲取轉(zhuǎn)換后的目標(biāo)狀態(tài),函數(shù)結(jié)束時返回最新的狀態(tài)。我們期望它對于任一種合法的入?yún)⑾㈩愋停梢宰詣由伤奶幚磉壿嫛?/p>

例如對于DanceRobot的Close消息,我們希望它可以自動生成如下代碼:

int processEvent(const Close& event)
{
    if(state == opened)
    {
        sayClosed(event);
        state = closed;
    }
    else if(state == dancing)
    {
        sayClosed(event);
        state = closed;
    }
    else
    {
        reportError(event);
    }

    return this->state;
}

而這所有神奇的事情,都是通過如下兩句代碼完成的:

using Dispatcher = typename DispatcherGenerator<typename Derived::TransitionTable, Event>::Result;

this->state = Dispatcher::dispatch(*static_cast<Derived*>(this), this->state, e);

上面第一句,我們通過把狀態(tài)表Derived::TransitionTable和當(dāng)前事件類型交給DispatcherGenerator,通過它得到了Dispatcher類型。從第二句中我們知道Dispatcher類型必須有一個靜態(tài)方法dispatch,它接收當(dāng)前狀態(tài)和事件對象,然后完成所有的處理邏輯。

所以一切的關(guān)鍵都在于DispatcherGenerator<typename Derived::TransitionTable, Event>::Result所做的類型生成。它能夠根據(jù)狀態(tài)轉(zhuǎn)化表以及當(dāng)前類型,生成正確的處理邏輯。那么DispatcherGenerator怎么實現(xiàn)呢?

我們再來看看如下DanceRobot的Close消息處理函數(shù)的實現(xiàn):

if(state == opened)
{
    sayClosed(event);
    state = closed;
}
else if(state == dancing)
{
    sayClosed(event);
    state = closed;
}
else
{
    reportError(event);
}

我們發(fā)現(xiàn),它的實現(xiàn)是形式化的。就是根據(jù)當(dāng)前消息類型Close,在狀態(tài)轉(zhuǎn)換表Derived::TransitionTable中找到所有可以處理它的行:

//  +----------+----------+----------+----------------+
//  |  current |   event  |  target  |  action        |
//  +----------+----------+----------+----------------+
Row <  opened  ,   Close  ,  closed  ,  &R::sayClosed >,
Row <  dancing ,   Close  ,  closed  ,  &R::sayClosed >

TypeList已經(jīng)有__filter元函數(shù),它根據(jù)一個指定的規(guī)則,將TypeList中所有滿足條件的元素過濾出來,返回由所有滿足條件的元素組成的TypeList。接下來要做的是用過濾出來的行,遞歸地完成如下模式的if-else結(jié)構(gòu):

template<typename Transition, typename Next>
struct EventDispatcher
{
    using Fsm = typename Transition::Fsm;
    using Event = typename Transition::Event;

    static int dispatch(Fsm& fsm, int state, const Event& e)
    {
        if(state == Transition::current)
        {
            Transition::execute(fsm, e);
            return Transition::next;
        }
        else
        {
            return Next::dispatch(fsm, state, e);
        }
    }
};

最后的一個else中調(diào)用未定義消息的處理函數(shù):

struct DefaultDispatcher
{
    template<typename Fsm, typename Event>
    static int dispatch(Fsm& fsm, int state, const Event& e)
    {
        return fsm.onUndefined(state, e);
    }
};

到此,基本的思路清楚了,我們把上述生成processEvent函數(shù)的這一切串起來。

template<typename Event, typename Transition>
struct EventMatcher
{
    using Result = __is_eq(Event, typename Transition::Event);
};

template<typename Table, typename Event>
struct DispatcherGenerator
{
private:
    template<typename Transition>
    using TransitionMatcher = typename EventMatcher<Event, Transition>::Result;

    using MatchedTransitions = __filter(Table, TransitionMatcher);

public:
    using Result = __fold(MatchedTransitions, DefaultDispatcher, EventDispatcher);
};

上面我們首先使用__filter(Table, TransitionMatcher)在表中過濾出滿足條件的所有transition,將過濾出來的TypeList交給MatchedTransitions保存。TransitionMatcher是過濾條件,它調(diào)用了EventMatcher去匹配和DispatcherGenerator入?yún)⒅?code>Event相同的Transition::Event

最后,我們對過濾出來的列表MatchedTransitions調(diào)用__fold元函數(shù),將其中每個transition按照EventDispatcher的模式去遞歸折疊,折疊的默認參數(shù)為DefaultDispatcher。如此我們按照過濾出來的表行,自動生成了遞歸的if-else結(jié)構(gòu),該結(jié)構(gòu)存在于返回值類型的靜態(tài)函數(shù)dispatch中。

這就是整個DSL背后的代碼,該代碼的核心在于利用模板元編程遞歸地生成了每種消息處理函數(shù)中形式化的if-else嵌套代碼結(jié)構(gòu)。由于模板元編程的所有計算在編譯期,模板中出現(xiàn)的封裝函數(shù)在編譯期都可以被內(nèi)聯(lián),所以最終生成的二進制代碼和最開始我們手寫的是基本一致的。

如果對本例的完整代碼該興趣,可以查看TLP庫中的源碼,位置在"tlp/sample/fsm"中。


后記

返回 C++11模板元編程 - 目錄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,776評論 18 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,707評論 18 399
  • 領(lǐng)域驅(qū)動設(shè)計(DDD)旨在軟件設(shè)計過程中提煉領(lǐng)域模型,以領(lǐng)域模型為核心改善業(yè)務(wù)專家和軟件開發(fā)者的溝通方式,對企業(yè)級...
    MagicBowen閱讀 5,559評論 0 29
  • 前言 人生苦多,快來 Kotlin ,快速學(xué)習(xí)Kotlin! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,240評論 9 118
  • 一 “我們分手吧!” 黎籬從未想過,分手會從他的口中說出,平鋪直敘,如此冷漠。她原本滿心期待,卻如水在煮沸的一刻涌...
    若臆閱讀 2,118評論 6 10