Implémentation de l'état d'objet dans un langage OO?

11

On m'a donné du code Java à regarder, qui simule une course de voitures, dont une implémentation d'une machine d'état de base. Il ne s'agit pas d'une machine à états informatique classique, mais simplement d'un objet qui peut avoir plusieurs états et qui peut basculer entre ses états en fonction d'une série de calculs.

Pour décrire le problème, j'ai une classe Car, avec une classe enum imbriquée qui définit certaines constantes pour l'état de la voiture (telles que OFF, IDLE, DRIVE, REVERSE, etc.). Dans cette même classe de voitures, j'ai une fonction de mise à jour, qui consiste essentiellement en une grande déclaration de commutateur qui active l'état actuel des voitures, effectue des calculs, puis modifie l'état des voitures.

Pour autant que je puisse voir, l'état Cars n'est utilisé que dans sa propre classe.

Ma question est la suivante: est-ce la meilleure façon de traiter la mise en œuvre d'une machine à états de la nature décrite ci-dessus? Cela semble être la solution la plus évidente, mais dans le passé, j'ai toujours entendu dire que "les instructions de commutation sont mauvaises".

Le principal problème que je peux voir ici est que l'instruction switch peut devenir très volumineuse à mesure que nous ajoutons plus d'états (si cela est jugé nécessaire) et que le code peut devenir lourd et difficile à maintenir.

Quelle serait une meilleure solution à ce problème?

PythonNewb
la source
3
Votre description ne me semble pas être une machine d'état; cela ressemble simplement à un tas d'objets de voiture, chacun ayant son propre état interne. Pensez à publier votre code de travail réel sur codereview.stackexchange.com ; ces gens sont très bons pour fournir des commentaires sur le code de travail.
Robert Harvey
Peut-être que "machine d'état" est un mauvais choix de mots, mais oui, fondamentalement, nous avons un tas d'objets de voiture qui commutent sur leur propre état interne. Le système peut être décrit avec éloquence avec un diagramme d'état UML, c'est pourquoi j'ai intitulé mon message en tant que tel. Avec le recul, ce n'est pas la meilleure façon de décrire le problème, je vais modifier mon message.
PythonNewb
1
Je pense toujours que vous devriez envisager de publier votre code dans codereview.
Robert Harvey
1
sonne comme une machine d'état pour moi. object.state = object.function(object.state);
robert bristow-johnson
Toutes les réponses données jusqu'à présent, y compris la réponse acceptée, manquent la principale raison pour laquelle les déclarations de changement sont considérées comme mauvaises. Ils ne permettent pas d'adhérer au principe ouvert / fermé.
Dunk

Réponses:

13
  • J'ai transformé la voiture en une sorte de machine à états en utilisant State Pattern . Remarquez qu'aucune switchou aucune if-then-elseinstruction n'est utilisée pour la sélection de l'état.

  • Dans ce cas, tous les états sont des classes internes, mais cela pourrait être implémenté autrement.

  • Chaque état contient les états valides dans lesquels il peut changer.

  • L'utilisateur est invité à indiquer l'état suivant si plusieurs sont possibles, ou simplement à confirmer si un seul est possible.

  • Vous pouvez le compiler et l'exécuter pour le tester.

  • J'ai utilisé une boîte de dialogue graphique car c'était plus facile de cette façon de l'exécuter de manière interactive dans Eclipse.

entrez la description de l'image ici

Le diagramme UML est extrait d' ici .

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JOptionPane;

public class Car {

    private State state;
    public static final int ST_OFF=0;
    public static final int ST_IDDLE=1;
    public static final int ST_DRIVE=2;
    public static final int ST_REVERSE=3;

    Map<Integer,State> states=new HashMap<Integer,State>();

    public Car(){
        this.states.put(Car.ST_OFF, new Off());
        this.states.put(Car.ST_IDDLE, new Idle());
        this.states.put(Car.ST_DRIVE, new Drive());
        this.states.put(Car.ST_REVERSE, new Reverse()); 
        this.state=this.states.get(Car.ST_OFF);
    }

    private abstract class State{

        protected List<Integer> nextStates = new ArrayList<Integer>();

        public abstract void handle();
        public abstract void change();

        protected State promptForState(String prompt){
            State s = state;
            String word = JOptionPane.showInputDialog(prompt);
            int ch = -1;
            try {
                ch = Integer.parseInt(word);
            }catch (NumberFormatException e) {
            }   

            if (this.nextStates.contains(ch)){
                s=states.get(ch);
            } else {
                System.out.println("Invalid option");
            }
            return s;               
        }       

    }

    private class Off extends State{

