Implementacja stanu obiektu w języku OO?

11

Dostałem trochę kodu Java do obejrzenia, który symuluje wyścig samochodowy, w tym implementację podstawowej maszyny stanu. To nie jest klasyczna machina stanów komputerowych, lecz jedynie obiekt, który może mieć wiele stanów i może przełączać się między tymi stanami na podstawie serii obliczeń.

Aby opisać tylko problem, mam klasę samochodów z zagnieżdżoną klasą enum, która definiuje niektóre stałe dla stanu samochodu (takie jak OFF, IDLE, DRIVE, REVERSE itp.). W tej samej klasie samochodów mam funkcję aktualizacji, która zasadniczo składa się z dużej instrukcji przełączania, która włącza bieżący stan samochodu, wykonuje pewne obliczenia, a następnie zmienia stan samochodu.

Z tego, co widzę, stan Samochody jest używany tylko w swojej klasie.

Moje pytanie brzmi: czy jest to najlepszy sposób radzenia sobie z implementacją maszyny stanu o charakterze opisanym powyżej? Brzmi jak najbardziej oczywiste rozwiązanie, ale w przeszłości zawsze słyszałem, że „instrukcje przełączników są złe”.

Główny problem, jaki widzę tutaj, polega na tym, że instrukcja switch może stać się bardzo duża, ponieważ dodamy więcej stanów (jeśli zostanie to uznane za konieczne), a kod może stać się niewygodny i trudny do utrzymania.

Jakie byłoby lepsze rozwiązanie tego problemu?

PythonNewb
źródło
3
Twój opis nie brzmi dla mnie jak automat państwowy; to tylko brzmi jak garść obiektów samochodowych, z których każdy ma swój własny stan wewnętrzny. Rozważ opublikowanie aktualnego, działającego kodu na codereview.stackexchange.com ; ci ludzie są bardzo dobrzy w przekazywaniu opinii na temat działającego kodu.
Robert Harvey
Być może „maszyna stanowa” to zły wybór słów, ale tak, w zasadzie mamy kilka obiektów samochodowych, które włączają swój własny stan wewnętrzny. System można opisać elokwentnie za pomocą diagramu stanu UML, dlatego właśnie podtytuję swój post jako taki. Z perspektywy czasu nie jest to najlepszy sposób na opisanie problemu, zedytuję swój post.
PythonNewb
1
Nadal uważam, że powinieneś rozważyć opublikowanie kodu w widoku kodu.
Robert Harvey
1
brzmi dla mnie jak automat stanowy. object.state = object.function(object.state);
Robert Bristol-Johnnson
Wszystkie odpowiedzi udzielone do tej pory, w tym odpowiedź zaakceptowana, pomijają główny powód, dla którego instrukcje zamiany są uważane za złe. Nie pozwalają na przestrzeganie zasady otwartej / zamkniętej.
Dunk

Odpowiedzi:

13
  • Za pomocą State Pattern zmieniłem samochód w swego rodzaju maszynę stanową . Zauważ, że nie switchlub if-then-elseinstrukcje służą do wyboru stanu.

  • W takich przypadkach wszystkie stany są klasami wewnętrznymi, ale można je zaimplementować w inny sposób.

  • Każdy stan zawiera prawidłowe stany, w których może się zmienić.

  • Użytkownik jest monitowany o następny stan w przypadku, gdy więcej niż jeden jest możliwy, lub po prostu o potwierdzenie, jeśli tylko jeden jest możliwy.

  • Możesz go skompilować i uruchomić, aby go przetestować.

  • Użyłem graficznego okna dialogowego, ponieważ łatwiej było interaktywnie uruchamiać go w Eclipse.

wprowadź opis zdjęcia tutaj

Schemat UML jest pobierany stąd .

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
źródło
1
Naprawdę to lubię. Doceniam najlepszą odpowiedź i obronę instrukcji przełączania (teraz na zawsze to zapamiętam), naprawdę podoba mi się pomysł tego wzorca. Dziękuję
PythonNewb
@PythonNewb Uruchomiłeś to?
Tulains Córdova
Tak, działa idealnie. Implementacja będzie nieco inna dla mojego kodu, ale ogólny pomysł jest świetny. Myślę jednak, że mógłbym rozważyć przeniesienie klas stanowych poza klasę zamykającą.
PythonNewb
1
@PythonNewb Zmieniłem kod na krótszą wersję, ponownie wykorzystując stan zmiany / monit o logikę wprowadzania przy użyciu klasy abstrakcyjnej zamiast interfejsu. Jest o 20 linii krótszy, ale przetestowałem i działa tak samo. Zawsze możesz uzyskać starszą, dłuższą wersję, przeglądając historię edycji.
Tulains Córdova
1
@Caleth W rzeczywistości napisałem to w ten sposób, ponieważ zwykle robię to w prawdziwym życiu, tj. Przechowuję wymienne elementy na mapach i pobieram je na podstawie identyfikatorów załadowanych z pliku parametrów. Zwykle to, co przechowuję na mapach, to nie same obiekty, ale ich twórcy, jeśli obiekty są drogie lub mają dużo stanu niestatycznego.
Tulains Córdova
16

instrukcje switch są złe

To takie uproszczenie, które nadaje programowaniu obiektowemu złą nazwę. Używanie ifjest tak samo „złe” jak używanie instrukcji switch. Tak czy inaczej, nie wysyłamy polimorficznie.

Jeśli potrzebujesz reguły pasującej do zgryzu, wypróbuj tę:

Instrukcje przełączników stają się bardzo złe, gdy masz ich dwie kopie.

