web-dev-qa-db-fra.com

Code C ++ pour la machine d'état

Il s'agissait d'une question d'entrevue à coder en C++:

Écrire du code pour un distributeur automatique: commencez par un simple où il ne distribue qu'un type d'élément. Donc, deux variables d'état: l'argent et l'inventaire, feraient l'affaire.

Ma réponse:

J'utiliserais une machine d'état qui a environ 3-4 états. Utilisez une variable enum pour indiquer l'état et utilisez une instruction switch case, où chaque cas a les opérations à effectuer correspondant à chaque état et reste dans une boucle pour passer d'un état à un autre.

La question suivante:

Mais l'utilisation d'une instruction de cas de commutateur ne "s'adapte pas bien" pour plus d'états ajoutés et modifiant les opérations existantes dans un état. Comment allez-vous gérer ce problème?

Je n'ai pas pu répondre à cette question à ce moment-là. Mais plus tard, je peux probablement:

  • avoir des fonctions différentes pour différents états (chaque fonction correspondant à un état)
  • avoir un std::map from (chaîne, fonction) où chaîne indique l'état pour appeler la fonction d'état correspondante.
  • La fonction principale a une variable chaîne (commençant dans l'état initial) et appelle la fonction correspondant à cette variable dans une boucle. Chaque fonction effectue les opérations nécessaires et renvoie le nouvel état à la fonction principale.

Mes questions sont:

  • Quel est le problème avec les déclarations de casse de commutateur en ce qui concerne l'évolutivité dans le contexte des systèmes logiciels à grande échelle?
  • Si c'est le cas, ma solution (qui actuellement me semble un peu plus modulaire que d'avoir un long code linéaire) va-t-elle résoudre le problème?

La question d'entrevue attend des réponses des idiomes C++ et des modèles de conception pour les systèmes logiciels à grande échelle.

32
Romonov

Je pensais dans une approche plus OO, en utilisant State Pattern :

La machine:

// 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;
}

Les États:

// 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());
}

Je ne suis pas habitué à programmer en C++, mais ce code compile de manière transparente avec GCC 4.8.2 et valgrind ne montre aucune fuite, donc je suppose que ça va. Je ne calcule pas d'argent, mais je n'en ai pas besoin pour vous montrer l'idée.

Pour le tester:

#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;
}

La sortie est:

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

Maintenant, si vous voulez ajouter un état Broken, il vous suffit d'avoir un autre enfant AbstractState. Vous devrez peut-être également ajouter une propriété broken sur Machine.

Pour ajouter plus de produits, vous devez avoir une carte des produits et leur quantité en stock respective et ainsi de suite ...

42
Henrique Barcelos

Pensez à utiliser des tables au lieu des instructions switch. Une colonne pourrait être les critères de transition et une autre colonne est l'état de destination.

Cela évolue bien car vous n'avez pas besoin de changer la fonction de traitement des tables; ajoutez simplement une autre ligne au tableau.

+------------------+---------------------+---------------+
| Current state ID | transition criteria | Next state ID |
+------------------+---------------------+---------------+
|                  |                     |               |
+------------------+---------------------+---------------+

Dans mon code au travail, nous utilisons une colonne de pointeurs de fonction plutôt que le "Next State ID". Le tableau est un fichier séparé avec des fonctions d'accesseur définies. Il existe une ou plusieurs instructions include pour résoudre chaque pointeur de fonction.

Édition 1: exemple de fichiers de table séparés.

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";
         }
    }
}
25
Thomas Matthews

J'ai écrit une fois une machine à états en C++, où j'avais besoin de la même transition pour beaucoup de paires d'états (source → paires cibles). Je veux illustrer un exemple:

4 -> 8   \
5 -> 9    \_ action1()
6 -> 10   /
7 -> 11  /

8 -> 4   \
9 -> 5    \_ action2()
10 -> 6   /
11 -> 7  /

Ce que j'ai trouvé était un ensemble de (critères de transition + état suivant + fonction "action" à appeler). Pour garder les choses générales, les critères de transition et l'état suivant ont été écrits comme des foncteurs (fonctions lambda):

typedef std::function<bool(int)> TransitionCriteria;
typedef std::function<int(int)>  TransitionNewState;
typedef std::function<void(int)> TransitionAction;   // gets passed the old state

Cette solution est bien si vous avez beaucoup de transitions qui s'appliquent à beaucoup d'états différents comme dans l'exemple ci-dessus. Cependant, pour chaque "étape", cette méthode nécessite de parcourir linéairement la liste de toutes les transitions différentes.

Pour les exemples ci-dessus, il y aurait deux transitions de ce type:

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; }
));
7
leemes

Je ne sais pas si cela vous aurait permis de passer l'entretien, mais je m'abstiendrais personnellement de coder manuellement n'importe quelle machine d'état, surtout si c'est dans un cadre professionnel. Les machines d'état sont un problème bien étudié, et il existe des outils open source bien testés qui produisent souvent un code supérieur à ce que vous produirez vous-même à la main, et ils vous aident également à diagnostiquer les problèmes avec votre machine d'état par exemple. pouvoir générer automatiquement des diagrammes d'état.

Mes outils goto pour ce genre de problème sont:

5
unthought

J'ai écrit de nombreuses machines à états en utilisant ces méthodes. Mais quand j'ai écrit la bibliothèque d'émetteurs-récepteurs de Cisco pour le Nexus 7000 (un commutateur de 117 000 $), j'ai utilisé une méthode que j'ai inventée dans les années 80. Cela consistait à utiliser une macro qui fait ressembler la machine d'état à un code de blocage multitâche. Les macros sont écrites pour C mais je les ai utilisées avec de petites modifications pour C++ lorsque je travaillais pour Dell. Vous pouvez en savoir plus à ce sujet ici: https://www.codeproject.com/Articles/37037/Macros-to-simulate-multi-tasking-blocking-code-at

3
eddyq
#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;
}
2
himanshu mishra