        public Off(){ 
            super.nextStates.add(Car.ST_IDDLE);             
        }

        public void handle() { System.out.println("Stopped");}

        public void change() {
            state = this.promptForState("Stopped, iddle="+Car.ST_IDDLE+": ");
        }

    }

    private class Idle extends State{
        private List<Integer> nextStates = new ArrayList<Integer>();
        public Idle(){
            super.nextStates.add(Car.ST_DRIVE);
            super.nextStates.add(Car.ST_REVERSE);
            super.nextStates.add(Car.ST_OFF);       
        }

        public void handle() {  System.out.println("Idling");}

        public void change() { 
            state=this.promptForState("Idling, enter 0=off 2=drive 3=reverse: ");
        }

    }

    private class Drive extends State{

        private List<Integer> nextStates = new ArrayList<Integer>();
        public Drive(){
            super.nextStates.add(Car.ST_IDDLE);
        }       
        public void handle() {System.out.println("Driving");}

        public void change() {
            state=this.promptForState("Idling, enter 1=iddle: ");
        }       
    }

    private class Reverse extends State{
        private List<Integer> nextStates = new ArrayList<Integer>();
        public Reverse(){ 
            super.nextStates.add(Car.ST_IDDLE);
        }           
        public void handle() {System.out.println("Reversing");} 

        public void change() {
            state = this.promptForState("Reversing, enter 1=iddle: ");
        }       
    }

    public void request(){
        this.state.handle();
    }

    public void changeState(){
        this.state.change();
    }

