これは、C++でコーディングされるインタビューの質問でした。
自動販売機用のコードを書く:1つのタイプのアイテムを販売するだけの簡単なものから始めます。したがって、お金と在庫という2つの状態変数があります。
私の答え:
約3〜4個の状態を持つステートマシンを使用します。列挙型変数を使用して状態を示し、switch caseステートメントを使用します。各caseには各状態に対応する操作があり、ループを維持してある状態から別の状態に移動します。
次の質問:
ただし、switch caseステートメントを使用しても、追加される状態が増えたり、状態内の既存の操作が変更されたりしても、「適切にスケーリング」されません。その問題にどのように対処しますか?
私はその時にこの質問に答えることができませんでした。しかし、後で考えて、おそらく次のことができます。
std::map
from(string、function)stringは、対応する状態関数を呼び出す状態を示します。私の質問は:
インタビューの質問では、大規模なソフトウェアシステムのC++イディオムとデザインパターンからの回答を期待しています。
もっとOOアプローチで、 State Pattern
:
// machine.h
#pragma once
#include "MachineStates.h"
class AbstractState;
class Machine {
friend class AbstractState;
public:
Machine(unsigned int inStockQuantity);
void sell(unsigned int quantity);
void refill(unsigned int quantity);
unsigned int getCurrentStock();
~Machine();
private:
unsigned int mStockQuantity;
AbstractState* mState;
};
// machine.cpp
#include "Machine.h"
Machine::Machine(unsigned int inStockQuantity) :
mStockQuantity(inStockQuantity),
mState(inStockQuantity > 0 ? new Normal() : new SoldOut()) {
}
Machine::~Machine() {
delete mState;
}
void Machine::sell(unsigned int quantity) {
mState->sell(*this, quantity);
}
void Machine::refill(unsigned int quantity) {
mState->refill(*this, quantity);
}
unsigned int Machine::getCurrentStock() {
return mStockQuantity;
}
// MachineStates.h
#pragma once
#include "Machine.h"
#include <exception>
#include <stdexcept>
class Machine;
class AbstractState {
public:
virtual void sell(Machine& machine, unsigned int quantity) = 0;
virtual void refill(Machine& machine, unsigned int quantity) = 0;
virtual ~AbstractState();
protected:
void setState(Machine& machine, AbstractState* st);
void updateStock(Machine& machine, unsigned int quantity);
};
class Normal : public AbstractState {
public:
virtual void sell(Machine& machine, unsigned int quantity);
virtual void refill(Machine& machine, unsigned int quantity);
virtual ~Normal();
};
class SoldOut : public AbstractState {
public:
virtual void sell(Machine& machine, unsigned int quantity);
virtual void refill(Machine& machine, unsigned int quantity);
virtual ~SoldOut();
};
// MachineStates.cpp
#include "MachineStates.h"
AbstractState::~AbstractState() {
}
void AbstractState::setState(Machine& machine, AbstractState* state) {
AbstractState* aux = machine.mState;
machine.mState = state;
delete aux;
}
void AbstractState::updateStock(Machine& machine, unsigned int quantity) {
machine.mStockQuantity = quantity;
}
Normal::~Normal() {
}
void Normal::sell(Machine& machine, unsigned int quantity) {
int currStock = machine.getCurrentStock();
if (currStock < quantity) {
throw std::runtime_error("Not enough stock");
}
updateStock(machine, currStock - quantity);
if (machine.getCurrentStock() == 0) {
setState(machine, new SoldOut());
}
}
void Normal::refill(Machine& machine, unsigned int quantity) {
int currStock = machine.getCurrentStock();
updateStock(machine, currStock + quantity);
}
SoldOut::~SoldOut() {
}
void SoldOut::sell(Machine& machine, unsigned int quantity) {
throw std::runtime_error("Sold out!");
}
void SoldOut::refill(Machine& machine, unsigned int quantity) {
updateStock(machine, quantity);
setState(machine, new Normal());
}
私はC++でのプログラミングには慣れていませんが、このコードはGCC 4.8.2に対して明らかにコンパイルされており、valgrindはリークを示していないので、大丈夫だと思います。私はお金を計算していませんが、アイデアを示すためにこれは必要ありません。
テストするには:
#include <iostream>
#include <stdexcept>
#include "Machine.h"
#include "MachineStates.h"
int main() {
Machine m(10), m2(0);
m.sell(10);
std::cout << "m: " << "Sold 10 items" << std::endl;
try {
m.sell(1);
} catch (std::exception& e) {
std::cerr << "m: " << e.what() << std::endl;
}
m.refill(20);
std::cout << "m: " << "Refilled 20 items" << std::endl;
m.sell(10);
std::cout << "m: " << "Sold 10 items" << std::endl;
std::cout << "m: " << "Remaining " << m.getCurrentStock() << " items" << std::endl;
m.sell(5);
std::cout << "m: " << "Sold 5 items" << std::endl;
std::cout << "m: " << "Remaining " << m.getCurrentStock() << " items" << std::endl;
try {
m.sell(10);
} catch (std::exception& e) {
std::cerr << "m: " << e.what() << std::endl;
}
try {
m2.sell(1);
} catch (std::exception& e) {
std::cerr << "m2: " << e.what() << std::endl;
}
return 0;
}
出力は次のとおりです。
m: Sold 10 items m: Sold out! m: Refilled 20 items m: Sold 10 items m: Remaining 10 items m: Sold 5 items m: Remaining 5 items m: Not enough stock m2: Not enough stock
Broken
状態を追加する場合、必要なのは別のAbstractState
子だけです。 broken
にもMachine
プロパティを追加する必要があるかもしれません。
製品を追加するには、製品のマップとそれぞれの在庫量などが必要です...
switch
ステートメントの代わりにテーブルの使用を検討してください。 1つの列が遷移基準になり、別の列が宛先状態になります。
テーブル処理機能を変更する必要がないため、これはうまくスケールします。テーブルに別の行を追加するだけです。
+------------------+---------------------+---------------+
| Current state ID | transition criteria | Next state ID |
+------------------+---------------------+---------------+
| | | |
+------------------+---------------------+---------------+
仕事中の私のコードでは、「Next state ID」ではなく関数ポインターの列を使用しています。テーブルは、アクセサー関数が定義された別個のファイルです。各関数ポインターを解決するための1つ以上のincludeステートメントがあります。
table.h
#ifndef TABLE_H
#define TABLE_H
struct Table_Entry
{
unsigned int current_state_id;
unsigned char transition_letter;
unsigned int next_state_id;
};
Table_Entry const * table_begin(void);
Table_Entry const * table_end(void);
#endif // TABLE_H
table.cpp:
#include "table.h"
static const Table_Entry my_table[] =
{
// Current Transition Next
// State ID Letter State ID
{ 0, 'A', 1}, // From 0 goto 1 if letter is 'A'.
{ 0, 'B', 2}, // From 0 goto 2 if letter is 'B'.
{ 0, 'C', 3}, // From 0 goto 3 if letter is 'C'.
{ 1, 'A', 1}, // From 1 goto 1 if letter is 'A'.
{ 1, 'B', 3}, // From 1 goto 3 if letter is 'B'.
{ 1, 'C', 0}, // From 1 goto 0 if letter is 'C'.
};
static const unsigned int TABLE_SIZE =
sizeof(my_table) / sizeof(my_table[0]);
Table_Entry const *
table_begin(void)
{
return &my_table[0];
}
Table_Entry const *
table_end(void)
{
return &my_table[TABLE_SIZE];
}
state_machine.cpp
#include "table.h"
#include <iostream>
using namespace std; // Because I'm lazy.
void
Execute_State_Machine(void)
{
unsigned int current_state = 0;
while (1)
{
char transition_letter;
cout << "Current state: " << current_state << "\n";
cout << "Enter transition letter: ";
cin >> transition_letter;
cin.ignore(1000, '\n'); /* Eat up the '\n' still in the input stream */
Table_Entry const * p_entry = table_begin();
Table_Entry const * const p_table_end = table_end();
bool state_found = false;
while ((!state_found) && (p_entry != p_table_end))
{
if (p_entry->current_state_id == current_state)
{
if (p_entry->transition_letter == transition_letter)
{
cout << "State found, transitioning"
<< " from state " << current_state
<< ", to state " << p_entry->next_state_id
<< "\n";
current_state = p_entry->next_state_id;
state_found = true;
break;
}
}
++p_entry;
}
if (!state_found)
{
cerr << "Transition letter not found, current state not changed.\n";
}
}
}
私はかつてC++でステートマシンを記述しました。そこでは、多くのステートペア(ソース→ターゲットペア)に対して同じ遷移が必要でした。私は例を示したい:
4 -> 8 \
5 -> 9 \_ action1()
6 -> 10 /
7 -> 11 /
8 -> 4 \
9 -> 5 \_ action2()
10 -> 6 /
11 -> 7 /
私が思いついたのは、(遷移基準+次の状態+呼び出される「アクション」関数)のセットでした。物事を一般的にするために、遷移基準と次の状態の両方がファンクター(ラムダ関数)として記述されました。
typedef std::function<bool(int)> TransitionCriteria;
typedef std::function<int(int)> TransitionNewState;
typedef std::function<void(int)> TransitionAction; // gets passed the old state
上記の例のように多くの異なる状態に適用される多くの遷移がある場合、このソリューションは素晴らしいです。ただし、この方法では、「ステップ」ごとに、すべての異なる遷移のリストを直線的にスキャンする必要があります。
上記の例では、このような遷移が2つあります。
struct Transition {
TransitionCriteria criteria;
TransitionNewState newState;
TransitionAction action;
Transition(TransitionCriteria c, TransitionNewState n, TransitionAction a)
: criteria(c), newState(n), action(a) {}
};
std::vector<Transition> transitions;
transitions.Push_back(Transition(
[](int oldState){ return oldState >= 4 && oldState < 8; },
[](int oldState){ return oldState + 4; },
[](int oldState){ std::cout << "action1" << std::endl; }
));
transitions.Push_back(Transition(
[](int oldState){ return oldState >= 8 && oldState < 12; },
[](int oldState){ return oldState - 4; },
[](int oldState){ std::cout << "action2" << std::endl; }
));
面接でそれが得られたかどうかはわかりませんが、特にプロフェッショナルな環境の場合は、手動でステートマシンをコーディングすることは個人的に控えます。ステートマシンはよく研究された問題であり、手作業で作成するものよりも優れたコードを生成することがよくテストされたオープンソースツールが存在します。また、ステートマシンの問題の診断などにも役立ちます。状態図を自動的に生成できること。
この種の問題に対する私のgotoツールは次のとおりです。
これらの方法を使用して、多くのステートマシンを作成しました。しかし、CiscoのNexus 7000用トランシーバーライブラリ(117,000ドルのスイッチ)を書いたとき、80年代に発明した方法を使用しました。それは、ステートマシンをマルチタスクブロッキングコードのように見えるようにするマクロを使用することでした。マクロはC用に作成されていますが、Dellで働いていたときにC++用に少し変更してマクロを使用しました。詳しくはこちらをご覧ください: https://www.codeproject.com/Articles/37037/Macros-to-simulate-multi-tasking-blocking-code-at
#include <stdio.h>
#include <iostream>
using namespace std;
class State;
enum state{ON=0,OFF};
class Switch {
private:
State* offState;
State* onState;
State* currState;
public:
~Switch();
Switch();
void SetState(int st);
void on();
void off();
};
class State{
public:
State(){}
virtual void on(Switch* op){}
virtual void off(Switch* op){}
};
class OnState : public State{
public:
OnState(){
cout << "OnState State Initialized" << endl;
}
void on(Switch* op);
void off(Switch* op);
};
class OffState : public State{
public:
OffState(){
cout << "OffState State Initialized" << endl;
}
void on(Switch* op);
void off(Switch* op);
};
Switch::Switch(){
offState = new OffState();
onState = new OnState();
currState=offState;
}
Switch::~Switch(){
if(offState != NULL)
delete offState;
if(onState != NULL)
delete onState;
}
void Switch::SetState(int newState){
if(newState == ON)
{
currState = onState;
}
else if(newState == OFF)
{
currState = offState;
}
}
void Switch::on(){
currState->on(this);
}
void Switch::off(){
currState->off(this);
}
void OffState::on(Switch* op){
cout << "State transition from OFF to ON" << endl;
op->SetState(ON);
}
void OffState::off(Switch* op){
cout << "Already in OFF state" << endl;
}
void OnState::on(Switch* op){
cout << "Already in ON state" << endl;
}
void OnState::off(Switch* op){
cout << "State transition from ON to OFF" << endl;
op->SetState(OFF);
}
int main(){
Switch* swObj = new Switch();
int ch;
do{
switch(ch){
case 1: swObj->on();
break;
case 0: swObj->off();
break;
default : cout << "Invalid choice"<<endl;
break;
}
cout << "Enter 0/1: ";
cin >> ch;
}while(true);`enter code here`
delete swObj;
return 0;
}