用OO语言实现对象状态?


11

我已经看过一些Java代码,它可以模拟赛车,其中包括基本状态机的实现。这不是经典的计算机科学状态机,而仅仅是一个可以具有多个状态并且可以基于一系列计算在其状态之间进行切换的对象。

为了描述问题,我有一个Car类,它带有一个嵌套的枚举类,该类为汽车的状态定义了一些常量(例如OFF,IDLE,DRIVE,REVERSE等)。在同一个Car类中,我有一个update函数,该函数基本上由一个大的switch语句组成,该语句打开汽车的当前状态,进行一些计算,然后更改汽车的状态。

据我所知,Cars状态仅在其自己的类中使用。

我的问题是,这是处理上述性质的状态机的最佳方法吗?听起来确实是最明显的解决方案,但过去我一直听说“ switch语句不好”。

我在这里看到的主要问题是,随着我们添加更多状态(如果认为必要),switch语句可能会变得非常大,并且代码可能变得笨拙且难以维护。

有什么更好的解决方案?


3
您的描述对我来说听起来不像是一台状态机。它听起来只不过是一堆汽车物体,每个物体都有自己的内部状态。考虑将您的实际有效代码发布到codereview.stackexchange.com;这些人非常擅长提供有关工作代码的反馈。
罗伯特·哈维,

也许“状态机”是一个不好的选择,但是,是的,基本上,我们有一堆汽车对象打开了它们自己的内部状态。该系统可以用UML状态图雄辩地描述,这就是为什么我这样称呼我的帖子。事后看来,这不是描述问题的最佳方法,我将编辑我的帖子。
PythonNewb '16

1
我仍然认为您应该考虑将代码发布到codereview。
罗伯特·哈维,

1
对我来说听起来像是一台状态机。 object.state = object.function(object.state);
罗伯特·布里斯托

到目前为止,所有给出的答案(包括已接受的答案)都缺少将switch语句视为错误的主要原因。他们不允许遵守开放/封闭原则。
Dunk

Answers:


13
  • 我使用State Pattern将Car变成了某种状态机。请注意,否switchif-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
我真的很喜欢 我很欣赏最重要的答案,它是对switch语句的辩护(我现在将永远记住这一点),但我真的很喜欢这种模式的想法。谢谢
PythonNewb '16

@PythonNewb您运行它了吗?
图兰斯·科尔多瓦

是的,它运作良好。我所拥有的代码的实现会略有不同,但是总体思路很好。我想我可以考虑将状态类从封闭类中移出。
PythonNewb'9

1
@PythonNewb我将代码更改为较短的版本,使用抽象类而非接口重新利用了更改状态/输入逻辑的提示。它短了20行,但我进行了测试并且工作相同。您总是可以查看编辑历史记录来获得较旧的较长版本。
图兰斯·科尔多瓦

1
@Caleth实际上,我之所以这样写是因为我通常在现实生活中这样做,即,将可互换的零件存储在地图中,并根据从参数文件加载的ID来获取它们。通常,我存储在地图中的不是对象本身,而是对象的创建者(如果对象价格昂贵或具有很多非静态状态)。
TulainsCórdova17年

16

开关语句不好

正是这种过度的简化使面向对象的编程取了一个坏名字。使用if和使用switch语句一样“糟糕”。无论哪种方式,您都不会进行多态调度。

如果您必须有一个适合声音的规则,请尝试以下规则:

当您拥有两个副本时,switch语句变得非常糟糕。

在代码库的其他任何地方都没有重复的switch语句有时可以设法避免邪恶。如果这些案件不是公开的,而是被封装的,那实际上是别人的事。特别是如果您知道如何以及何时将其重构为类。仅仅因为可以做并不意味着一定要做。这是因为您可以立即执行此操作,因此不太重要。

如果您发现自己试图将越来越多的内容添加到switch语句中,传播有关案例的知识,或者希望仅复制副本并不那么邪恶,那么就该将案例重构为单独的类了。

如果您有足够的时间阅读有关重构switch语句的一些声音,则c2会非常平衡地显示有关switch语句气味的页面。

即使使用OOP代码,也不是每个开关都不好。这就是您的使用方式,以及原因。


2

汽车是一种状态机。switch语句是实现缺少超级状态和子状态的状态机的最简单方法。


2

switch语句还不错。不要听别人说“开关语句不好”之类的话!switch语句的某些特定用途是一种反模式,例如使用switch来模拟子类。(但是您也可以使用if来实现此反模式,所以我想if也是不好的!)。

您的实现听起来不错。您是正确的,如果添加更多状态,将很难维护。但这不仅是实现的问题-具有一个状态很多且行为不同的对象本身就是一个问题。为您的汽车成像时,有25个状态分别显示不同的行为和不同的状态转换规则。仅指定和记录此行为将是一项艰巨的任务。您将拥有数千种状态转换规则!的大小switch只是更大问题的征兆。因此,如果可能,请避免走这条路。

一种可能的解决方法是将状态分成独立的子状态。例如,REVERSE是否真的不同于DRIVE?也许汽车状态可以分为两种:发动机状态(关闭,怠速,行驶)和方向(前进,倒车)。引擎状态和方向可能大部分是独立的,因此可以减少逻辑重复和状态转换规则。具有较少状态的更多对象比具有多种状态的单个对象更容易管理。


1

在您的示例中,汽车只是经典计算机科学意义上的状态机。它们具有一组状态明确的小型状态和某种状态转换逻辑。

我的第一个建议是考虑将转换逻辑分解为自己的函数(如果您的语言不支持一流的函数,则将其划分为类)。

我的第二个建议是考虑将过渡逻辑分解为状态本身,该状态将具有自己的功能(或类,如果您的语言不支持一流的功能)。

在这两种方案中,转换状态的过程都将如下所示:

mycar.transition()

要么

mycar.state.transition()

当然,可以将第二个包裹在汽车类中,使其看起来像第一个。

在这两种情况下,添加新状态(例如DRAFTING)仅涉及添加新类型的状态对象并更改专门切换到新状态的对象。


0

这取决于switch可能有多大。

在您的示例中,我认为a switch可以,因为实际上我没有想到您Car可以拥有的其他任何状态,因此它不会随着时间的推移而变大。

如果唯一的问题是开关很大,每个开关case都有很多指令,那么只需为每个开关创建不同的私有方法。

有时人们会建议使用状态设计模式,但是当您处理复杂的逻辑并为许多不同的操作制定不同的业务决策时,状态设计模式更为合适。否则,简单的问题应该有简单的解决方案。

在某些情况下,您可能具有仅在状态为A或B而不是C或D时才执行任务的方法,或者具有根据状态依赖于非常简单的操作的多种方法。那么一个或几个switch语句会更好。


0

这听起来像是一种老式的状态机,在任何人进行面向对象的编程之前就使用过,更不用说设计模式了。可以用任何带有switch语句的语言来实现,例如C。

正如其他人所说的那样,switch语句本质上没有错。替代方案通常更复杂且更难以理解。

除非开关箱的数量变得非常荒谬,否则事情将保持相当可管理的状态。保持可读性的第一步是在每种情况下都用一个函数调用替换代码以实现状态的行为。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.