オブジェクト指向のオブジェクト指向言語での実装?


11

カーレースをシミュレートするJavaコードをいくつか見てきました。これには基本的なステートマシンの実装が含まれています。これは、古典的なコンピューターサイエンスステートマシンではなく、複数の状態を持つことができ、一連の計算に基づいて状態を切り替えることができるオブジェクトにすぎません。

問題だけを説明するために、Carの状態の定数(OFF、IDLE、DRIVE、REVERSEなど)を定義するネストされたenumクラスを持つCarクラスを取得しました。この同じCarクラス内には、更新機能があります。これは基本的に、現在の車の状態を切り替え、計算を行い、車の状態を変更する大きなswitchステートメントで構成されています。

私が見る限り、Cars状態は独自のクラス内でのみ使用されます。

私の質問は、これが上記の性質のステートマシンの実装を処理する最良の方法ですか?それは最も明白な解決策のように聞こえますが、過去には「switchステートメントが悪い」といつも聞いていました。

ここで見られる主な問題は、状態を追加すると(必要に応じて)switchステートメントが非常に大きくなり、コードが扱いにくくなり、保守が難しくなる可能性があることです。

この問題のより良い解決策は何ですか?


3
私の説明はステートマシンのようには聞こえません。それは、それぞれが独自の内部状態を持つ自動車オブジェクトの束のように聞こえます。実際の作業コードをcodereview.stackexchange.comに投稿することを検討してください。これらの人々は、作業中のコードに関するフィードバックを非常に得意としています。
ロバートハーベイ

おそらく「ステートマシン」は不適切な言葉の選択ですが、はい、基本的に、独自の内部状態をオンにする多数の自動車オブジェクトがあります。システムは、UML状態図で雄弁に説明できます。そのため、私の投稿にそのようなタイトルを付けました。後知恵では、それは問題を説明する最良の方法ではありません。投稿を編集します。
PythonNewb

1
まだコードレビューにコードを投稿することを検討すべきだと思います。
ロバートハーベイ

1
私にはステートマシンのように聞こえます。 object.state = object.function(object.state);
ロバートブリストージョンソン

受け入れられた回答を含む、これまでに与えられたすべての回答は、switchステートメントが不良と見なされる主な理由を逃しています。彼らは、オープン/クローズド原則の順守を許可しません。
ダンク

回答:


13
  • State Patternを使用して、Carをある種のステートマシンに変えました。ステートの選択にはno switchまたはif-then-elseステートメントが使用されていることに注意してください。

  • この場合、すべての状態は内部クラスですが、それ以外の場合は実装できます。

  • 各状態には、変更可能な有効な状態が含まれています。

  • ユーザーは、複数の状態が可能な場合は次の状態を要求されます。1つだけが可能な場合は単に確認するように求められます。

  • コンパイルして実行し、テストすることができます。

  • 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

@PythonNewb実行しましたか?
Tulainsコルドバ

はい、完全に機能します。実装は私が持っているコードとは少し異なりますが、一般的な考え方は素晴らしいです。ただし、状態クラスを囲んでいるクラスから移動することを検討するかもしれません。
PythonNewb

1
@PythonNewbコードを短いバージョンに変更し、インターフェイスの代わりに抽象クラスを使用して、入力ロジックの変更状態/プロンプトを再利用しました。20行短くなっていますが、テストと動作は同じです。編集履歴を見ると、いつでも古いバージョンを入手できます。
Tulainsコルドバ

1
@Caleth実際のところ、私はこのように書きました。なぜなら、私は通常、実際にそれを行うからです。つまり、交換可能なピースをマップに保存し、パラメータファイルからロードされたIDに基づいて取得します。通常、マップに保存するのはオブジェクト自体ではなく、オブジェクトが高価であるか、静的でない状態が多い場合は作成者です。
Tulainsコルドバ

16

switchステートメントが悪い

オブジェクト指向プログラミングに悪い名前を与えるのは、このような単純化の過剰です。を使用してifは、switchステートメントと同じくらい「悪い」です。どちらにしても、多態的にディスパッチしていません。

サウンドバイトに適合するルールが必要な場合は、このルールを試してください。

Switchステートメントは、コピーが2つあると非常に悪くなります。

コードベースの他の場所で複製されていないswitchステートメントは、悪ではないことがあります。ケースが公開されていないが、カプセル化されている場合、それは本当に他の誰のビジネスでもありません。特に、クラスにリファクタリングする方法とタイミングを知っている場合。できるからといって、そうする必要があるわけではありません。できるのは、今やることがそれほど重要ではないからです。

