Implementazione dello stato dell'oggetto in un linguaggio OO?


11

Mi è stato dato un codice Java da guardare, che simula una gara automobilistica, di cui include un'implementazione di una macchina a stati di base. Questa non è una classica macchina a stati informatici, ma semplicemente un oggetto che può avere più stati e può passare da uno stato all'altro in base a una serie di calcoli.

Per descrivere solo il problema, ho una classe Car, con una classe enum nidificata che definisce alcune costanti per lo stato dell'auto (come OFF, IDLE, DRIVE, REVERSE, ecc.). All'interno di questa stessa classe di auto ho una funzione di aggiornamento, che sostanzialmente consiste in una grande istruzione switch che attiva lo stato corrente delle auto, esegue alcuni calcoli e quindi cambia lo stato delle auto.

Per quanto posso vedere, lo stato di Cars viene utilizzato solo all'interno della sua stessa classe.

La mia domanda è: è questo il modo migliore di gestire l'implementazione di una macchina a stati della natura sopra descritta? Sembra la soluzione più ovvia, ma in passato ho sempre sentito che "le istruzioni switch sono cattive".

Il problema principale che posso vedere qui è che l'istruzione switch potrebbe diventare molto grande quando si aggiungono più stati (se ritenuto necessario) e il codice potrebbe diventare ingombrante e difficile da mantenere.

Quale sarebbe una soluzione migliore a questo problema?


3
La tua descrizione non mi sembra una macchina statale; suona semplicemente come un mucchio di oggetti auto, ognuno con il proprio stato interno. Valuta la possibilità di pubblicare il tuo vero codice funzionante su codereview.stackexchange.com ; quelle persone sono molto brave a fornire feedback sul codice di lavoro.
Robert Harvey,

Forse "macchina dello stato" è una cattiva scelta di parole, ma sì, fondamentalmente abbiamo un mucchio di oggetti auto che accendono il loro stato interno. Il sistema può essere descritto eloquentemente con un diagramma di stato UML, motivo per cui ho intitolato il mio post come tale. Col senno di poi, non è il modo migliore per descrivere il problema, modificherò il mio post.
PythonNewb,

1
Penso ancora che dovresti considerare di pubblicare il tuo codice su codereview.
Robert Harvey,

1
sembra una macchina statale per me. object.state = object.function(object.state);
Robert Bristow-Johnson

Tutte le risposte fornite finora, inclusa la risposta accettata, mancano il motivo principale per cui le dichiarazioni di scambio sono considerate errate. Non consentono l'adesione al principio aperto / chiuso.
Dunk

Risposte:


13
  • Ho trasformato la Macchina in una sorta di macchina a stati usando State Pattern . Si noti che no switcho if-then-elseistruzioni vengono utilizzate per la selezione dello stato.

  • In questi casi tutti gli stati sono classi interne ma potrebbero essere implementati diversamente.

  • Ogni stato contiene gli stati validi in cui può essere modificato.

  • All'utente viene richiesto lo stato successivo nel caso in cui sia possibile più di uno o semplicemente per confermare nel caso in cui sia possibile solo uno.

  • Puoi compilarlo ed eseguirlo per testarlo.

  • Ho usato una finestra di dialogo grafica perché era più semplice eseguirla in modo interattivo in Eclipse.

inserisci qui la descrizione dell'immagine

Il diagramma UML è preso da qui .

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

}

1
Mi piace molto questo. Mentre apprezzo la risposta migliore ed è la difesa delle dichiarazioni switch (lo ricorderò per sempre ora), mi piace molto l'idea di questo modello. Grazie
PythonNewb,

@PythonNewb L'hai eseguito?
Tulains Córdova,

Sì, funziona perfettamente. L'implementazione sarà leggermente diversa per il codice che ho, ma l'idea generale è fantastica. Penso che potrei prendere in considerazione l'idea di spostare le classi di stato fuori dalla classe che la racchiude.
PythonNewb,

1
@PythonNewb Ho modificato il codice in una versione più breve riutilizzando lo stato / prompt di modifica per la logica di input utilizzando una classe astratta anziché un'interfaccia. È più corto di 20 righe ma ho provato e funziona allo stesso modo. Puoi sempre ottenere la versione più vecchia e più lunga guardando la cronologia delle modifiche.
Tulains Córdova,

1
@Caleth In effetti l'ho scritto in questo modo perché di solito lo faccio nella vita reale, cioè immagazzino pezzi intercambiabili nelle mappe e li ottengo in base agli ID caricati da un file di parametri. Di solito ciò che immagazzino nelle mappe non sono gli oggetti stessi ma i loro creatori se gli oggetti sono costosi o hanno molto stato non statico.
Tulains Córdova,

16

le istruzioni switch sono sbagliate

È questo tipo di semplificazione eccessiva che dà un cattivo nome alla programmazione orientata agli oggetti. L'uso ifè "cattivo" come l'uso di un'istruzione switch. In entrambi i casi non stai inviando polimorficamente.

Se devi avere una regola che si adatta a un morso sonoro prova questa:

Le istruzioni Switch diventano molto brutte nel momento in cui ne hai due copie.

