Реализация состояния объекта на ОО языке?

11

Мне дали посмотреть на Java-код, который имитирует гонку автомобиля, включая реализацию базового автомата. Это не классическая машина состояний компьютерных наук, а просто объект, который может иметь несколько состояний и может переключаться между его состояниями на основе серии вычислений.

Чтобы описать только проблему, у меня есть класс Car с вложенным классом enum, который определяет некоторые константы для состояния Car (например, OFF, IDLE, DRIVE, REVERSE и т. Д.). В этом же классе автомобилей у меня есть функция обновления, которая в основном состоит из большого оператора switch, который включает текущее состояние автомобилей, выполняет некоторые вычисления и затем изменяет состояние автомобилей.

Насколько я вижу, состояние Cars используется только в своем классе.

Мой вопрос заключается в том, является ли это лучшим способом реализации конечного автомата природы, описанного выше? Это звучит как наиболее очевидное решение, но в прошлом я всегда слышал, что «операторы переключения плохие».

Основная проблема, которую я вижу здесь, заключается в том, что оператор switch может стать очень большим, если мы добавим больше состояний (если это будет сочтено необходимым), а код станет громоздким и сложным в обслуживании.

Что было бы лучшим решением этой проблемы?

PythonNewb
источник
3
Ваше описание не похоже на конечный автомат для меня; это просто звучит как куча автомобильных объектов, каждый из которых имеет свое внутреннее состояние. Рассмотрите возможность размещения своего действующего рабочего кода на codereview.stackexchange.com ; эти люди очень хороши в предоставлении отзывов о рабочем коде.
Роберт Харви
Возможно, «конечный автомат» - плохой выбор слов, но да, в основном, у нас есть куча автомобильных объектов, которые переключаются в собственное внутреннее состояние. Систему можно красноречиво описать диаграммой состояний UML, поэтому я назвал свой пост как таковой. Оглядываясь назад, это не лучший способ описать проблему, я отредактирую свой пост.
PythonNewb
1
Я все еще думаю, что вы должны рассмотреть возможность размещения своего кода в codereview.
Роберт Харви,
1
звучит как государственная машина для меня. object.state = object.function(object.state);
Роберт Бристоу-Джонсон
Все приведенные до сих пор ответы, в том числе принятый ответ, не содержат основной причины, по которой заявления о переключении считаются плохими. Они не допускают соблюдения принципа открытого / закрытого.
Данк

Ответы:

13
  • Я превратил Автомобиль в своего рода конечный автомат, используя State Pattern . Notice no switchили if-then-elseоператоры используются для выбора состояния.

  • В этом случае все состояния являются внутренними классами, но это может быть реализовано иначе.

  • Каждое состояние содержит действительные состояния, на которые оно может измениться.

  • Пользователю предлагается указать следующее состояние, если возможно более одного, или просто подтвердить, если возможно только одно.

  • Вы можете скомпилировать и запустить его для проверки.

  • Я использовал графическое диалоговое окно, потому что таким способом было проще запустить его в Eclipse.

введите описание изображения здесь

Диаграмма UML взята отсюда .

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
Мне действительно это нравится. Несмотря на то, что я ценю лучший ответ и его защиту высказываний переключателя (теперь я навсегда запомню это), мне действительно очень нравится идея этого шаблона. Спасибо
PythonNewb
@PythonNewb Ты его запускал?
Тулаинс Кордова
Да, работает отлично. Реализация будет немного отличаться для моего кода, но общая идея великолепна. Я думаю, что я мог бы рассмотреть вопрос о перемещении классов состояния из класса включения, хотя.
PythonNewb
1
@PythonNewb Я изменил код на более короткую версию, повторно используя состояние / приглашение изменения для логики ввода, используя абстрактный интерфейс вместо интерфейса. Это на 20 строк короче, но я тестировал и работает так же. Вы всегда можете получить более старую, более длинную версию, просматривая историю изменений.
Тулаинс Кордова
1
@Caleth На самом деле я написал это так, потому что я обычно делаю это в реальной жизни, то есть храню взаимозаменяемые фрагменты на картах и ​​получаю их на основе идентификаторов, загруженных из файла параметров. Обычно на картах я храню не сами объекты, а их создателей, если они дорогие или имеют много нестатического состояния.
Тулин Кордова
16

Смена заявления плохая

Именно такое упрощение дает плохое имя объектно-ориентированному программированию. Использование ifтак же «плохо», как использование оператора switch. В любом случае вы не полиморфно отправляете.

Если у вас должно быть правило, подходящее под звуки, попробуйте следующее:

Операторы переключения становятся очень плохими, когда у вас есть две их копии.

Оператор switch, который не дублируется нигде в базе кода, иногда может не быть злым. Если дела не являются публичными, но заключены в капсулу, это действительно ничто другое. Особенно, если вы знаете, как и когда проводить рефакторинг в классах. То, что ты можешь, не означает, что ты должен. Это потому, что вы можете сделать это менее критично.

