Implementação do estado do objeto em uma linguagem OO?


11

Recebi um código Java para analisar, que simula uma corrida de carros, da qual inclui uma implementação de uma máquina de estado básica. Esta não é uma máquina clássica de estado da ciência da computação, mas apenas um objeto que pode ter vários estados e pode alternar entre seus estados com base em uma série de cálculos.

Para descrever apenas o problema, eu tenho uma classe Car, com uma classe enum aninhada que define algumas constantes para o estado do carro (como OFF, IDLE, DRIVE, REVERSE, etc.). Dentro dessa mesma classe Car, eu tenho uma função de atualização, que consiste basicamente em uma declaração de chave grande que liga o estado atual dos carros, faz alguns cálculos e depois altera o estado dos carros.

Tanto quanto eu posso ver, o estado Cars é usado apenas dentro de sua própria classe.

Minha pergunta é: essa é a melhor maneira de lidar com a implementação de uma máquina de estado da natureza descrita acima? Parece a solução mais óbvia, mas no passado eu sempre ouvi dizer que "as declarações de switch são ruins".

O principal problema que vejo aqui é que a instrução switch pode se tornar muito grande à medida que adicionamos mais estados (se necessário) e o código pode se tornar pesado e difícil de manter.

Qual seria a melhor solução para esse problema?


3
Sua descrição não me parece uma máquina de estado; apenas soa como um monte de objetos de carros, cada um com seu próprio estado interno. Considere postar seu código de trabalho real em codereview.stackexchange.com ; essas pessoas são muito boas em fornecer feedback sobre o código de trabalho.
Robert Harvey

Talvez "máquina de estado" seja uma má escolha de palavras, mas sim, basicamente temos um monte de objetos de carro que ativam seu próprio estado interno. O sistema pode ser descrito de forma eloquente com um diagrama de estados UML, e foi por isso que intitulei minha postagem como tal. Em retrospectiva, não é a melhor maneira de descrever o problema, editarei minha postagem.
PythonNewb

1
Ainda acho que você deve postar seu código na visualização por codificador.
Robert Harvey

1
parece uma máquina de estado para mim. object.state = object.function(object.state);
Robert Bristow-Johnson

Todas as respostas dadas até agora, incluindo a resposta aceita, perdem o principal motivo pelo qual as declarações de troca são consideradas ruins. Eles não permitem a adesão ao princípio aberto / fechado.
Dunk

Respostas:


13
  • Transformei o carro em uma espécie de máquina de estado usando padrão de estado . Observe que não são utilizadas declarações switchou if-then-elsepara seleção de estado.

  • Nesse caso, todos os estados são classes internas, mas poderia ser implementado de outra forma.

  • Cada estado contém os estados válidos para os quais pode mudar.

  • O usuário é solicitado para o próximo estado, caso seja possível mais de um, ou simplesmente para confirmar se apenas um é possível.

  • Você pode compilá-lo e executá-lo para testá-lo.

  • Usei uma caixa de diálogo gráfica porque era mais fácil executá-la interativamente no Eclipse.

insira a descrição da imagem aqui

O diagrama UML é retirado daqui .

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
Eu realmente gosto disso. Embora eu aprecie a resposta principal e seja a defesa das declarações de troca (eu sempre lembrarei disso agora), eu realmente gosto muito da ideia desse padrão. Obrigado
PythonNewb 1/16

@PythonNewb Você executou?
Tulains Córdova 01/09/16

Sim, funciona perfeitamente. A implementação será um pouco diferente para o código que tenho, mas a ideia geral é ótima. Acho que posso considerar mover as classes de estado para fora da classe anexa.
precisa saber é o seguinte

1
@PythonNewb Alterei o código para uma versão mais curta, reutilizando o estado / prompt de alteração da lógica de entrada usando uma classe abstrata em vez de uma interface. São 20 linhas mais curtas, mas eu testei e funciona da mesma maneira. Você sempre pode obter a versão mais antiga e mais longa olhando para o histórico de edições.
Tulains Córdova 01/09/16

1
@Caleth Por uma questão de fato, eu escrevi assim, porque normalmente faço isso na vida real, ou seja, armazeno peças intercambiáveis ​​em mapas e as obtemos com base nos IDs carregados de um arquivo de parâmetro. Normalmente, o que guardo nos mapas não são os objetos em si, mas seus criadores, se os objetos forem caros ou tiverem um estado não estático.
Tulains Córdova

16

as instruções do switch são ruins

É esse tipo de simplificação excessiva que atribui um mau nome à programação orientada a objetos. Usar ifé tão "ruim" quanto usar uma instrução switch. De qualquer maneira, você não está despachando polimorficamente.

Se você deve ter uma regra que se encaixa em uma mordida sonora, tente esta:

As instruções de troca ficam muito ruins no momento em que você tem duas cópias delas.

