¿Implementación del estado del objeto en un lenguaje OO?

11

Me han dado un código Java para ver, que simula una carrera de autos, de los cuales incluye una implementación de una máquina de estado básica. Esta no es una máquina de estados de informática clásica, sino simplemente un objeto que puede tener múltiples estados y puede cambiar entre sus estados en función de una serie de cálculos.

Para describir solo el problema, tengo una clase Car, con una clase enum anidada que define algunas constantes para el estado del automóvil (como OFF, IDLE, DRIVE, REVERSE, etc.). Dentro de esta misma clase de automóviles, tengo una función de actualización, que básicamente consiste en una declaración de cambio grande que activa el estado actual de los automóviles, hace algunos cálculos y luego cambia el estado de los automóviles.

Por lo que puedo ver, el estado de Cars solo se usa dentro de su propia clase.

Mi pregunta es, ¿es esta la mejor manera de lidiar con la implementación de una máquina de estado de la naturaleza descrita anteriormente? Suena como la solución más obvia, pero en el pasado siempre he escuchado que "las declaraciones de cambio son malas".

El principal problema que puedo ver aquí es que la declaración de cambio podría volverse muy grande a medida que agregamos más estados (si se considera necesario) y el código podría volverse difícil de manejar y difícil de mantener.

¿Cuál sería una mejor solución para este problema?

PythonNewb
fuente
3
Su descripción no me parece una máquina de estados; simplemente suena como un montón de objetos de automóviles, cada uno con su propio estado interno. Considere publicar su código de trabajo actual en codereview.stackexchange.com ; Esas personas son muy buenas para proporcionar comentarios sobre el código de trabajo.
Robert Harvey
Quizás "máquina de estados" es una mala elección de palabras, pero sí, básicamente tenemos un montón de objetos de automóviles que se encienden en su propio estado interno. El sistema se puede describir de manera elocuente con un diagrama de estado UML, por lo que titulé mi publicación como tal. En retrospectiva, no es la mejor manera de describir el problema, editaré mi publicación.
PythonNewb
1
Todavía creo que deberías considerar publicar tu código en codereview.
Robert Harvey
1
Suena como una máquina de estado para mí. object.state = object.function(object.state);
Robert Bristow-Johnson
Todas las respuestas dadas hasta ahora, incluida la respuesta aceptada, pierden la razón principal por la cual las declaraciones de cambio se consideran malas. No permiten la adhesión al principio abierto / cerrado.
Dunk

Respuestas:

13
  • Convertí el automóvil en una especie de máquina de estados usando el patrón de estado . Aviso no switcho if-then-elsedeclaraciones se utilizan para la selección del estado.

  • En este caso, todos los estados son clases internas, pero podría implementarse de otra manera.

  • Cada estado contiene los estados válidos a los que puede cambiar.

  • Se le solicita al usuario el siguiente estado en caso de que sea posible más de uno, o simplemente para confirmar en caso de que solo uno sea posible.

  • Puede compilarlo y ejecutarlo para probarlo.

  • Utilicé un cuadro de diálogo gráfico porque era más fácil de esa manera ejecutarlo interactivamente en Eclipse.

ingrese la descripción de la imagen aquí

El diagrama UML se toma de aquí .

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
fuente
1
Realmente me gusta esto. Si bien aprecio la respuesta principal y es la defensa de las declaraciones de cambio (lo recordaré para siempre ahora), realmente me gusta la idea de este patrón. Gracias
PythonNewb
@PythonNewb ¿Lo ejecutaste?
Tulains Córdova
Si, funciona perfectamente. La implementación será ligeramente diferente para el código que tengo, pero la idea general es genial. Sin embargo, creo que podría considerar sacar las clases estatales de la clase que las encierra.
PythonNewb
1
@PythonNewb Cambié el código a una versión más corta reutilizando el estado / solicitud de cambio para la lógica de entrada usando una clase abstracta en lugar de una interfaz. Es 20 líneas más cortas pero lo probé y funciona igual. Siempre puede obtener la versión anterior y más larga mirando el historial de edición.
Tulains Córdova
1
@Caleth De hecho, lo escribí así porque generalmente lo hago en la vida real, es decir, almacenar piezas intercambiables en mapas y obtenerlas basadas en ID cargados de un archivo de parámetros. Por lo general, lo que almaceno en los mapas no son los objetos en sí, sino sus creadores si los objetos son caros o tienen mucho estado no estático.
Tulains Córdova
16

las declaraciones de cambio son malas

Es este tipo de simplificación excesiva lo que da un mal nombre a la programación orientada a objetos. Usar ifes tan "malo" como usar una instrucción switch. De cualquier manera, no estás despachando polimórficamente.

Si debe tener una regla que se ajuste a un sonido, intente esta:

Las declaraciones de cambio se vuelven muy malas en el momento en que tiene dos copias de ellas.

