Implementierung des Objektstatus in einer OO-Sprache?


11

Ich habe mir Java-Code zum Anschauen gegeben, der ein Autorennen simuliert, zu dem auch die Implementierung einer Basis-Zustandsmaschine gehört. Dies ist keine klassische Informatik-Zustandsmaschine, sondern lediglich ein Objekt, das mehrere Zustände haben kann und auf der Grundlage einer Reihe von Berechnungen zwischen seinen Zuständen wechseln kann.

Um nur das Problem zu beschreiben, habe ich eine Auto-Klasse mit einer verschachtelten Enum-Klasse, die einige Konstanten für den Zustand des Autos definiert (wie AUS, LEERLAUF, ANTRIEB, RÜCKWÄRTS usw.). Innerhalb derselben Fahrzeugklasse habe ich eine Aktualisierungsfunktion, die im Wesentlichen aus einer großen switch-Anweisung besteht, die den aktuellen Status des Fahrzeugs einschaltet, einige Berechnungen durchführt und dann den Fahrzeugstatus ändert.

Soweit ich sehen kann, wird der Cars-Status nur innerhalb seiner eigenen Klasse verwendet.

Meine Frage ist, ist dies der beste Weg, um mit der Implementierung einer Zustandsmaschine der oben beschriebenen Art umzugehen? Es klingt nach der naheliegendsten Lösung, aber in der Vergangenheit habe ich immer gehört, dass "switch-Anweisungen schlecht sind".

Das Hauptproblem, das ich hier sehen kann, ist, dass die switch-Anweisung möglicherweise sehr groß werden kann, wenn wir weitere Zustände hinzufügen (falls dies als notwendig erachtet wird) und der Code unhandlich und schwer zu warten sein kann.

Was wäre eine bessere Lösung für dieses Problem?


3
Ihre Beschreibung klingt für mich nicht nach einer Zustandsmaschine. es klingt nur wie ein Haufen Autoobjekte, von denen jedes seinen eigenen inneren Zustand hat. Erwägen Sie, Ihren tatsächlichen Arbeitscode auf codereview.stackexchange.com zu veröffentlichen . Diese Leute sind sehr gut darin, Feedback zum Arbeitscode zu geben.
Robert Harvey

Vielleicht ist "Zustandsmaschine" eine schlechte Wortwahl, aber ja, im Grunde haben wir eine Reihe von Autoobjekten, die ihren eigenen internen Zustand einschalten. Das System kann mit einem UML-Zustandsdiagramm eloquent beschrieben werden, weshalb ich meinen Beitrag als solchen betitelt habe. Im Nachhinein ist es nicht die beste Art, das Problem zu beschreiben. Ich werde meinen Beitrag bearbeiten.
PythonNewb

1
Ich denke immer noch, dass Sie erwägen sollten, Ihren Code in codereview zu veröffentlichen.
Robert Harvey

1
klingt für mich wie eine Zustandsmaschine. object.state = object.function(object.state);
Robert Bristow-Johnson

Alle bisher gegebenen Antworten, einschließlich der akzeptierten Antwort, verfehlen den Hauptgrund, warum switch-Anweisungen als schlecht angesehen werden. Sie erlauben keine Einhaltung des offenen / geschlossenen Prinzips.
Dunk

Antworten:


13
  • Ich habe das Auto mit State Pattern in eine Art Staatsmaschine verwandelt . Beachten Sie , dass für die Statusauswahl keine switchoder if-then-elseAnweisungen verwendet werden.

  • In diesen Fällen sind alle Zustände innere Klassen, aber es könnte anders implementiert werden.

  • Jeder Status enthält die gültigen Status, in die er geändert werden kann.

  • Der Benutzer wird aufgefordert, den nächsten Status einzugeben, falls mehr als einer möglich ist, oder einfach zu bestätigen, falls nur einer möglich ist.

  • Sie können es kompilieren und ausführen, um es zu testen.

  • Ich habe ein grafisches Dialogfeld verwendet, weil es auf diese Weise einfacher war, es interaktiv in Eclipse auszuführen.

Geben Sie hier die Bildbeschreibung ein