Если вы обнаружите, что пытаетесь внедрить все больше и больше материала в оператор switch, распространяете знания о случаях вокруг или хотите, чтобы это не было так просто, просто скопируйте его, тогда пришло время реорганизовать случаи в отдельные классы.

Если у вас есть время, чтобы прочитать несколько цитат о рефакторинге операторов switch, у c2 есть очень хорошо сбалансированная страница о запахе операторов switch .

Даже в коде ООП не каждый переключатель плох. Это то, как вы используете это и почему.

candied_orange
источник
2

Автомобиль является типом государственной машины. Операторы переключения - это самый простой способ реализовать конечный автомат, в котором отсутствуют суперсостояния и субсостояния.

Фрэнк Хилман
источник
2

Смена оператора не плохая. Не слушайте людей, которые говорят такие вещи, как «переключение оценок плохое»! Некоторые конкретные применения операторов switch являются антипаттернами, например использование switch для эмуляции подклассов. (Но вы также можете реализовать этот антипаттерн с помощью if, так что я думаю, что если тоже плохие!).

Ваша реализация звучит нормально. Вы правы, это будет трудно поддерживать, если вы добавите еще много состояний. Но это не просто вопрос реализации - наличие объекта со многими состояниями с различным поведением само по себе является проблемой. Во время визуализации в вашем автомобиле было 25 состояний, в каждом из которых было разное поведение и разные правила перехода между состояниями. Просто указать и задокументировать это поведение было бы огромной задачей. У вас будут тысячи правил перехода состояний! Размер switchбудет просто симптомом более крупной проблемы. Поэтому, если возможно, избегайте идти по этому пути.

Возможное решение - разбить государство на независимые подсостояния. Например, действительно ли REVERSE отличается от DRIVE? Возможно, состояние автомобиля может быть разбито на две части: состояние двигателя (ВЫКЛ, ОЖИДАНИЕ, ПРИВОД) и направление (ВПЕРЕД, ОБРАТНО). Состояние и направление движка, вероятно, будут в основном независимыми, поэтому вы уменьшите дублирование логики и правила перехода состояний. Большим количеством объектов с меньшим количеством состояний управлять намного проще, чем одному объекту с множеством состояний.

JacquesB
источник
1

В вашем примере автомобили - это просто конечные автоматы в классическом понимании информатики. У них есть небольшой, четко определенный набор состояний и какая-то логика перехода состояний.

Мое первое предложение состоит в том, чтобы рассмотреть возможность разбиения логики перехода на собственную функцию (или класс, если ваш язык не поддерживает функции первого класса).

Мое второе предложение - рассмотреть возможность разбиения логики перехода на само состояние, которое будет иметь свою собственную функцию (или класс, если ваш язык не поддерживает функции первого класса).

В любой схеме процесс перехода состояния будет выглядеть примерно так:

mycar.transition()

или же

mycar.state.transition()

Второе можно, конечно, тривиально обернуть в классе автомобилей, чтобы выглядеть как первое.

В обоих сценариях добавление нового состояния (скажем, DRAFTING) будет включать только добавление нового типа объекта состояния и изменение объектов, которые конкретно переключаются в новое состояние.

Джоэл Хармон
источник
0

Это зависит от того, насколько большой switchможет быть.

В вашем примере, я думаю, что все в switchпорядке, потому что нет другого состояния, которое я мог бы придумать, которое вы Carмогли бы иметь, поэтому со временем оно не увеличится.

Если единственной проблемой является наличие большого переключателя, в котором у каждого caseесть много инструкций, то просто создайте для каждого из них индивидуальные закрытые методы.

Иногда люди предлагают шаблон проектирования состояния , но он более уместен, когда вы имеете дело со сложной логикой и состояниями, принимающими различные бизнес-решения для множества различных операций. В противном случае простые проблемы должны иметь простые решения.

В некоторых сценариях у вас могут быть методы, которые выполняют задачи, только когда состояние A или B, но не C или D, или есть несколько методов с очень простыми операциями, которые зависят от состояния. Тогда одно или несколько switchутверждений будет лучше.

Максим Бернард
источник
0

Это звучит как машина состояния старой школы, которая использовалась до того, как кто-либо занимался объектно-ориентированным программированием, не говоря уже о шаблонах проектирования. Это может быть реализовано на любом языке, который имеет операторы switch, например C.

Как уже говорили другие, в выражениях switch нет ничего плохого. Альтернативы часто являются более сложными и трудными для понимания.

Пока количество случаев переключения не станет смехотворно большим, вещь может оставаться вполне управляемой. Первым шагом в поддержании его читабельности является замена кода в каждом случае вызовом функции для реализации поведения состояния.

Саймон Б
источник