    public static void main (String args[]){
        Car c = new Car();
        c.request(); //car is stopped
        c.changeState();
        c.request(); // car is iddling
        c.changeState(); // prompts for next state
        c.request(); 
        c.changeState();
        c.request();    
        c.changeState();
        c.request();        
    }

}
Tulains Córdova
la source
1
J'aime vraiment ça. Bien que j'apprécie la meilleure réponse et sa défense des instructions de commutation (je m'en souviendrai toujours), j'aime vraiment vraiment l'idée de ce modèle. Merci
PythonNewb
@PythonNewb L'avez-vous exécuté?
Tulains Córdova
Oui, cela fonctionne parfaitement. L'implémentation sera légèrement différente pour le code que j'ai, mais l'idée générale est géniale. Je pense que je pourrais envisager de déplacer les classes d'état hors de la classe englobante.
PythonNewb
1
@PythonNewb J'ai changé le code en une version plus courte réutilisant l'état de changement / invite pour la logique d'entrée en utilisant une classe abstraite au lieu d'une interface. C'est 20 lignes plus courtes mais j'ai testé et fonctionne de la même manière. Vous pouvez toujours obtenir l'ancienne version plus longue en consultant l'historique des modifications.
Tulains Córdova
1
@Caleth En fait, je l'ai écrit comme ça parce que je le fais habituellement dans la vraie vie, c'est-à-dire stocker des pièces interchangeables dans des cartes et les obtenir en fonction des identifiants chargés à partir d'un fichier de paramètres. Habituellement, ce que je stocke dans les cartes n'est pas les objets eux-mêmes mais leurs créateurs si les objets sont chers ou ont beaucoup d'état non statique.
Tulains Córdova
16

les instructions de commutation sont mauvaises

C'est ce genre de simplification excessive qui donne un mauvais nom à la programmation orientée objet. L'utilisation ifest tout aussi "mauvaise" que l'utilisation d'une instruction switch. Quoi qu'il en soit, vous ne répartissez pas de manière polymorphe.

Si vous devez avoir une règle qui rentre dans une morsure saine, essayez celle-ci:

Les instructions Switch deviennent très mauvaises au moment où vous en avez deux copies.

Une instruction switch qui n'est pas dupliquée ailleurs dans la base de code peut parfois réussir à ne pas être mauvaise. Si les cas ne sont pas publics, mais sont encapsulés, ce n'est vraiment l'affaire de personne d'autre. Surtout si vous savez comment et quand le refactoriser en cours. Ce n'est pas parce que vous le pouvez que vous le devez. C'est parce que vous le pouvez qu'il est moins essentiel de le faire maintenant.

Si vous vous retrouvez à essayer d'introduire de plus en plus de choses dans l'instruction switch, à diffuser les connaissances sur les cas, ou à souhaiter que ce ne soit pas si mal d'en faire une copie, alors il est temps de refactoriser les cas en classes distinctes.

Si vous avez le temps de lire plus de quelques extraits sonores sur la refactorisation des instructions switch, c2 a une page très bien équilibrée sur l' odeur de l'instruction switch .

Même dans le code POO, tous les commutateurs ne sont pas mauvais. C'est comment vous l'utilisez et pourquoi.

candied_orange
la source
2

La voiture est un type de machine d'état. Les instructions switch sont le moyen le plus simple d'implémenter une machine à états dépourvue de super états et de sous-états.

Frank Hileman
la source
2

Les instructions de commutation ne sont pas mauvaises. N'écoutez pas les gens qui disent des choses comme "changer les déclarations sont mauvaises"! Certaines utilisations particulières des instructions switch sont un contre-modèle, comme l'utilisation de switch pour émuler le sous-classement. (Mais vous pouvez également implémenter cet antipattern avec des ifs, donc je suppose que les ifs sont mauvais aussi!).

Votre implémentation sonne bien. Vous avez raison, il sera difficile à maintenir si vous ajoutez de nombreux autres états. Mais ce n'est pas seulement une question d'implémentation - avoir un objet avec de nombreux états avec un comportement différent est en soi un problème. L'imagerie de votre voiture a 25 états, chacun présentant un comportement différent et des règles de transition d'état différentes. Préciser et documenter ce comportement serait une tâche énorme. Vous aurez des milliers de règles de transition d'état! La taille de la switchne serait qu'un symptôme d'un problème plus important. Donc, si possible, évitez de suivre cette voie.

Un remède possible consiste à diviser l'État en sous-états indépendants. Par exemple, REVERSE est-il vraiment un état distinct de DRIVE? Peut-être que les états de la voiture pourraient être divisés en deux: l'état du moteur (OFF, IDLE, DRIVE) et la direction (FORWARD, REVERSE). L'état et la direction du moteur seront probablement principalement indépendants, vous réduisez donc la duplication logique et les règles de transition d'état. Plus d'objets avec moins d'états sont beaucoup plus faciles à gérer qu'un seul objet avec de nombreux états.

JacquesB
la source
1

Dans votre exemple, les voitures sont simplement des machines d'État au sens classique de l'informatique. Ils ont un petit ensemble d'états bien définis et une sorte de logique de transition d'état.

Ma première suggestion est d'envisager de décomposer la logique de transition en sa propre fonction (ou classe, si votre langage ne prend pas en charge les fonctions de première classe).

Ma deuxième suggestion est d'envisager de décomposer la logique de transition en l'état lui-même, qui aurait sa propre fonction (ou classe, si votre langage ne prend pas en charge les fonctions de première classe).

Dans les deux cas, le processus de transition de l'état ressemblerait à ceci:

mycar.transition()

ou

mycar.state.transition()

La seconde pourrait bien sûr être insignifiante dans la catégorie des voitures pour ressembler à la première.

Dans les deux scénarios, l'ajout d'un nouvel état (par exemple, RÉDACTION), impliquerait uniquement l'ajout d'un nouveau type d'objet d'état et la modification des objets qui basculent spécifiquement vers le nouvel état.

Joel Harmon
la source
0

Cela dépend de sa taille switch.

Dans votre exemple, je pense que switchc'est OK car il n'y a pas vraiment d'autre état auquel je puisse penser que vous Carpourriez avoir, donc il ne s'aggraverait pas avec le temps.

Si le seul problème est d'avoir un grand commutateur où chacun casea beaucoup d'instructions, alors créez simplement des méthodes privées distinctes pour chacun.

Parfois, les gens suggèrent le modèle de conception de l' état , mais il est plus approprié lorsque vous traitez avec une logique complexe et que les États prennent des décisions commerciales différentes pour de nombreuses opérations distinctes. Sinon, les problèmes simples devraient avoir des solutions simples.

Dans certains scénarios, vous pouvez avoir des méthodes qui n'effectuent des tâches que lorsque l'état est A ou B, mais pas C ou D, ou plusieurs méthodes avec des opérations très simples qui dépendent de l'état. Ensuite, une ou plusieurs switchdéclarations seraient mieux.

Maxim Bernard
la source
0

Cela ressemble à une machine d'état de la vieille école du type qui était utilisé avant que quiconque ne fasse de la programmation orientée objet, sans parler des modèles de conception. Il peut être implémenté dans n'importe quel langage contenant des instructions switch, comme C.

Comme d'autres l'ont dit, il n'y a rien de fondamentalement mauvais avec les instructions switch. Les alternatives sont souvent plus compliquées et plus difficiles à comprendre.

À moins que le nombre de boîtiers de commutation ne devienne ridiculement grand, la chose peut rester tout à fait gérable. La première étape pour le garder lisible consiste à remplacer le code dans chaque cas par un appel de fonction pour implémenter le comportement de l'état.

Simon B
la source