コードでスイッチの使用を排除する方法は何ですか?
コードでスイッチの使用を排除する方法は何ですか?
回答:
スイッチステートメント自体はアンチパターンではありませんが、オブジェクト指向をコーディングしている場合は、スイッチステートメントを使用する代わりに、スイッチの使用がポリモーフィズムで解決されるかどうかを検討する必要があります。
ポリモーフィズムでは、これは:
foreach (var animal in zoo) {
switch (typeof(animal)) {
case "dog":
echo animal.bark();
break;
case "cat":
echo animal.meow();
break;
}
}
これになる:
foreach (var animal in zoo) {
echo animal.speak();
}
typeof
がないので、この回答は、他の状況でswitchステートメントを回避する方法や理由を示唆していません。
Switchステートメントのにおいをご覧ください。
通常、同様のswitchステートメントがプログラム全体に散在しています。1つのスイッチで句を追加または削除すると、多くの場合、他の句も見つけて修復する必要があります。
パターンへのリファクタリングとリファクタリングの両方これを解決するためのアプローチがあります。
(疑似)コードが次のようになっている場合:
class RequestHandler {
public void handleRequest(int action) {
switch(action) {
case LOGIN:
doLogin();
break;
case LOGOUT:
doLogout();
break;
case QUERY:
doQuery();
break;
}
}
}
このコードはOpen Closed Principleに違反しており、新しいタイプのアクションコードすべてに脆弱です。これを修正するには、「コマンド」オブジェクトを導入します。
interface Command {
public void execute();
}
class LoginCommand implements Command {
public void execute() {
// do what doLogin() used to do
}
}
class RequestHandler {
private Map<Integer, Command> commandMap; // injected in, or obtained from a factory
public void handleRequest(int action) {
Command command = commandMap.get(action);
command.execute();
}
}
(疑似)コードが次のようになっている場合:
class House {
private int state;
public void enter() {
switch (state) {
case INSIDE:
throw new Exception("Cannot enter. Already inside");
case OUTSIDE:
state = INSIDE;
...
break;
}
}
public void exit() {
switch (state) {
case INSIDE:
state = OUTSIDE;
...
break;
case OUTSIDE:
throw new Exception("Cannot leave. Already outside");
}
}
次に、「状態」オブジェクトを導入できます。
// Throw exceptions unless the behavior is overriden by subclasses
abstract class HouseState {
public HouseState enter() {
throw new Exception("Cannot enter");
}
public HouseState leave() {
throw new Exception("Cannot leave");
}
}
class Inside extends HouseState {
public HouseState leave() {
return new Outside();
}
}
class Outside extends HouseState {
public HouseState enter() {
return new Inside();
}
}
class House {
private HouseState state;
public void enter() {
this.state = this.state.enter();
}
public void leave() {
this.state = this.state.leave();
}
}
お役に立てれば。
Map<Integer, Command>
スイッチは必要ありませんか?
switchは、switchステートメントで実装されているかどうかに関係なく、チェーン、ルックアップテーブル、oopポリモーフィズム、パターンマッチングなどのパターンです。
「switchステートメント」または「switchパターン」の使用を削除しますか?1つ目は削除でき、2つ目は別のパターン/アルゴリズムを使用できる場合にのみ削除できます。ほとんどの場合、それは不可能であるか、そうするのが適切な方法ではありません。
コードからswitchステートメントを削除したい場合、最初に尋ねる質問は、switchステートメントを削除して他の手法を使用することが理にかなっている場所です。残念ながら、この質問への答えはドメイン固有です。
また、コンパイラはステートメントを切り替えるためにさまざまな最適化を実行できることを覚えておいてください。したがって、たとえばメッセージ処理を効率的に実行したい場合は、switchステートメントがほとんどの方法です。ただし、その一方で、switchステートメントに基づいてビジネスルールを実行することはおそらく最善の方法ではなく、アプリケーションを再設計する必要があります。
switchステートメントのいくつかの代替方法を次に示します。
スイッチ自体はそれほど悪いことではありませんが、メソッド内のオブジェクトに多くの「スイッチ」または「if / else」がある場合、デザインが少し「手続き型」であり、オブジェクトが単なる値であることを示している可能性がありますバケツ。ロジックをオブジェクトに移動し、オブジェクトのメソッドを呼び出し、代わりに応答方法を決定させます。
最良の方法は良い地図を使うことだと思います。辞書を使用すると、ほとんどすべての入力を他の値/オブジェクト/関数にマッピングできます。
あなたのコードは次のようになります(疑似):
void InitMap(){
Map[key1] = Object/Action;
Map[key2] = Object/Action;
}
Object/Action DoStuff(Object key){
return Map[key];
}
switch
ステートメントに新しい状態または新しい動作を追加する場合は、ステートメントを置き換えることをお勧めします。
int状態。 String getString(){ スイッチ(状態){ ケース0://状態0の動作 「ゼロ」を返します。 ケース1://状態1の動作 「1」を返す; } 新しいIllegalStateException();をスローします。 } double getDouble(){ スイッチ(this.state){ ケース0://状態0の動作 0dを返します。 ケース1://状態1の動作 1dを返す; } 新しいIllegalStateException();をスローします。 }
新しい動作を追加するにはswitch
、をコピーする必要があります。新しい状態を追加することはcase
、すべての switch
ステートメントに別の状態を追加することを意味します。
Javaでは、実行時に値がわかっている非常に限られた数のプリミティブ型のみを切り替えることができます。これはそれ自体に問題があります。状態はマジックナンバーまたは文字として表現されています。
パターンマッチング、および複数のif - else
ブロックを使用できますが、新しい動作や新しい状態を追加するときに実際に同じ問題が発生します。
他の人が「ポリモーフィズム」として提案したソリューションは、Stateパターンのインスタンスです。
各状態を独自のクラスに置き換えます。各動作には、クラスに独自のメソッドがあります。
IStateの状態。 String getString(){ return state.getString(); } double getDouble(){ return state.getDouble(); }
新しい状態を追加するたびに、IState
インターフェースの新しい実装を追加する必要があります。ではswitch
世界、あなたは追加することと思いますcase
それぞれにしますswitch
。
新しい動作を追加するたびに、新しいメソッドをIState
インターフェースと各実装に追加する必要があります。これは以前と同じ負担ですが、コンパイラは、既存の各状態で新しい動作の実装があることを確認します。
他の人はすでにこれは重すぎるかもしれないと言っているので、もちろん、あなたが別の場所に移動するところに到達するポイントがあります。個人的には、2回目にスイッチを書くときがリファクタリングのポイントです。
「スイッチ」は単なる言語構成要素であり、すべての言語構成要素は仕事を成し遂げるためのツールと考えることができます。実際のツールと同様に、一部のツールは別のタスクよりも1つのタスクにより適しています(スレッジハンマーを使用して画像フックを付けることはありません)。重要な部分は、「仕事を終わらせる」ことをどのように定義するかです。それは、保守可能である必要があるか、高速である必要があるか、拡張する必要があるか、拡張可能である必要があるかなどです。
プログラミングプロセスの各ポイントには、通常、使用できるさまざまな構造とパターンがあります。スイッチ、if-else-ifシーケンス、仮想関数、ジャンプテーブル、関数ポインターを含むマップなどです。経験があれば、プログラマーは、与えられた状況で使用する適切なツールを本能的に知っています。
コードを保守またはレビューする人は、構成を安全に使用できるように、少なくとも元の作者と同じくらいのスキルがあると想定する必要があります。
C ++の場合
つまり、AbstractFactoryを参照している場合は、通常、registerCreatorFunc(..)メソッドの方が、必要なすべての「新しい」ステートメントごとにケースを追加する必要があるよりも優れていると思います。次に、すべてのクラスにcreateFunction(..)を作成および登録させます。これは、マクロを使用して簡単に実装できます(言及する場合)。これは多くのフレームワークが行う一般的なアプローチだと思います。私は最初にそれをET ++で見ましたが、DECLおよびIMPLマクロを必要とする多くのフレームワークがそれを使用していると思います。
Cのような手続き型言語では、switchは他のどの方法よりも優れています。
オブジェクト指向言語では、ほとんどの場合、オブジェクト構造、特にポリモーフィズムをより有効に利用できる他の代替手段があります。
switchステートメントの問題は、いくつかの非常に類似したswitchブロックがアプリケーションの複数の場所で発生し、新しい値のサポートを追加する必要がある場合に発生します。開発者がアプリケーションに散在するスイッチブロックの1つに新しい値のサポートを追加するのを忘れることはよくあることです。
ポリモーフィズムを使用すると、新しいクラスが新しい値を置き換え、新しいクラスの追加の一部として新しい動作が追加されます。これらのスイッチポイントでの動作は、スーパークラスから継承されるか、オーバーライドされて新しい動作を提供するか、スーパーメソッドが抽象の場合にコンパイラエラーを回避するために実装されます。
明確なポリモーフィズムが進行していない場合、戦略パターンを実装する価値があります。
しかし、代替案が大きなIF ... THEN ... ELSEブロックの場合は、忘れてください。
関数ポインタは、巨大な分厚いswitchステートメントを置き換える1つの方法です。それらは、名前で関数をキャプチャし、関数を作成できる言語で特に優れています。
もちろん、switchステートメントを強制的にコードから削除するべきではありません。また、すべてを間違って実行している可能性が常にあります。(これは避けられない場合もありますが、適切な言語を使用すると、クリーンな状態で冗長性を削除できます。)
これは素晴らしい分割統治の例です:
なんらかの通訳がいるとします。
switch(*IP) {
case OPCODE_ADD:
...
break;
case OPCODE_NOT_ZERO:
...
break;
case OPCODE_JUMP:
...
break;
default:
fixme(*IP);
}
代わりに、これを使用できます。
opcode_table[*IP](*IP, vm);
... // in somewhere else:
void opcode_add(byte_opcode op, Vm* vm) { ... };
void opcode_not_zero(byte_opcode op, Vm* vm) { ... };
void opcode_jump(byte_opcode op, Vm* vm) { ... };
void opcode_default(byte_opcode op, Vm* vm) { /* fixme */ };
OpcodeFuncPtr opcode_table[256] = {
...
opcode_add,
opcode_not_zero,
opcode_jump,
opcode_default,
opcode_default,
... // etc.
};
Cでopcode_tableの冗長性を削除する方法がわからないことに注意してください。おそらくそれについて質問する必要があります。:)
スイッチはOpen Closeプリンシパルを壊すので、良い方法ではありません。これが私のやり方です。
public class Animal
{
public abstract void Speak();
}
public class Dog : Animal
{
public virtual void Speak()
{
Console.WriteLine("Hao Hao");
}
}
public class Cat : Animal
{
public virtual void Speak()
{
Console.WriteLine("Meauuuu");
}
}
そして、ここにそれを使用する方法があります(あなたのコードを取ります):
foreach (var animal in zoo)
{
echo animal.speak();
}
基本的に、私たちがしていることは、親に子供をどうするかを決めるのではなく、子クラスに責任を委任することです。
また、「リスコフの代替原則」について読むこともできます。
連想配列を使用するJavaScriptの場合:
これ:
function getItemPricing(customer, item) {
switch (customer.type) {
// VIPs are awesome. Give them 50% off.
case 'VIP':
return item.price * item.quantity * 0.50;
// Preferred customers are no VIPs, but they still get 25% off.
case 'Preferred':
return item.price * item.quantity * 0.75;
// No discount for other customers.
case 'Regular':
case
default:
return item.price * item.quantity;
}
}
これになる:
function getItemPricing(customer, item) {
var pricing = {
'VIP': function(item) {
return item.price * item.quantity * 0.50;
},
'Preferred': function(item) {
if (item.price <= 100.0)
return item.price * item.quantity * 0.75;
// Else
return item.price * item.quantity;
},
'Regular': function(item) {
return item.price * item.quantity;
}
};
if (pricing[customer.type])
return pricing[customer.type](item);
else
return pricing.Regular(item);
}
if / elseへの別の投票。それらを使用しない人もいるので、私はcase文やswitch文の大ファンではありません。ケースやスイッチを使用すると、コードが読みにくくなります。読みやすさは劣らないかもしれませんが、コマンドを使用する必要がなかった人には読みやすいでしょう。
オブジェクトファクトリについても同様です。
if / elseブロックは、誰もが簡単に取得できる構造です。問題を引き起こさないようにするためにできることがいくつかあります。
まず、ifステートメントを数回以上インデントしないでください。自分がインデントしていることに気づいたら、それは間違っています。
if a = 1 then
do something else
if a = 2 then
do something else
else
if a = 3 then
do the last thing
endif
endif
endif
本当に悪いです-代わりにこれをしてください。
if a = 1 then
do something
endif
if a = 2 then
do something else
endif
if a = 3 then
do something more
endif
最適化はおろそかにされる。コードの速度はそれほど変わりません。
次に、明確にするために特定のコードブロックに十分なbreakステートメントが散在している限り、Ifブロックから抜け出すことを嫌いません。
procedure processA(a:int)
if a = 1 then
do something
procedure_return
endif
if a = 2 then
do something else
procedure_return
endif
if a = 3 then
do something more
procedure_return
endif
end_procedure
編集:スイッチで、なぜ私はそれを理解するのが難しいと思います:
これがswitchステートメントの例です...
private void doLog(LogLevel logLevel, String msg) {
String prefix;
switch (logLevel) {
case INFO:
prefix = "INFO";
break;
case WARN:
prefix = "WARN";
break;
case ERROR:
prefix = "ERROR";
break;
default:
throw new RuntimeException("Oops, forgot to add stuff on new enum constant");
}
System.out.println(String.format("%s: %s", prefix, msg));
}
私にとってここでの問題は、Cのような言語で適用される通常の制御構造が完全に壊れていることです。制御構造内に複数行のコードを配置する場合は、中かっこまたはbegin / endステートメントを使用するという一般的な規則があります。
例えば
for i from 1 to 1000 {statement1; statement2}
if something=false then {statement1; statement2}
while isOKtoLoop {statement1; statement2}
私にとっては(そして、私が間違っていれば、あなたは私を訂正することができます)、Caseステートメントはこのルールをウィンドウの外にスローします。条件付きで実行されるコードブロックは、開始/終了構造内に配置されません。このため、ケースは概念的には十分に使用されないほど異なると思います。
それがあなたの質問に答えることを願っています。