顯示具有 strategy pattern 標籤的文章。 顯示所有文章
顯示具有 strategy pattern 標籤的文章。 顯示所有文章

2017年7月8日 星期六

About states, part II

Today, we will be continuing our discussion about 「states」.

But, before that, let me digress a bit to talk about 「strategy pattern」.Those of you who made some study about design patterns may found that my example code are more like a strategy pattern than a state pattern. Yes, it is, in fact they are almost the same in spirit. What is strategy pattern? Simply put, under different circumstances(scenario, condition, option), the same behavior may be executed differently. Thus, we made them distinct strategy objects and choose the one used according to difference circumstance dynamically at runtime. When you change state, you may change the strategy used at the same time. It is reasonable. Thus, you could say without fault that state pattern is a kind of strategy pattern . So, let's forget about these jargon. The objective is the same: to prevent the use of a pile of 「if...else...」to clutter our code。It's just in strategy pattern's scenario, we maybe originally used user's choice in conditional statements to make judgement,on the other hand,in state pattern's scenario, we used runtime state to decide what code to run.

OK, back to state pattern. First, let's complete the example code. You can compile the following code and give it a test run.

//GameEvent.h
#ifndef Game_Event_H
#define Game_Event_H

enum {
    StartKey,
    OptionKey,
    UpKey,
    DownKey,
    LeftKey,
    RightKey,
    GameOverEvent,
};

struct InputEvent
{
    int event_;
    InputEvent (int e)
    : event_(e)
    {}  
};

#endif

//GameState.h
#ifndef Game_State_H
#define Game_State_H

#include <string>
#include "GameEvent.h"

class GameApp;

class GameState
{
public:
    virtual ~GameState() {}
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_system_event(GameApp &app, const InputEvent &e) = 0;
    virtual std::string name () const = 0;
};


#endif
//GameApp.h
#ifndef Game_App_H
#define Game_App_H

#include "GameEvent.h"
#include "GameState.h"

enum eDifficulty {
    EasyLevel,
    NormalLevel,
    HardLevel,
};

class StateTitleScreen: public GameState
{
public:
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e);
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e);
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e);
    virtual void handle_system_event(GameApp &app, const InputEvent &e);

    virtual std::string name () const { return "TitleScreen"; }
};

class StateOptionScreen: public GameState
{
public:
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e);
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e);
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e);
    virtual void handle_system_event(GameApp &app, const InputEvent &e);

    virtual std::string name () const { return "OptionScreen"; }
};

class StatePlaying: public GameState
{
public:
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e);
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e);
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e);
    virtual void handle_system_event(GameApp &app, const InputEvent &e);

    virtual std::string name () const { return "Playing"; }
};


class StateCountingScore: public GameState
{
    
public:
    StateCountingScore ();
    ~StateCountingScore ();
    
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e);
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e);
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e);
    virtual void handle_system_event(GameApp &app, const InputEvent &e);

    virtual std::string name () const { return "CountingScore"; }
};

class GameApp
{
    GameState *game_state_; // 目前遊戲狀態物件
    eDifficulty difficulty_;

    friend class StateTitleScreen;
    friend class StateOptionScreen;
    friend class StatePlaying;
    friend class StateCountingScore;

    void set_state (GameState *state);
    
public:
    GameApp();
    virtual ~GameApp() {}

    void handle_keyboard_input(const InputEvent &e);
    void handle_mouse_input(const InputEvent &e);
    void handle_gamepad_input(const InputEvent &e);
    void handle_system_event(const InputEvent &e);
};
#endif

// main.cpp
#include <iostream>
#include <string>
#include "GameApp.h"

using namespace std;

//class StateTitleScreen
void StateTitleScreen::handle_keyboard_input(GameApp &app, const  InputEvent &e)
{
    if (e.event_ == StartKey) {
        app.set_state(new StatePlaying);
    } else if (e.event_ == OptionKey) {
        app.set_state(new StateOptionScreen);
    }
}

void StateTitleScreen::handle_mouse_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateTitleScreen::handle_gamepad_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateTitleScreen::handle_system_event(GameApp &app, const  InputEvent &e)
{
    //...
}

//class StateOptionScreen
void StateOptionScreen::handle_keyboard_input(GameApp &app, const  InputEvent &e)
{
    // logic to navigate option screen, set game difficulty, screen resolution, etc.
}

void StateOptionScreen::handle_mouse_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateOptionScreen::handle_gamepad_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateOptionScreen::handle_system_event(GameApp &app, const  InputEvent &e)
{
    //...
}