Instrukcja switch, która nie jest zduplikowana nigdzie indziej w bazie kodu, może czasem udać, że nie jest zła. Jeśli sprawy nie są publiczne, ale są zamknięte, to naprawdę nie jest to sprawa nikogo innego. Zwłaszcza jeśli wiesz, jak i kiedy podzielić to na klasy. To, że możesz, nie oznacza, że ​​musisz. To dlatego, że możesz, że mniej krytyczne jest zrobić to teraz.

Jeśli próbujesz wepchnąć coraz więcej rzeczy do instrukcji zamiany, rozpowszechniać wiedzę na temat spraw lub żałować, że nie było tak źle, aby po prostu zrobić kopię, to nadszedł czas, aby przekształcić sprawy w osobne klasy.

Jeśli masz czas, aby przeczytać więcej niż kilka dźwięków na temat refaktoryzacji instrukcji switch, c2 ma bardzo dobrze zrównoważoną stronę o zapachu instrukcji switch .

Nawet w kodzie OOP nie każdy przełącznik jest zły. Tak to wykorzystujesz i dlaczego.

candied_orange
źródło
2

Samochód jest rodzajem maszyny stanowej. Instrukcje switch są najprostszym sposobem na implementację maszyny stanów bez super stanów i pod stanów.

Frank Hileman
źródło
2

Instrukcje Switch nie są złe. Nie słuchaj ludzi, którzy mówią takie rzeczy, jak „przełączanie statystyk jest złe”! Niektóre szczególne zastosowania instrukcji switch są anty-wzorcowe, jak użycie switcha do emulacji podklasy. (Ale możesz również wdrożyć ten antypattern z ifs, więc chyba też są złe!).

Twoja implementacja brzmi dobrze. Masz rację, będzie trudna do utrzymania, jeśli dodasz wiele więcej stanów. Ale to nie jest tylko kwestia implementacji - posiadanie obiektu z wieloma stanami o różnych zachowaniach jest problemem. Obrazowanie twojego samochodu ma 25 stanów, z których każdy wykazywał inne zachowanie i różne reguły dotyczące zmian stanu. Samo określenie i udokumentowanie tego zachowania byłoby ogromnym zadaniem. Będziesz miał tysiące zasad dotyczących zmiany stanu! Rozmiar switchbyłby tylko objawem większego problemu. Jeśli to możliwe, unikaj schodzenia tą drogą.

Możliwym rozwiązaniem jest rozbicie stanu na niezależne podstacje. Na przykład, czy ODWRÓCENIE naprawdę różni się od DRIVE? Być może stany samochodu można podzielić na dwie części: stan silnika (WYŁ., Jałowy, NAPĘD) i kierunek (DO PRZODU, DO TYŁU). Stan i kierunek silnika będą prawdopodobnie w większości niezależne, więc ograniczysz powielanie logiki i reguły zmiany stanu. Więcej obiektów z mniejszą liczbą stanów jest łatwiejszych w zarządzaniu niż pojedynczy obiekt z wieloma stanami.

JacquesB
źródło
1

W twoim przykładzie samochody to po prostu maszyny stanowe w klasycznym sensie informatycznym. Mają mały, dobrze zdefiniowany zestaw stanów i pewną logikę przejścia stanu.

Moją pierwszą sugestią jest rozważenie podziału logiki przejścia na własną funkcję (lub klasę, jeśli twój język nie obsługuje funkcji pierwszej klasy).

Moją drugą sugestią jest rozważenie przełamania logiki przejścia do samego stanu, który miałby swoją własną funkcję (lub klasę, jeśli twój język nie obsługuje funkcji pierwszej klasy).

W obu schematach proces przejścia do stanu wyglądałby mniej więcej tak:

mycar.transition()

lub

mycar.state.transition()

Drugi może oczywiście być trywialnie zawinięty w klasie samochodów, aby wyglądał jak pierwszy.

W obu scenariuszach dodanie nowego stanu (powiedzmy DRAFTING) wymagałoby tylko dodania nowego typu obiektu stanu i zmiany obiektów, które konkretnie przełączają się na nowy stan.

Joel Harmon
źródło
0

To zależy od tego, jak duży switchmoże być.

W twoim przykładzie myślę, że a switchjest w porządku, ponieważ tak naprawdę nie ma żadnego innego stanu, który mógłbym wymyślić, abyś Carmógł mieć, więc z czasem nie zwiększyłby się.

Jeśli jedynym problemem jest posiadanie dużego przełącznika, w którym każdy casema wiele instrukcji, to po prostu określ odrębne prywatne metody dla każdego.

Czasami ludzie sugerują wzorzec projektowania stanu , ale jest bardziej odpowiedni, gdy masz do czynienia ze złożoną logiką i stanami podejmującymi różne decyzje biznesowe dla wielu różnych operacji. W przeciwnym razie proste problemy powinny mieć proste rozwiązania.

W niektórych scenariuszach możesz mieć metody, które wykonują zadania tylko wtedy, gdy stan to A lub B, ale nie C lub D, lub mieć wiele metod z bardzo prostymi operacjami zależnymi od stanu. Wtedy jedno lub kilka switchstwierdzeń byłoby lepszych.

Maxim Bernard
źródło
0

To brzmi jak oldschoolowy automat państwowy, który był używany, zanim ktokolwiek zaczął programować obiektowo, nie mówiąc już o projektowaniu wzorów. Może być zaimplementowany w dowolnym języku, który ma instrukcje przełączników, takim jak C.

Jak powiedzieli inni, nie ma nic złego w instrukcjach zamiany. Alternatywy są często bardziej skomplikowane i trudniejsze do zrozumienia.

O ile liczba przełączników nie stanie się absurdalnie duża, sprawa może pozostać całkiem do opanowania. Pierwszym krokiem do zachowania jego czytelności jest zastąpienie kodu za każdym razem wywołaniem funkcji w celu zaimplementowania zachowania stanu.

Simon B.
źródło