Uma instrução switch que não é duplicada em nenhum outro lugar na base de código pode às vezes conseguir não ser má. Se os casos não são públicos, mas estão encapsulados, não é da conta de mais ninguém. Especialmente se você souber como e quando refatorá-lo em classes. Só porque você pode, não significa que você precisa. É porque você pode que é menos crítico fazê-lo agora.

Se você está tentando colocar cada vez mais coisas na instrução switch, espalhando o conhecimento dos casos, ou desejando que não seja tão ruim apenas fazer uma cópia, é hora de refatorar os casos em classes separadas.

Se você tiver tempo para ler mais do que algumas frases de efeito sobre refatoração das declarações de switch, o c2 possui uma página muito bem equilibrada sobre o cheiro da declaração de switch .

Mesmo no código OOP, nem todo switch é ruim. É como você está usando e por quê.


2

O carro é um tipo de máquina de estado. As instruções de comutação são a maneira mais simples de implementar uma máquina de estados sem super estados e sub estados.


2

As instruções de troca não são ruins. Não dê ouvidos a pessoas que dizem coisas como "mudar de estado é ruim"! Alguns usos específicos de instruções switch são um antipadrão, como usar switch para emular subclasses. (Mas você também pode implementar esse antipadrão com if's, então acho que os if também são ruins!).

Sua implementação parece bem. Você está certo é que será difícil manter se você adicionar muitos outros estados. Mas isso não é apenas uma questão de implementação - ter um objeto com muitos estados com comportamento diferente é um problema. A imagem do seu carro possui 25 estados, cada um exibindo comportamento diferente e regras diferentes para transições de estado. Apenas especificar e documentar esse comportamento seria uma tarefa enorme. Você terá milhares de regras de transição de estado! O tamanho do switchseria apenas um sintoma de um problema maior. Portanto, se possível, evite seguir esse caminho.

Um possível remédio é dividir o estado em subestados independentes. Por exemplo, REVERSE é realmente um estado distinto de DRIVE? Talvez os estados do carro possam ser divididos em dois: estado do motor (DESLIGADO, INATIVO, CONDUÇÃO) e direção (FORWARD, REVERSE). O estado e a direção do mecanismo provavelmente serão independentes, portanto você reduz a duplicação lógica e as regras de transição de estado. Mais objetos com menos estados são muito mais fáceis de gerenciar do que um único objeto com vários estados.


1

No seu exemplo, carros são simplesmente máquinas de estado no sentido clássico da ciência da computação. Eles têm um conjunto pequeno e bem definido de estados e algum tipo de lógica de transição de estado.

Minha primeira sugestão é considerar a quebra da lógica de transição em sua própria função (ou classe, se o seu idioma não suportar funções de primeira classe).

Minha segunda sugestão é considerar a quebra da lógica de transição para o próprio estado, que teria sua própria função (ou classe, se sua linguagem não suportar funções de primeira classe).

Em qualquer um dos esquemas, o processo de transição de estado seria algo como isto:

mycar.transition()

ou

mycar.state.transition()

O segundo poderia, é claro, ser trivialmente envolvido na classe de carros para parecer o primeiro.

Nos dois cenários, adicionar um novo estado (por exemplo, DRAFTING) envolveria apenas a adição de um novo tipo de objeto de estado e a alteração dos objetos que mudam especificamente para o novo estado.


0

Depende de quão grande switchpossa ser.

No seu exemplo, acho que switchestá tudo bem, já que não há realmente nenhum outro estado que eu possa imaginar, o que Carpoderia ter, para que não aumentasse com o tempo.

Se o único problema é ter um comutador grande, onde cada um casetem muitas instruções, basta criar métodos particulares distintos para cada um.

Às vezes, as pessoas sugerem o padrão de design do estado , mas é mais apropriado quando você lida com uma lógica complexa e com estados que tomam decisões de negócios diferentes para muitas operações distintas. Caso contrário, problemas simples devem ter soluções simples.

Em alguns cenários, você pode ter métodos que executam apenas tarefas quando o estado é A ou B, mas não C ou D, ou ter vários métodos com operações muito simples que dependem do estado. Então, uma ou várias switchdeclarações seriam melhores.


0

Isso soa como uma máquina de estado da velha escola do tipo que era usada antes que alguém fizesse programação orientada a objetos, muito menos Design Patterns. Pode ser implementado em qualquer idioma que possua instruções de opção, como C.

Como outros já disseram, não há nada de inerentemente errado nas instruções do switch. As alternativas são geralmente mais complicadas e difíceis de entender.

A menos que o número de casos de switch se torne ridiculamente grande, a coisa pode permanecer bastante gerenciável. O primeiro passo para mantê-lo legível é substituir o código em cada caso por uma chamada de função para implementar o comportamento do estado.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.