//class StatePlaying
void StatePlaying::handle_keyboard_input(GameApp &app, const  InputEvent &e)
{
    if (e.event_ == GameOverEvent) {
        app.set_state(new StateCountingScore);
    }
}

void StatePlaying::handle_mouse_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StatePlaying::handle_gamepad_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StatePlaying::handle_system_event(GameApp &app, const  InputEvent &e)
{
    //...
}


// for simplfication, we use a global variable to set if score counting is done.
bool g_counting_done;

//class StateCountingScore
StateCountingScore::StateCountingScore ()
{
    cout << "\t\tgame over!" << endl;
}

StateCountingScore::~StateCountingScore ()
{
    g_counting_done = false;
}

void StateCountingScore::handle_keyboard_input(GameApp &app, const  InputEvent &e)
{
    // handle input only after score counting done.
    if (g_counting_done) { // any input is ok
        app.set_state(new StateTitleScreen);
    }
}

void StateCountingScore::handle_mouse_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateCountingScore::handle_gamepad_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateCountingScore::handle_system_event(GameApp &app, const  InputEvent &e)
{
    //...
}


GameApp::GameApp()
: game_state_(NULL)
, difficulty_(EasyLevel)
{
    this->set_state(new StateTitleScreen);
}


void GameApp::set_state(GameState* state)
{
    delete game_state_;
    game_state_ = state;
    cout << "change state to " << state->name() << endl;
}


void GameApp::handle_keyboard_input(const  InputEvent &e)
{
    game_state_->handle_keyboard_input (*this, e);
}

void GameApp::handle_mouse_input(const  InputEvent &e)
{
    game_state_->handle_mouse_input (*this, e);
}

void GameApp::handle_gamepad_input(const  InputEvent &e)
{
    game_state_->handle_gamepad_input (*this, e);
}

void GameApp::handle_system_event(const  InputEvent &e)
{
    game_state_->handle_system_event(*this, e);
}


int main()
{
    GameApp game_app;
    cout << "\tpress up key" << endl;
    game_app.handle_keyboard_input(InputEvent(UpKey));
    cout << "\tpress up key" << endl;
    game_app.handle_keyboard_input(InputEvent(UpKey));
    cout << "\tpress down key" << endl;
    game_app.handle_keyboard_input(InputEvent(DownKey));
    cout << "\tpress down key" << endl;
    game_app.handle_keyboard_input(InputEvent(DownKey));
    cout << "\tpress start key" << endl;
    game_app.handle_keyboard_input(InputEvent(StartKey));
    cout << "\t\tplaying..." << endl;
    cout << "\t\tthe Boss..." << endl;
    game_app.handle_keyboard_input(InputEvent(GameOverEvent));
    cout << "\tpress start key" << endl;
    game_app.handle_keyboard_input(InputEvent(StartKey));
    cout << "\t\tscore counting done" << endl;
    g_counting_done = true;
    cout << "\tpress start key" << endl;
    game_app.handle_keyboard_input(InputEvent(StartKey));
return 0; }
You should see the following output:
change state to TitleScreen

        press up key

        press up key

        press down key

        press down key

        press start key

change state to Playing

                playing...

                the Boss...

                game over!

change state to CountingScore

        press start key

                score counting done

        press start key

change state to TitleScreen
We observe that the user's input is delegated to state object to handle. And when it is time to change state we utilize GameApp::set_state(GameState* state) to do the transition. You can make some adjustment to meet the runtime requirement, like use shared pointer to do memory management instead of raw pointers. You can also pre-created all state objects instead of spawn new one every time state change if you don't mind use more memory.

That's it, it's enough to do simple states management this way. For real life applications, you may need more. Unlike this simple example, you may want to do some job on entering a state, do some other job on leaving, and do yet another one in transition. You can, like StateCountingScore class here, do the entry job in constructor and do the exit job in destructor. This is obviously not a good idea. You don't do complex things in constructor or destructor, which may bring you unexpected trouble. Also, if you use pre-created state objects, no constructor or destructor will be invoked when state change! An obvious better method is define pure virtual methods onentry(), onexit() in base class. When doing state change, you call onexit() of old state object, and then call onentry() of the new. In addition, you would define an ontransit() method, and call it between onexit() and onentry(). Like the following code:

class GameState
{
public:
    virtual ~GameState() {}
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_system_event(GameApp &app, const InputEvent &e) = 0;
    virtual std::string name () const = 0;
    virtual void onentry(GameApp &app)=0;
    virtual void onexit(GameApp &app)=0;
    virtual void ontransit(GameApp &app, std::string const&target_state)=0;
};
void GameApp::set_state(GameState* state)
{
    if (game_state_) {
        game_state_->onexit(*this);
        game_state_->ontransit(*this, state->name());
        delete game_state_;
    }
    game_state_ = state;
    game_state_->onentry(*this);
}
Here, in ontransit() method, you may want to do different things depending on the target state. This is a potential maintenance hazard, we will come back to it later. In the mean time, let's think more thoroughly. What happened when you apply state concept to control system behavior? In addition to onentry(), onexit(), ontransit(), what physical requirements you may need to meet when apply state to control system behavior?

When you think about state, this may be the image presented in your mind


Every state is independent to each other, live separately. But, this is not real in real life. Everything in real life have the concept of state in it, period. We as programmer has a state named 'developing', you can divide it into three child states, 'coding', 'debugging', and 'testing'. When you are in one of these three states, ain't you in 'developing' state too? That's it, often the states are hierarchical. In addition, Object are composed of many components, each component has its state, they as a whole, is the state of the object. You can't modelling these physical states with state pattern effectively. Continue the example of a programmer, when you modelling it using state pattern, you can only define states like 'coding', 'debugging', 'testing', and other terminal states (like 'sleeping', 'foraging', 'resting', etc.). 'developing' is just a logical state, so, in the program, when you enter one of the three states('coding', et al.) you should know you are entering 'developing' state too, you have to remember to invoke developing state object's onentry() in addition to the terminal one. You have to remember to invoke developing state object's onexit() when leave one of the three states too. That's not an welcome dependency. And when you have to check whether you are in 'developing' state, you have to check if you are in one of the three. As you can see, when facing more practical scenarios, state pattern is powerless. What should we do?

We will continue our discussion next time, please look forward to it...

關於狀態(states) part II

今天我們來繼續「狀態」的話題。

不過我們先岔開一下話題談一下strategy pattern。有進行design pattern研究的朋友應該都發現了,我們的例子看起來更像strategy pattern。是的,它們在精神上是差不多的。strategy pattern簡單說就是在不同的情景、條件、選擇下,某個行為的執行方法可能不同,所以我們把這些方法變成可依照時機使用/切換的策略物件,可以在執行時期變換。狀態切換時,你同時切換某個行為的策略物件也是很合理的。因此,說state pattern是一種strategy pattern也不為過。其實不用拘泥於這些術語。我們目的一樣是避免一堆重複的「if..else...」。只是在strategy pattern原本可能是拿使用者選擇的策略來做判斷,而在state pattern則是用執行時的狀態做判斷。

ok, 回到state pattern。我們先把上次的範例做完整一點吧。你可以把下面的程式碼編譯起來跑看看。

//GameEvent.h
#ifndef Game_Event_H
#define Game_Event_H

enum {
    StartKey,
    OptionKey,
    UpKey,
    DownKey,
    LeftKey,
    RightKey,
    GameOverEvent,
};

struct InputEvent
{
    int event_;
    InputEvent (int e)
    : event_(e)
    {}  
};

#endif

//GameState.h
#ifndef Game_State_H
#define Game_State_H

#include <string>
#include "GameEvent.h"

class GameApp;

class GameState
{
public:
    virtual ~GameState() {}
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_system_event(GameApp &app, const InputEvent &e) = 0;
    virtual std::string name () const = 0;
};


#endif
//GameApp.h
#ifndef Game_App_H
#define Game_App_H

#include "GameEvent.h"
#include "GameState.h"

enum eDifficulty {
    EasyLevel,
    NormalLevel,
    HardLevel,
};

class StateTitleScreen: public GameState
{
public:
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e);
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e);
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e);
    virtual void handle_system_event(GameApp &app, const InputEvent &e);

    virtual std::string name () const { return "TitleScreen"; }
};

class StateOptionScreen: public GameState
{
public:
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e);
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e);
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e);
    virtual void handle_system_event(GameApp &app, const InputEvent &e);

    virtual std::string name () const { return "OptionScreen"; }
};

class StatePlaying: public GameState
{
public:
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e);
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e);
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e);
    virtual void handle_system_event(GameApp &app, const InputEvent &e);

    virtual std::string name () const { return "Playing"; }
};