Das UML-Diagramm stammt von hier .

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
Das gefällt mir wirklich gut. Obwohl ich die beste Antwort und die Verteidigung von Switch-Anweisungen schätze (daran werde ich mich jetzt für immer erinnern), mag ich die Idee dieses Musters wirklich sehr. Vielen Dank
PythonNewb

@ PythonNewb Hast du es ausgeführt?
Tulains Córdova

Ja, es funktioniert perfekt. Die Implementierung wird für den Code, den ich habe, etwas anders sein, aber die allgemeine Idee ist großartig. Ich denke, ich könnte erwägen, die Zustandsklassen aus der umschließenden Klasse zu entfernen.
PythonNewb

1
@PythonNewb Ich habe den Code in eine kürzere Version geändert und den Änderungsstatus / die Eingabeaufforderung für die Eingabelogik mithilfe einer abstrakten Klasse anstelle einer Schnittstelle wiederverwendet. Es ist 20 Zeilen kürzer, aber ich habe getestet und funktioniert genauso. Sie können jederzeit die ältere, längere Version mit Blick auf den Bearbeitungsverlauf erhalten.
Tulains Córdova

1
@Caleth Tatsächlich habe ich es so geschrieben, weil ich das normalerweise im wirklichen Leben mache, dh austauschbare Teile in Karten speichere und sie basierend auf IDs abrufe, die aus einer Parameterdatei geladen wurden. Normalerweise speichere ich in den Karten nicht die Objekte selbst, sondern deren Ersteller, wenn die Objekte teuer sind oder viel nicht statischen Zustand haben.
Tulains Córdova

16

switch-Anweisungen sind schlecht

Es ist diese Art der Übervereinfachung, die der objektorientierten Programmierung einen schlechten Ruf verleiht. Die Verwendung ifist genauso "schlecht" wie die Verwendung einer switch-Anweisung. In beiden Fällen versenden Sie nicht polymorph.

Wenn Sie eine Regel haben müssen, die in einen gesunden Biss passt, versuchen Sie diese:

Switch-Anweisungen werden sehr schlecht, sobald Sie zwei Kopien davon haben.

Eine switch-Anweisung, die an keiner anderen Stelle in der Codebasis dupliziert wird, kann manchmal dazu führen, dass sie nicht böse ist. Wenn die Fälle nicht öffentlich sind, sondern gekapselt sind, geht es niemanden etwas an. Vor allem, wenn Sie wissen, wie und wann Sie es in Klassen umgestalten müssen. Nur weil du kannst, heißt das nicht, dass du musst. Es ist, weil Sie können, dass es weniger kritisch ist, es jetzt zu tun.

Wenn Sie versuchen, mehr und mehr Dinge in die switch-Anweisung zu schieben, das Wissen über Fälle zu verbreiten oder sich wünschen, es wäre nicht so schlimm, nur eine Kopie davon zu erstellen, ist es Zeit, die Fälle in separate Klassen umzugestalten.

Wenn Sie Zeit haben, mehr als ein paar Soundbits über das Refactoring von switch-Anweisungen zu lesen, hat c2 eine sehr ausgewogene Seite über den Geruch von switch-Anweisungen .

Selbst im OOP-Code ist nicht jeder Switch schlecht. So verwenden Sie es und warum.


2

Das Auto ist eine Art Zustandsmaschine. Switch-Anweisungen sind der einfachste Weg, eine Zustandsmaschine ohne Super- und Unterzustände zu implementieren.


2

Switch-Anweisungen sind nicht schlecht. Hören Sie nicht auf Leute, die Dinge wie "Schalteraussagen sind schlecht" sagen! Einige spezielle Verwendungen von switch-Anweisungen sind Antimuster, z. B. die Verwendung von switch zum Emulieren von Unterklassen. (Aber Sie können dieses Antimuster auch mit if's implementieren, also denke ich, wenn's auch schlecht sind!).

Ihre Implementierung klingt gut. Sie haben Recht, es wird schwierig zu pflegen, wenn Sie viele weitere Zustände hinzufügen. Dies ist jedoch nicht nur eine Frage der Implementierung - ein Objekt mit vielen Zuständen mit unterschiedlichem Verhalten ist selbst ein Problem. Stellen Sie sich vor, Ihr Auto hat 25 Zustände, in denen jeder ein anderes Verhalten und unterschiedliche Regeln für Zustandsübergänge aufweist. Nur dieses Verhalten zu spezifizieren und zu dokumentieren, wäre eine enorme Aufgabe. Sie werden Tausende von Regeln für den Staatsübergang haben! Die Größe des switchwäre nur ein Symptom für ein größeres Problem. Vermeiden Sie es also, wenn möglich, diesen Weg zu gehen.