switchステートメントにもっと多くのものを押し込もうとしている、ケースの知識を広めようとしている、または単にコピーを作成するのがそれほど悪くないようにしたい場合は、ケースを個別のクラスにリファクタリングします。

switchステートメントのリファクタリングに関するいくつかのサウンドバイトを読む時間がある場合、c2にはswitchステートメントの匂いについて非常にバランスの取れたページがあります。

OOPコードでも、すべてのスイッチが悪いわけではありません。それはあなたがそれをどのように使用しているか、そしてなぜです。


2

車はステートマシンの一種です。Switchステートメントは、スーパーステートとサブステートがないステートマシンを実装する最も簡単な方法です。


2

Switchステートメントは悪くありません。「スイッチの状態が悪い」などのことを言う人の話を聞かないでください!switchステートメントの特定の用途には、スイッチを使用してサブクラス化をエミュレートするようなアンチパターンがあります。(しかし、このアンチパターンをifで実装することもできるので、ifも悪いと思います!)。

実装は問題ありません。ステートをさらに追加すると、メンテナンスが難しくなります。しかし、これは単なる実装の問題ではありません。さまざまな動作を持つ多くの状態を持つオブジェクトを持つこと自体が問題です。車に25の状態があることをイメージすると、それぞれが異なる動作と状態遷移の異なるルールを示します。この動作を指定して文書化するだけでも、膨大な作業になります。あなたは持っているだろう何千もの状態遷移ルールのを!のサイズは、switchより大きな問題の症状にすぎません。したがって、可能であれば、この道を下るのは避けてください。

可能な解決策は、状態を独立したサブ状態に分割することです。たとえば、REVERSEは本当にDRIVEとは異なる状態ですか?おそらく車の状態は、エンジン状態(OFF、IDLE、DRIVE)と方向(FORWARD、REVERSE)の2つに分けられます。エンジンの状態と方向はおそらくほとんど独立しているため、ロジックの重複と状態遷移の規則を減らします。状態が少ないオブジェクトが多いほど、状態が多数ある単一のオブジェクトよりも管理がはるかに簡単です。


1

あなたの例では、車は単に古典的なコンピューターサイエンスの意味でのステートマシンです。それらには、小さく明確に定義された状態のセットと、何らかの状態遷移ロジックがあります。

私の最初の提案は、遷移ロジックを独自の関数(または、言語がファーストクラス関数をサポートしていない場合はクラス)に分解することを検討することです。

2番目の提案は、遷移ロジックをステート自体に分解することを検討することです。ステート自体は、独自の機能(または、言語がファーストクラスの機能をサポートしていない場合はクラス)を持ちます。

どちらのスキームでも、状態を移行するプロセスは次のようになります。

mycar.transition()

または

mycar.state.transition()

もちろん、2番目のクラスを車のクラスにラップして、最初のクラスのように見せることもできます。

両方のシナリオで、新しい状態(たとえば、ドラフト)の追加には、新しいタイプの状態オブジェクトの追加と、特に新しい状態に切り替わるオブジェクトの変更のみが含まれます。


0

それはどれくらいの大きさに依存しswitchます。

あなたの例では、私が考えるswitchことができる他の状態は実際にはないので、a は大丈夫だと思いますCar

唯一の問題が、それぞれcaseに多数の命令がある大きなスイッチを持っている場合、それぞれに対して個別のプライベートメソッドを作成するだけです。

状態設計パターンを提案する人もいますが、複雑なロジックを扱う場合や、多くの異なる操作に対して異なるビジネス上の意思決定を行う場合に適しています。そうでなければ、単純な問題には単純な解決策が必要です。

一部のシナリオでは、状態がAまたはBでタスクを実行するがCまたはDでない場合にのみメソッドを実行したり、状態に依存する非常に単純な操作を行う複数のメソッドを使用したりできます。次に、1つまたは複数のswitchステートメントの方が良いでしょう。


0

これは、デザインパターンはもちろんのこと、誰もがオブジェクト指向プログラミングを行う前に使用されていた、古い学校のステートマシンのように聞こえます。Cなどのswitchステートメントを持つ任意の言語で実装できます。

他の人が言ったように、switchステートメントには本質的に問題はありません。多くの場合、選択肢はより複雑で理解しにくいものです。

スイッチケースの数が途方もなく大きくならない限り、物事は非常に管理しやすいままです。読みやすくするための最初のステップは、それぞれの場合にコードを関数呼び出しで置き換えて、状態の動作を実装することです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.