class StateCountingScore: public GameState
{
    
public:
    StateCountingScore ();
    ~StateCountingScore ();
    
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e);
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e);
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e);
    virtual void handle_system_event(GameApp &app, const InputEvent &e);

    virtual std::string name () const { return "CountingScore"; }
};

class GameApp
{
    GameState *game_state_; // 目前遊戲狀態物件
    eDifficulty difficulty_;

    friend class StateTitleScreen;
    friend class StateOptionScreen;
    friend class StatePlaying;
    friend class StateCountingScore;

    void set_state (GameState *state);
    
public:
    GameApp();
    virtual ~GameApp() {}

    void handle_keyboard_input(const InputEvent &e);
    void handle_mouse_input(const InputEvent &e);
    void handle_gamepad_input(const InputEvent &e);
    void handle_system_event(const InputEvent &e);
};
#endif

// main.cpp
#include <iostream>
#include <string>
#include "GameApp.h"

using namespace std;

//class StateTitleScreen
void StateTitleScreen::handle_keyboard_input(GameApp &app, const  InputEvent &e)
{
    if (e.event_ == StartKey) {
        app.set_state(new StatePlaying);
    } else if (e.event_ == OptionKey) {
        app.set_state(new StateOptionScreen);
    }
}

void StateTitleScreen::handle_mouse_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateTitleScreen::handle_gamepad_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateTitleScreen::handle_system_event(GameApp &app, const  InputEvent &e)
{
    //...
}

//class StateOptionScreen
void StateOptionScreen::handle_keyboard_input(GameApp &app, const  InputEvent &e)
{
    // logic to navigate option screen, set game difficulty, screen resolution, etc.
}

void StateOptionScreen::handle_mouse_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateOptionScreen::handle_gamepad_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateOptionScreen::handle_system_event(GameApp &app, const  InputEvent &e)
{
    //...
}

//class StatePlaying
void StatePlaying::handle_keyboard_input(GameApp &app, const  InputEvent &e)
{
    if (e.event_ == GameOverEvent) {
        app.set_state(new StateCountingScore);
    }
}

void StatePlaying::handle_mouse_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StatePlaying::handle_gamepad_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StatePlaying::handle_system_event(GameApp &app, const  InputEvent &e)
{
    //...
}


// for simplfication, we use a global variable to set if score counting is done.
bool g_counting_done;

//class StateCountingScore
StateCountingScore::StateCountingScore ()
{
    cout << "\t\tgame over!" << endl;
}

StateCountingScore::~StateCountingScore ()
{
    g_counting_done = false;
}

void StateCountingScore::handle_keyboard_input(GameApp &app, const  InputEvent &e)
{
    // handle input only after score counting done.
    if (g_counting_done) { // any input is ok
        app.set_state(new StateTitleScreen);
    }
}

void StateCountingScore::handle_mouse_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateCountingScore::handle_gamepad_input(GameApp &app, const  InputEvent &e)
{
    //...
}

void StateCountingScore::handle_system_event(GameApp &app, const  InputEvent &e)
{
    //...
}


GameApp::GameApp()
: game_state_(NULL)
, difficulty_(EasyLevel)
{
    this->set_state(new StateTitleScreen);
}


void GameApp::set_state(GameState* state)
{
    delete game_state_;
    game_state_ = state;
    cout << "change state to " << state->name() << endl;
}


void GameApp::handle_keyboard_input(const  InputEvent &e)
{
    game_state_->handle_keyboard_input (*this, e);
}

void GameApp::handle_mouse_input(const  InputEvent &e)
{
    game_state_->handle_mouse_input (*this, e);
}

void GameApp::handle_gamepad_input(const  InputEvent &e)
{
    game_state_->handle_gamepad_input (*this, e);
}

void GameApp::handle_system_event(const  InputEvent &e)
{
    game_state_->handle_system_event(*this, e);
}


int main()
{
    GameApp game_app;
    cout << "\tpress up key" << endl;
    game_app.handle_keyboard_input(InputEvent(UpKey));
    cout << "\tpress up key" << endl;
    game_app.handle_keyboard_input(InputEvent(UpKey));
    cout << "\tpress down key" << endl;
    game_app.handle_keyboard_input(InputEvent(DownKey));
    cout << "\tpress down key" << endl;
    game_app.handle_keyboard_input(InputEvent(DownKey));
    cout << "\tpress start key" << endl;
    game_app.handle_keyboard_input(InputEvent(StartKey));
    cout << "\t\tplaying..." << endl;
    cout << "\t\tthe Boss..." << endl;
    game_app.handle_keyboard_input(InputEvent(GameOverEvent));
    cout << "\tpress start key" << endl;
    game_app.handle_keyboard_input(InputEvent(StartKey));
    cout << "\t\tscore counting done" << endl;
    g_counting_done = true;
    cout << "\tpress start key" << endl;
    game_app.handle_keyboard_input(InputEvent(StartKey));
return 0; }
你應該可以得到下面的output:

change state to TitleScreen

        press up key

        press up key

        press down key

        press down key

        press start key

change state to Playing

                playing...

                the Boss...

                game over!

change state to CountingScore

        press start key

                score counting done

        press start key

change state to TitleScreen
我們可以看到,使用者的輸入會轉交給目前的state object處理,需要改變state時,呼叫app的GameApp::set_state(GameState* state)。你可以做些變化使符合執行時的要求。比如說使用shared pointer來管理記憶體而不是raw pointer。或者我們不介意多用些記憶體,使用預先建立好的物件而不是每次切換state時重新new一個物件。

好的,管理簡單的狀態變化這樣就夠了。但是在開發實際的程式時,相對於簡單的範例,我們很可能會希望在進入一個狀態時執行某個動作,在離開一個狀態時執行另一個動作,在狀態轉換時執行另另一個動作。 如果你使用你這邊的範例的做法,像StateCountingScore這樣,我們可以把進入狀態時做的事寫在constructor,離開狀態時做的事寫在destructor。不過這樣做顯然不夠理想,除了在construct/destruct時執行複雜的動作可能帶來不可遇期的麻煩之外,如果你使用預先產生好的物件,這招基本上就行不通了。顯然比較好的做法是,在base class定義一組onentry(), onexit()方法。在轉換state時在依序呼舊state的onexit(),新state的onentry()。另外,你可以定義一個轉換時的ontransit()方法,在兩個狀態轉換中間呼叫。像下面的code

class GameState
{
public:
    virtual ~GameState() {}
    virtual void handle_keyboard_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_mouse_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_gamepad_input(GameApp &app, const InputEvent &e) = 0;
    virtual void handle_system_event(GameApp &app, const InputEvent &e) = 0;
    virtual std::string name () const = 0;
    virtual void onentry(GameApp &app)=0;
    virtual void onexit(GameApp &app)=0;
    virtual void ontransit(GameApp &app, std::string const&target_state)=0;
};
void GameApp::set_state(GameState* state)
{
    if (game_state_) {
        game_state_->onexit(*this);
        game_state_->ontransit(*this, state->name());
        delete game_state_;
    }
    game_state_ = state;
    game_state_->onentry(*this);
}

而這邊,顯然在ontransit()中我們可能會針對轉換的目標狀態來決定要執行的動作,這似乎會造成將來維護上的困難,我們之後將回來討論這一部分。在那之前,我們再想想,除了onentry/onexit/ontransit這些東西之外,在使用狀態控制程式時還會有什麼實際的需求?一般來說,當我們想到狀態圖時,心裏浮現的圖大概是長這樣


每個狀態之間涇渭分明,互相獨立。然而,每一種東西都有狀態的概念在裏頭,就拿我們身為開發者來說,開發程式中(developing)也是一種狀態,而開發程式可以再細分成coding,debugging,testing等子狀態,你在這些子狀態中難道不都叫開發中嗎?所以,狀態常常是階層式的。再來,就像一個物體可能由幾個部分組成,這些部分都是構成該物體的必要成分一樣,一個狀態可能由幾個同時存在的次狀態構成。使用state pattern無法有效的模仿這些實際的狀況。以這個developer的例子來說,使用state pattern來模擬developer的狀態時,我們可能只能定義coding, debugging, testing與其它的終端狀態(睡眠、覓食、休息...)。developing只是一個概念上的狀態,所以實際的程式裏每當進入這三個狀態的其中之時,我們要記得我們也進入了developing的狀態,要執行developing的onentry,離開時也一樣不能忘了developing的onexit。要檢查developer是不是在developing的狀態則是看看developer是不是在coding, deugging,或testing中。面對比較實際的問題時,state pattern顯得力不從心,該怎麼辦呢?我們下次將繼續這個話題。敬請期待...


ftps.space released!

  I started doing web site development some time ago.   After fiddled with Flask, nginx, javascript, html, css, websocket, etc, etc... I h...