Eine mögliche Abhilfe besteht darin, den Staat in unabhängige Unterzustände aufzuteilen. Ist REVERSE beispielsweise wirklich ein anderer Zustand als DRIVE? Möglicherweise könnten die Fahrzeugzustände in zwei Teile unterteilt werden: Motorzustand (AUS, LEERLAUF, ANTRIEB) und Richtung (VORWÄRTS, RÜCKWÄRTS). Der Motorstatus und die Motorrichtung sind wahrscheinlich weitgehend unabhängig voneinander, sodass Sie die Regeln für die Logikduplizierung und den Zustandsübergang reduzieren. Mehr Objekte mit weniger Status sind viel einfacher zu verwalten als ein einzelnes Objekt mit zahlreichen Status.


1

In Ihrem Beispiel sind Autos einfach Zustandsmaschinen im klassischen Sinne der Informatik. Sie haben eine kleine, genau definierte Menge von Zuständen und eine Art Zustandsübergangslogik.

Mein erster Vorschlag ist, die Übergangslogik in eine eigene Funktion (oder Klasse, wenn Ihre Sprache keine erstklassigen Funktionen unterstützt) aufzuteilen.

Mein zweiter Vorschlag ist, die Übergangslogik in den Zustand selbst aufzuteilen, der eine eigene Funktion haben würde (oder eine Klasse, wenn Ihre Sprache keine erstklassigen Funktionen unterstützt).

In beiden Schemata würde der Prozess zum Übergang des Zustands ungefähr so ​​aussehen:

mycar.transition()

oder

mycar.state.transition()

Der zweite könnte natürlich trivial in die Autoklasse eingewickelt werden, um wie der erste auszusehen.

In beiden Szenarien würde das Hinzufügen eines neuen Status (z. B. ENTWURF) nur das Hinzufügen eines neuen Typs von Statusobjekten und das Ändern der Objekte umfassen, die speziell in den neuen Status wechseln.


0

Es hängt davon ab, wie groß die sein switchkönnte.

In Ihrem Beispiel denke ich, dass a switchin Ordnung ist, da ich mir keinen anderen Zustand vorstellen kann, den Sie haben Carkönnten, sodass er mit der Zeit nicht größer wird.

Wenn das einzige Problem darin besteht, einen großen Switch zu haben, in dem jeder caseviele Anweisungen enthält, erstellen Sie einfach unterschiedliche private Methoden für jeden.

Manchmal schlagen die Leute das Zustandsdesignmuster vor , aber es ist angemessener, wenn Sie sich mit komplexer Logik befassen und Staaten unterschiedliche Geschäftsentscheidungen für viele verschiedene Vorgänge treffen. Ansonsten sollten einfache Probleme einfache Lösungen haben.

In einigen Szenarien können Methoden vorhanden sein, die nur Aufgaben ausführen, wenn der Status A oder B, nicht jedoch C oder D ist, oder mehrere Methoden mit sehr einfachen Operationen, die vom Status abhängen. Dann switchwären eine oder mehrere Aussagen besser.


0

Dies klingt wie eine Zustandsmaschine der alten Schule, wie sie verwendet wurde, bevor jemand objektorientierte Programmierung durchführte, geschweige denn Entwurfsmuster. Es kann in jeder Sprache implementiert werden, die switch-Anweisungen enthält, wie z. B. C.

Wie andere gesagt haben, ist an switch-Anweisungen nichts von Natur aus falsch. Die Alternativen sind oft komplizierter und schwerer zu verstehen.

Wenn die Anzahl der Switch-Fälle nicht lächerlich groß wird, kann das Ding ziemlich überschaubar bleiben. Der erste Schritt, um es lesbar zu halten, besteht darin, den Code jeweils durch einen Funktionsaufruf zu ersetzen, um das Verhalten des Zustands zu implementieren.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.