私はいくつかのJava参照するコードを与えられました。これは、カーレースをシミュレートします。これには、基本的な状態機械の実装が含まれます。これは、古典的なコンピュータサイエンスの状態機械ではなく、単に複数の状態を持つことができ、一連の計算に基づいてその状態を切り替えることができるオブジェクト。
問題だけを説明するために、Carの状態の定数(OFF、IDLE、DRIVE、REVERSEなど)を定義する列挙型クラスがネストされたCarクラスがあります。この同じCarクラス内にupdate関数があり、これは基本的に、車の現在の状態を切り替え、いくつかの計算を行ってから車の状態を変更する大きなswitchステートメントで構成されています。
私が見る限り、Cars状態はそれ自体のクラス内でのみ使用されます。
私の質問は、これが上記の性質の状態機械の実装を処理する最良の方法ですか?それは最も明白な解決策のように聞こえますが、以前は「switch文は悪い」といつも聞いていました。
ここで確認できる主な問題は、状態が追加されると(必要と思われる場合)、switchステートメントが非常に大きくなり、コードが扱いにくくなり、保守が困難になる可能性があることです。
この問題のより良い解決策は何でしょうか?
私は State Pattern を使用して、車をある種の状態機械に変えました。状態の選択にswitch
またはif-then-else
ステートメントが使用されていないことに注意してください。
この場合、すべての状態は内部クラスですが、他の方法で実装することもできます。
各状態には、変更可能な有効な状態が含まれています。
複数の可能性がある場合はユーザーは次の状態の入力を求められ、1つしか可能でない場合は単に確認するよう求められます。
コンパイルして実行すると、テストできます。
Eclipseでインタラクティブに実行する方が簡単だったので、グラフィックダイアログボックスを使用しました。
UMLダイアグラムは here から取得されます。
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();
}
}
switchステートメントが悪い
オブジェクト指向プログラミングに悪い名前を与えるのは、この種の単純化です。 if
を使用することは、switchステートメントを使用するのと同じくらい「悪い」ことです。どちらの方法でも、多態的にディスパッチするわけではありません。
あなたが健全なかみ傷に収まるルールを持っている必要があるなら、これを試してください:
Switchステートメントは、2つのコピーがあると非常に悪くなります。
コードベースのどこにも複製されていないswitchステートメントは、時として悪にならないようにできます。ケースが公開されていないが、カプセル化されている場合、それは実際には他人のビジネスではありません。特に、クラスにリファクタリングする方法とタイミングを知っている場合。できるからといって、そうしなければならないという意味ではありません。それはあなたが今それをすることはそれほど重要ではないことができるからです。
Switchステートメントにますます多くのものを入れようとしている、ケースの知識を広めている、またはそれをコピーするだけでそれほど悪ではないことを望む場合は、ケースを個別のクラスにリファクタリングするときがきました。
Switchステートメントのリファクタリングについていくつかの音を読む時間がある場合、c2には switchステートメントの匂い について非常にバランスのとれたページがあります。
OOPコードでも、すべてのスイッチが不良であるとは限りません。それが使用方法とその理由です。
車は状態機械の一種です。 switchステートメントは、スーパーステートとサブステートがないステートマシンを実装する最も簡単な方法です。
Switchステートメントは悪くありません。 「ステートメントの切り替えは悪い」などと言う人の言うことを聞かないでください。 switchステートメントの特定の用途には、switchを使用してサブクラス化をエミュレートするようなアンチパターンがあります。 (ただし、ifを使用してこのアンチパターンを実装することもできるので、ifも悪いと思います!).
実装は正常に聞こえます。正しいです。状態をさらに追加すると、維持が難しくなります。しかし、これは実装の問題だけではありません。異なる動作を持つ多くの状態を持つオブジェクトを持つこと自体が問題です。あなたの車の画像には25の州があり、それぞれ異なる動作と状態遷移の異なるルールを示しています。この動作を指定して文書化するだけでも、非常に大きな作業になります。 数千の状態遷移規則があります! switch
のサイズは、より大きな問題の兆候にすぎません。したがって、可能であれば、この道を下るのは避けてください。
可能な解決策は、州を独立した下位州に分割することです。たとえば、REVERSEは実際にはDRIVEとは異なる状態ですか?おそらく、車の状態は、エンジン状態(OFF、IDLE、DRIVE)と方向(FORWARD、REVERSE)の2つに分割される可能性があります。エンジンの状態と方向はおそらくほとんど独立しているため、ロジックの重複と状態遷移のルールを減らすことができます。状態が少ないオブジェクトが多いほど、状態が多い単一のオブジェクトよりも管理がはるかに簡単になります。
あなたの例では、自動車は古典的なコンピュータサイエンスの意味での単なるステートマシンです。それらには、小さく明確に定義された状態のセットと、ある種の状態遷移ロジックがあります。
最初の提案は、遷移ロジックを独自の関数(または、言語がファーストクラスの関数をサポートしていない場合はクラス)に分割することを検討することです。
2番目の提案は、遷移ロジックを状態自体に分解することを検討することです。これは、独自の関数(または、言語がファーストクラスの関数をサポートしていない場合はクラス)を持ちます。
どちらのスキームでも、状態を移行するプロセスは次のようになります。
mycar.transition()
または
mycar.state.transition()
もちろん、2番目のクラスは、1番目のクラスのように見えるように、車のクラスに簡単にラップできます。
どちらのシナリオでも、新しい状態(たとえば、DRAFTING)を追加するには、新しいタイプの状態オブジェクトを追加し、特に新しい状態に切り替わるオブジェクトを変更するだけです。
switch
の大きさによって異なります。
あなたの例ではswitch
は大丈夫だと思います。あなたのCar
が持っていると私が考えることができる他の状態は実際にはないので、時間の経過とともに大きくなることはありません。
唯一の問題が、各case
に多数の命令がある大きなスイッチがある場合は、それぞれに個別のプライベートメソッドを作成します。
状態設計パターンが推奨される場合もありますが、複雑なロジックを扱う場合や、状態が多くの異なる操作に対して異なるビジネス上の意思決定を行う場合に適しています。そうでなければ、単純な問題には単純な解決策があるはずです。
いくつかのシナリオでは、状態がAまたはBであるがCまたはDではない場合にのみタスクを実行するメソッド、または状態に依存する非常に単純な操作を持つ複数のメソッドがある場合があります。その場合、1つまたは複数のswitch
ステートメントの方が適しています。
これは、デザインパターンはもちろんのこと、誰もがオブジェクト指向プログラミングを行う前に使用されていたような旧式のステートマシンのように聞こえます。 Cなど、switchステートメントを持つ任意の言語で実装できます。
他の人が言ったように、switchステートメントには本質的に問題はありません。多くの場合、代替案はより複雑で理解が困難です。
スイッチケースの数が途方もなく大きくならない限り、事柄はかなり扱いやすいままでいることができます。可読性を維持するための最初のステップは、いずれの場合もコードを関数呼び出しに置き換えて、状態の動作を実装することです。