私はリクエストを処理するプロジェクトに取り組んでおり、リクエストにはコマンドとパラメーターの2つのコンポーネントがあります。各コマンドのハンドラーは非常に単純です(10行未満、多くの場合5行未満)。少なくとも20個のコマンドがあり、50個を超える可能性があります。
私はいくつかの解決策を考え出しました:
各コマンドは少しのエラーチェックを行い、抽出できる唯一のビットは、各コマンドに定義されているパラメータの数をチェックすることです。
この問題の最善の解決策は何ですか?なぜですか?私はまた、見逃したかもしれないどんなデザインパターンにもオープンです。
それぞれについて次の賛成/反対のリストを考え出しました:
スイッチ
マップコマンド->関数
マップコマンド->静的クラス/シングルトン
補足:
私はGoでこれを書いていますが、解決策は言語固有ではないと思います。他の言語で非常に似たようなことをする必要があるかもしれないので、より一般的な解決策を探しています。
コマンドは文字列ですが、必要に応じて簡単に数値にマッピングできます。関数のシグネチャは次のようなものです:
Reply Command(List<String> params)
Goにはトップレベルの機能があり、私が検討している他のプラットフォームにもトップレベルの機能があるため、2番目と3番目のオプションの違いがあります。
これはマップに最適です(2番目または3番目の提案されたソリューション)。私はそれを何十回も使用したことがあり、シンプルで効果的です。私はこれらのソリューションを本当に区別しません。重要な点は、関数名をキーとするマップがあることです。
私の意見では、マップアプローチの主な利点は、テーブルがデータであることですこれは、テーブルを渡したり、拡張したり、その他の方法で渡したりできることを意味します実行時に変更されます。また、マップを新しくエキサイティングな方法で解釈する追加の関数を簡単にコーディングすることもできます。これは、ケース/スイッチソリューションでは不可能です。
私はあなたが言及している短所を実際には経験していませんが、追加の欠点について言及したいと思います:ディスパッチは文字列名だけが重要である場合は簡単ですが、実行する関数を決定するために追加情報を考慮する必要がある場合、それほどきれいではありません。
おそらく、私は十分な困難な問題に遭遇したことがないだけですが、他の人が述べたように、コマンドパターンとディスパッチをクラス階層としてエンコードすることの両方にほとんど価値がありません。問題の中核は、リクエストを関数にマッピングすることです。マップはシンプルで明白で、テストが簡単です。クラス階層は、より多くのコードと設計を必要とし、そのコード間のカップリングを増やし、後で変更する必要がある可能性が高いより多くの決定を前もって行わなければならない場合があります。コマンドパターンはまったく当てはまらないと思います。
あなたの問題は コマンドデザインパターン に非常に適しています。したがって、基本的にはベースのCommand
インターフェースがあり、そのインターフェースを実装する複数のCommandImpl
クラスがあります。インターフェースは基本的に単一のメソッドdoCommand(Args args)
を持つ必要があります。 Args
クラスのインスタンスを介して引数を渡すことができます。このように、不格好なif/elseステートメントではなく、ポリモーフィズムの力を活用します。また、このデザインは簡単に拡張できます。
SwitchステートメントとOOスタイルのポリモーフィズムのどちらを使用するかを尋ねるときはいつでも、 式の問題 を参照します。基本的に、データと異なる「アクション」をサポートするための異なる「ケース」があり、各アクションがケースごとに異なる場合、新しいケースと新しいアクションの両方を自然に追加できるシステムを作成するのは非常に困難です。将来は。
Switchステートメント(またはVisitorパターン)を使用する場合、新しいアクションを追加するのは簡単ですが(すべてを単一の関数で処理するため)、新しいケースを追加するのは困難です(以前の関数に戻って編集する必要があるため)。
逆に、OOスタイルのポリモーフィズムを使用する場合、新しいケースを追加するのは簡単ですが(新しいクラスを作成するだけ)、インターフェースにメソッドを追加するのは困難です(戻って一連のクラスを編集する必要があるため)
あなたの場合、サポートする必要があるメソッドは1つしかありません(要求を処理します)が、多くの可能なケース(それぞれ異なるコマンド)があります。新しいメソッドを追加するよりも新しいケースを簡単に追加できるようにすることが重要であるため、コマンドごとに個別のクラスを作成します。
ちなみに、拡張性の観点から見ると、クラスと関数のどちらを使用しても大きな違いはありません。 switchステートメントと比較する場合、重要なのは、どのように「ディスパッチ」され、クラスと関数の両方が同じようにディスパッチされるかです。したがって、言語でより便利なものを使用するだけです(Goには字句スコープとクロージャーがあるため、クラスと関数の違いは実際には非常に小さいものです)。
たとえば、継承に依存する代わりに、通常は委任を使用してエラーチェックの部分を実行できます(私の例はJavaScriptです。O構文はGo構文を知らないので、気にしないでください)
function make_command(real_command){
return function(x){
if(check_arguments(x)){
return real_command(x);
}else{
//handle error here
}
}
}
command1 = make_command(function(x){
//do something
})
command2 = make_command(function(x){
//do something else
})
command1(17);
commnad2(42);
もちろん、この例では、すべてのケースでラッパー関数または親クラスが引数をチェックする賢明な方法があることを前提としています。状況によっては、check_argumentsの呼び出しをコマンド自体の内部に置く方が簡単な場合があります(引数の数やコマンドの種類などが異なるため、各コマンドは異なる引数でチェック関数を呼び出す必要がある場合があるため)。
tl; dr:すべての問題を解決する最良の方法はありません。 「物事を機能させる」という観点からは、重要な不変条件を強制し、間違いからユーザーを保護する方法で抽象化を作成することに焦点を当てます。 「将来を保証する」という観点からは、コードのどの部分が拡張される可能性が高いかを覚えておいてください。
私はgoを使用したことがありません。c#プログラマーとして、おそらく次の行に進むでしょう。
メイン関数を実行するために、それぞれに小さなクラス/オブジェクトを作成します。それぞれが文字列表現を知っている必要があります。これによりプラグインが可能になり、機能の数が増えるので、あなたが望むように聞こえます。本当に必要な場合を除いて、私はstaticを使用しないことに注意してください。それらは利点の多くを与えません。
次に、実行時にこれらのクラスを検出して、異なるアセンブルなどからロードされるように変更する方法を知っているファクトリーを用意します。これは、同じプロジェクトですべてを必要としないことを意味します。
したがって、テストのためのモジュール化も進んでおり、コードを小さく、小さくする必要があります。これにより、後で保守しやすくなります。
Goがどのように機能するかはわかりませんが、ActionScriptで使用したアーキテクチャは、責任の連鎖として機能する二重にリンクされたリストを持つことです。各リンクには、determineResponsibility関数があります(これはコールバックとして実装しましたが、実行しようとしていることにうまく機能する場合は、各リンクを個別に作成することもできます)。リンクが責任があると判断した場合、リンクはmeetResponsibility(これもコールバック)を呼び出し、それによってチェーンが終了します。責任がない場合は、チェーン内の次のリンクにリクエストを渡します。
既存のチェーンのリンクの間に(または最後に)新しいリンクを追加するだけで、さまざまなユースケースを簡単に追加および削除できます。
これは、関数マップのアイデアに似ていますが、微妙に異なります。いいのは、リクエストを渡すだけで、他に何もすることなくそのリクエストを処理できることです。欠点は、値を返す必要がある場合にうまく機能しないことです。
コマンド/関数をどのように選択しますか?
正しい関数を選択する「賢い」方法がある場合、それが先の方法です。つまり、コアロジックを変更せずに、(おそらく外部ライブラリに)新しいサポートを追加できます。
また、個々の関数をテストすることは、膨大なswitchステートメントよりも分離して簡単です。
最後に、1つの場所でのみ使用されています。50に到達すると、さまざまな関数のさまざまなビットを再利用できることに気付くでしょうか。