【設計を具体的なコードへ】ステート図をコードへ落とし込む

ソフトウェア

設計から実装へ

Parserクラス

Parserクラスにステートとメソッドを定義する

まずParserクラスのヘッダに、ステート図の状態と一対一で対応するenumを宣言し、それのプライベート変数を定義しましょう。

class Parser
{
private:
	enum class State
	{
		StartWait,
		Inputting,
		Skip,
	};

	State state;
public:
	Parser();
	void Input(char data);
	bool Output(std::string &dest);
};

コンストラクタで、初期状態を設定します。

#include "Parser.h"

Parser::Parser()
{
	state = State::StartWait;
}

void Parser::Input(char data)
{
}

bool Output(std::string &dest)
{
}

bool Parser::IsIdle()
{
}

Inputメソッドの状態毎の処理を整理

Inputメソッドでは、状態毎に処理が異なります。
簡単な内容ならInputメソッドに直接書いてしまってもいいのですが、長くなりがちなので別メソッドに分ける事にします。

class Parser
{
<<中略>>
	bool Output(std::string &dest);
private:
	void subStartWait(char data);
	void subInputting(char data);
	void subSkip(char data);
};
void Parser::Input(char data)
{
	switch (state)
	{
	case Parser::StartWait:
		subStartWait(data);
		break;
	case Parser::Inputting:
		subInputting(data);
		break;
	case Parser::Skip:
		subSkip(data);
		break;
	}
}

<<中略>>

void Parser::subStartWait(char data)
{
	
}

void Parser::subInputting(char data)
{
}

void Parser::subSkip(char data)
{
}

Inputメソッドの実装

小分けにした状態毎の処理を実装していきます。
処理に必要な一時バッファと、最終出力用のキューも用意します。
実装する中で、開始文字待ち状態へ戻る時の共通処理を見つけたので、プライベートメソッドとして追加しました。

class Parser
{
private:
	State state;
	std::string tempBuffer;
	std::queue<std::string> resultQueue;

<<中略>>

private:
	void subStartWait(char data);
	void subInputting(char data);
	void subSkip(char data);

	void moveToStartWait();
};
void Parser::subStartWait(char data)
{
	if (data == 's') 
	{
		state = State::Inputting;
		tempBuffer.push_back(data);
	}
}

void Parser::subInputting(char data)
{
	if (data == 'n')
	{
		state = State::Skip;
	}
	else if (data == 'e')
	{
		moveToStartWait();
	}
	else
	{
		tempBuffer.push_back(data);
	}
}

void Parser::subSkip(char data)
{
	if (data == 'n')
	{
		state = State::Inputting;
	}
	else if (data == 'e')
	{
		moveToStartWait();
	}
}

void Parser::moveToStartWait()
{
	state = State::StartWait;
	if (!tempBuffer.empty())
	{
		resultQueue.push(tempBuffer);
		tempBuffer.clear();
	}
}

Outputメソッドの実装

bool Parser::Output(std::string &dest)
{
	if (resultQueue.empty())
	{
		return false;
	}
	dest = resultQueue.front();
	resultQueue.pop();
	return true;
}

IsIdleメソッドの実装

bool Parser::IsIdle()
{
	return state == State::StartWait;
}

ビジネスロジックの実装

あまり大きなソフトでもないので、Parserクラスを呼び出して要求を満たすロジックはmain関数に直接書いていってしまいます。

main関数実装

「ユーザーからの入力に関係なく、アプリは1秒ごとに「.」を画面に出力する」という要求があるため、時間を扱うために「std::chrono」を使っています。
またキーボードからの入力を処理を止めずに読み取る必要があったため、c++特有のcinやcoutを使うことができず、古き良き「conio.h」をincludeし、「_kbhit」「getch」などの関数を使っています。「_kbhit」はwindows特有の関数だそうです。

#include <iostream>
#include <chrono>
#include <conio.h>
#include "Parser.h"

using namespace std::chrono;

int main()
{
    Parser parser;

    auto start = system_clock::now();
    char input = EOF;
    do {
        // 入力された文字を最後まで読み込む
        while(_kbhit())  // 入力がある場合繰り返す
        {
            input = _getch();
            parser.Input(input);
        }

        // 1秒毎に「.」を出力
        auto elapse = system_clock::now() - start; // 引き算で経過時間を計算
        auto ms = duration_cast<milliseconds>(elapse).count();  // ms単位に変換
        if (1000 <= ms)
        {
            start = system_clock::now();      // スタート時刻を更新
            std::cout << ".";
        }

    } while ((parser.IsIdle() && (input == 'e')) == false);

    std::cout << "\n<<解析結果表示>>\n";
    std::string result;
    while (parser.Output(result))
    {
        std::cout << result << "\n";
    }
    std::cout << "\n終了する場合、何かキーを押してください。\n";
    getchar();
}

実行結果

ソフトを実行して、試しに以下の文字列を打ち込んでみました。

123s456s789n123s456n789eens456n789ee

結果は以下の通り。

1行目に「…..」が表示されていますが、これが1秒ごとに出力されている「.」です。

3行目が1つ目の分解結果で、「s456s789789」となっており、「123s456s789n123s456n789e」までを分解した結果です。初期状態は「開始文字待ち」なので、sが来るまでの間入力を無視するため初めの「123」は無視されています。その後「s」が来るので記録すると共に「入力中」状態に移行します。その後「456s789」を記録したところで「n」が来るので、「スキップ中」状態に移行します。そこからまたnが来るまでの「123s456n」がスキップされています。「n」が来ると「入力中」状態に戻るので、その後「789」を記録しています。その後「e」が来たところで「開始文字待ち」状態に移行して、結果がキューにたまります。


4行目からは「ens456n789eee123」を分解した結果です。、「s」を受け取るまでの「en」は無視されています。「s456」までを記録したところで「n」がくるため「スキップ中」状態に移行します。ですが、この後「n」は来ることはなく「e」が来るため、「開始文字待ち」状態に戻り結果がキューにたまります。

その後「e」が入力されるのですが、「開始文字待ち」状態のためソフトウェアが完了しています。

まとめ

いかがでしょうか。
今回は状態遷移の契機が「入力」だけだったため割と見やすくまとまっていますが、契機や条件が増えても応用は効くのではないでしょうか。

タイトルとURLをコピーしました