Una declaración de cambio que no está duplicada en ningún otro lugar de la base del código a veces puede no ser malvada. Si los casos no son públicos, pero están encapsulados, en realidad no es asunto de nadie más. Especialmente si sabes cómo y cuándo refactorizarlo en clases. Solo porque puedas no significa que tengas que hacerlo. Es porque puedes hacerlo que es menos crítico hacerlo ahora.

Si se encuentra tratando de meter más y más cosas en la declaración de cambio, difundir el conocimiento de los casos o desear que no sea tan malo hacer una copia, es hora de refactorizar los casos en clases separadas.

Si tiene tiempo para leer más de unas pocas frases sonoras sobre la refactorización de las declaraciones de cambio, c2 tiene una página muy equilibrada sobre el olor de la declaración de cambio .

Incluso en el código OOP, no todos los conmutadores son malos. Es cómo lo estás usando y por qué.

naranja confitada
fuente
2

El automóvil es un tipo de máquina de estados. Las declaraciones de cambio son la forma más sencilla de implementar una máquina de estados que carece de estados superiores y secundarios.

Frank Hileman
fuente
2

Las declaraciones de cambio no son malas. ¡No escuche a las personas que dicen cosas como "cambiar las declaraciones son malas"! Algunos usos particulares de las declaraciones de switch son un antipatrón, como usar switch para emular subclases. (¡Pero también puede implementar este antipatrón con if, así que supongo que los if también son malos!).

Su implementación suena bien. Estás en lo correcto es difícil de mantener si agregas muchos más estados. Pero esto no es solo un problema de implementación: tener un objeto con muchos estados con un comportamiento diferente es en sí mismo un problema. Las imágenes de su automóvil tienen 25 estados donde cada uno exhibe un comportamiento diferente y diferentes reglas para las transiciones de estado. Solo especificar y documentar este comportamiento sería una tarea enorme. ¡Tendrás miles de reglas de transición de estado! El tamaño del switchsería solo un síntoma de un problema mayor. Entonces, si es posible, evite seguir por este camino.

Un posible remedio es dividir el estado en subestados independientes. Por ejemplo, ¿REVERSE es realmente un estado distinto de DRIVE? Quizás los estados del automóvil podrían dividirse en dos: estado del motor (APAGADO, INACTIVO, CONDUCCIÓN) y dirección (ADELANTE, REVERSA). El estado y la dirección del motor probablemente serán en su mayoría independientes, por lo que reduce la duplicación lógica y las reglas de transición de estado. Más objetos con menos estados son mucho más fáciles de administrar que un solo objeto con numerosos estados.

JacquesB
fuente
1

En su ejemplo, los automóviles son simplemente máquinas de estado en el sentido clásico de la informática. Tienen un conjunto de estados pequeño y bien definido y algún tipo de lógica de transición de estado.

Mi primera sugerencia es considerar dividir la lógica de transición en su propia función (o clase, si su idioma no admite funciones de primera clase).

Mi segunda sugerencia es considerar romper la lógica de transición en el estado mismo, que tendría su propia función (o clase, si su lenguaje no admite funciones de primera clase).

En cualquier esquema, el proceso para el estado de transición se vería así:

mycar.transition()

o

mycar.state.transition()

El segundo podría, por supuesto, trivialmente ser envuelto en la clase de autos para parecerse al primero.

En ambos escenarios, agregar un nuevo estado (por ejemplo, DRAFTING) solo implicaría agregar un nuevo tipo de objeto de estado y cambiar los objetos que cambian específicamente al nuevo estado.

Joel Harmon
fuente
0

Depende de qué tan grande switchpueda ser.

En su ejemplo, creo que a switchestá bien, ya que no hay ningún otro estado en el que pueda pensar que Carpueda tener, por lo que no se agrandaría con el tiempo.

Si el único problema es tener un interruptor grande donde cada uno casetiene muchas instrucciones, simplemente haga métodos privados distintos para cada uno.

A veces, las personas sugieren el patrón de diseño del estado , pero es más apropiado cuando se trata de una lógica compleja y estados que toman decisiones comerciales diferentes para muchas operaciones distintas. De lo contrario, los problemas simples deberían tener soluciones simples.

En algunos escenarios, podría tener métodos que solo realicen tareas cuando el estado es A o B, pero no C o D, o tener métodos múltiples con operaciones muy simples que dependen del estado. Entonces una o varias switchdeclaraciones serían mejores.

Maxim Bernard
fuente
0

Esto suena como una máquina de estado de la vieja escuela del tipo que se usó antes de que nadie hiciera programación orientada a objetos, y mucho menos a Design Patterns. Se puede implementar en cualquier lenguaje que tenga instrucciones de cambio, como C.

Como han dicho otros, no hay nada intrínsecamente malo con las declaraciones de cambio. Las alternativas son a menudo más complicadas y más difíciles de entender.

A menos que el número de casos de cambio se vuelva ridículamente grande, la cosa puede seguir siendo bastante manejable. El primer paso para mantenerlo legible es reemplazar el código en cada caso con una llamada de función para implementar el comportamiento del estado.

Simon B
fuente