Un'istruzione switch che non è duplicata in nessun altro punto della base di codice a volte può riuscire a non essere cattiva. Se i casi non sono pubblici, ma sono incapsulati, in realtà non sono affari di nessun altro. Soprattutto se sai come e quando trasformarlo in classi. Solo perché puoi non significa che devi farlo. È perché puoi che è meno critico farlo adesso.

Se ti ritrovi a provare a inserire sempre più cose nell'istruzione switch, a diffondere la conoscenza dei casi o a desiderare che non sia così male farne una copia, è tempo di trasformare i casi in classi separate.

Se hai tempo di leggere più di alcuni morsi sonori sul refactoring delle istruzioni switch, c2 ha una pagina molto ben bilanciata sull'odore delle istruzioni switch .

Anche nel codice OOP, non tutti gli switch sono danneggiati. È come lo stai usando e perché.


2

L'auto è un tipo di macchina statale. Le istruzioni switch sono il modo più semplice per implementare una macchina a stati priva di super stati e stati secondari.


2

Le istruzioni switch non sono male. Non ascoltare le persone che dicono cose come "scambiare le dichiarazioni sono cattive"! Alcuni usi particolari delle istruzioni switch sono un antipattern, come usare switch per emulare la sottoclasse. (Ma puoi anche implementare questo antipattern con if's, quindi immagino che anche se lo siano anche male!).

L'implementazione suona bene. Hai ragione è difficile da mantenere se aggiungi molti altri stati. Ma questo non è solo un problema di implementazione: avere un oggetto con molti stati con comportamenti diversi è esso stesso un problema. L'imaging della tua auto ha 25 stati, ciascuno con comportamenti diversi e regole diverse per le transizioni di stato. Giusto specificare e documentare questo comportamento sarebbe un compito enorme. Avrai migliaia di regole per la transizione di stato! La dimensione del switchsarebbe solo un sintomo di un problema più grande. Quindi, se possibile, evitare di percorrere questa strada.

Un possibile rimedio è di suddividere lo stato in sottostati indipendenti. Ad esempio, REVERSE è davvero uno stato distinto da DRIVE? Forse gli stati dell'auto potrebbero essere suddivisi in due: stato del motore (OFF, IDLE, DRIVE) e direzione (FORWARD, REVERSE). Lo stato e la direzione del motore saranno probabilmente per lo più indipendenti, quindi ridurrete la duplicazione logica e le regole di transizione dello stato. Più oggetti con meno stati sono molto più facili da gestire rispetto a un singolo oggetto con numerosi stati.


1

Nel tuo esempio, le auto sono semplicemente macchine a stati nel classico senso dell'informatica. Hanno un piccolo insieme ben definito di stati e una sorta di logica di transizione di stato.

Il mio primo suggerimento è di considerare di rompere la logica di transizione nella propria funzione (o classe, se la tua lingua non supporta funzioni di prima classe).

Il mio secondo suggerimento è quello di considerare di rompere la logica di transizione nello stato stesso, che avrebbe una sua funzione (o classe, se la tua lingua non supporta funzioni di prima classe).

In entrambi gli schemi, il processo per lo stato di transizione sarebbe simile a questo:

mycar.transition()

o

mycar.state.transition()

Il secondo potrebbe, ovviamente, essere banalmente avvolto nella classe dell'auto per assomigliare al primo.

In entrambi gli scenari, l'aggiunta di un nuovo stato (ad esempio DRAFTING) implicherebbe solo l'aggiunta di un nuovo tipo di oggetto stato e la modifica degli oggetti che passano specificamente al nuovo stato.


0

Dipende da quanto switchpotrebbe essere grande.

Nel tuo esempio, penso che a switchsia OK dato che non ci sono davvero altri stati a cui posso pensare che Carpotresti avere, quindi non si ingrandirà nel tempo.

Se l'unico problema è avere un interruttore di grandi dimensioni in cui ognuno caseha molte istruzioni, quindi crea metodi privati ​​distinti per ciascuno.

A volte le persone suggeriscono il modello di progettazione dello stato , ma è più appropriato quando si ha a che fare con una logica complessa e gli stati prendono decisioni aziendali diverse per molte operazioni distinte. Altrimenti, i problemi semplici dovrebbero avere soluzioni semplici.

In alcuni scenari, è possibile disporre di metodi che eseguono attività solo quando lo stato è A o B, ma non C o D, oppure avere più metodi con operazioni molto semplici che dipendono dallo stato. Quindi una o più switchaffermazioni sarebbero migliori.


0

Sembra una vecchia macchina dello stato del tipo usato prima che qualcuno eseguisse la programmazione orientata agli oggetti, e tanto meno Design Patterns. Può essere implementato in qualsiasi linguaggio con istruzioni switch, come C.

Come altri hanno già detto, non c'è nulla di intrinsecamente sbagliato nelle istruzioni switch. Le alternative sono spesso più complicate e più difficili da capire.

A meno che il numero di casi di switch non diventi ridicolmente grande, la cosa può rimanere abbastanza gestibile. Il primo passo per mantenerlo leggibile è sostituire il codice in ogni caso con una chiamata di funzione per implementare il comportamento dello stato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.