目次
設計から実装へ
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」が入力されるのですが、「開始文字待ち」状態のためソフトウェアが完了しています。
まとめ
いかがでしょうか。
今回は状態遷移の契機が「入力」だけだったため割と見やすくまとまっていますが、契機や条件が増えても応用は効くのではないでしょうか。