関数とswitchステートメントのマップ


21

私はリクエストを処理するプロジェクトに取り組んでおり、リクエストにはコマンドとパラメーターの2つのコンポーネントがあります。各コマンドのハンドラーは非常にシンプルです(<10行、多くの場合<5)。少なくとも20のコマンドがあり、50を超える可能性があります。

私はいくつかの解決策を考え出しました:

  • コマンド上の1つの大きなスイッチ/ if-else
  • 機能へのコマンドのマップ
  • 静的クラス/シングルトンへのコマンドのマップ

各コマンドは少しエラーチェックを行い、抽象化できるビットは、各コマンドに定義されているパラメーターの数をチェックすることだけです。

この問題の最善の解決策は何ですか?また、その理由は何ですか?また、私が見逃したかもしれないデザインパターンにもオープンです。

それぞれについて、次の賛否両論のリストを作成しました。

スイッチ

  • 長所
    • すべてのコマンドを1つの関数に保持します。シンプルなので、これは視覚的なルックアップテーブルになります
    • 1か所でしか使用されない大量の小さな関数/クラスでソースを乱雑にする必要はありません。
  • 短所
    • とても長い
    • プログラムでコマンドを追加するのが難しい(デフォルトのケースを使用してチェーンする必要がある)

マップコマンド->関数

  • 長所
    • 小さい一口サイズのチャンク
    • プログラムでコマンドを追加/削除できる
  • 短所
    • インラインで行う場合、スイッチと視覚的に同じ
    • インラインで行われない場合、多くの機能が1か所でのみ使用されます

マップコマンド->静的クラス/シングルトン

  • 長所
    • ポリモーフィズムを使用して単純なエラーチェックを処理できます(3行だけですが、それでも)
    • マップと同様のメリット->機能ソリューション
  • 短所
    • 多くの非常に小さなクラスがプロジェクトを混乱させます
    • 実装がすべて同じ場所にあるわけではないため、実装をスキャンするのは簡単ではありません

追加のメモ:

私はGoでこれを書いていますが、ソリューションは言語固有のものではないと思います。他の言語でも非常に似たようなことをする必要があるかもしれないので、私はより一般的な解決策を探しています。

コマンドは文字列ですが、便利であれば、これを簡単に数値にマップできます。関数のシグネチャは次のようなものです。

Reply Command(List<String> params)

Goにはトップレベルの機能があり、私が検討している他のプラットフォームにもトップレベルの機能があるため、2番目と3番目のオプションの違いがあります。


8
コマンドを関数にマップし、実行時に構成からロードします。コマンドパターンを使用します。
スティーブンエバーズ

3
たくさんの小さな関数を作成することを恐れないでください。通常、いくつかの小さな関数のコレクションは、1つの巨大な関数よりも保守が容易です。
バートヴァンインゲンシェナウ

8
人々がコメントで質問に答え、答えでより多くの情報を要求する、このトプシータービーワールドとは何ですか?
pdr

4
@SteveEvers:精緻化する必要がない場合、それはどれほど短くても答えです。もしそれがあり、あなたが時間や何も持っていないなら、他の誰かが答えるのを待ってください(すでに半ダースの賛成票を持っているコメントを確認する答えを書くために常に不正行為のように感じます)。個人的には、これには精緻化が必要だと感じています。OPは、なぜ最高のソリューションが最高のソリューションであるのかを知りたいと思っています。
pdr

1
@pdr-そう。私の傾向は機能へのコマンドのマップでしたが、私はCS設計コースの比較的若いプログラマーです。私の教授はたくさんのクラスが好きなので、少なくとも2つの正当な解決策があります。コミュニティのお気に入りを知りたかった。
beatgammit

回答:


14

これは、マップに最適です(2番目または3番目に提案されたソリューション)。何十回も使用しましたが、シンプルで効果的です。私はこれらのソリューションを本当に区別していません。重要な点は、キーとして関数名を持つマップがあることです。

私の意見では、マップアプローチの主な利点は、テーブルがデータであることです。 これは、実行時に渡されたり、追加されたり、変更されたりすることを意味します。また、新しいエキサイティングな方法でマップを解釈する追加の関数をコーディングするのも簡単です。これは、ケース/スイッチソリューションでは不可能です。

あなたが言及した短所は実際には経験していませんが、追加の欠点に言及したいと思います:文字列名だけが重要であればディスパッチは簡単ですが、実行する機能を決定するために追加情報を考慮する必要がある場合、あまりきれいではありません。

おそらく、私は十分に難しい問題に遭遇したことは一度もないが、他の人が述べたように、コマンドパターンとディスパッチをクラス階層としてエンコードすることの両方にほとんど価値がない。問題の中心は、リクエストを関数にマッピングすることです。マップはシンプルで、明白で、テストが簡単です。クラス階層では、より多くのコードと設計が必要であり、そのコード間の結合が増加し、後で変更する必要があると思われるより多くの決定を前もって行うように強制される場合があります。コマンドパターンはまったく当てはまらないと思います。


4

あなたの問題はCommandデザインパターンに非常に役立ちます。したがって、基本的にはベースCommandインターフェイスがあり、CommandImplそのインターフェイスを実装する複数のクラスがあります。インターフェースには、基本的に単一のメソッドが必要doCommand(Args args)です。Argsクラスのインスタンスを介して引数を渡すことができます。このように、不格好なif / elseステートメントではなく、ポリモーフィズムの力を活用します。また、この設計は簡単に拡張できます。


3

switchステートメントを使用するかOOスタイルのポリモーフィズムを使用するかを尋ねるたびに、Expression Problemを参照します。基本的に、データに異なる「ケース」があり、異なる「アクション」をサポートする杖がある場合(各アクションはケースごとに異なることを行います)、新しいケースと新しいアクションの両方を自然に追加できるシステムを作るのは本当に難しいです将来は。

switchステートメント(または訪問者パターン)を使用する場合、新しいアクションを追加するのは簡単ですが(単一の関数ですべてを書くため)、新しいケースを追加するのは困難です(元の関数に戻って編集する必要があるため)

逆に、オブジェクト指向スタイルのポリモーフィズムを使用する場合、新しいケースを追加するのは簡単ですが(新しいクラスを作成するだけ)、インターフェイスにメソッドを追加するのは困難です(そのため、戻ってクラスの束を編集する必要があるため)

あなたの場合、サポートする必要があるメソッドは1つしかなく(リクエストを処理する)、可能性のあるケースはたくさんあります(それぞれ異なるコマンド)。新しいケースを簡単に追加できるようにすることは、新しいメソッドを追加することよりも重要なので、異なるコマンドごとに個別のクラスを作成するだけです。


ところで、拡張性の観点から見ると、クラスを使用するか関数を使用するかに大きな違いはありません。switchステートメントと比較する場合、重要なのは、「ディスパッチ」され、クラスと関数の両方が同じようにディスパッチされる方法です。したがって、言語でより便利なものを使用するだけです(Goには字句スコープとクロージャーがあるため、クラスと関数の違いは実際には非常に小さいです)。

たとえば、通常、継承に依存する代わりに委任を使用してエラーチェックの部分を実行できます(Oの構文はGoがわからないため、私の例はJavascriptです。気にしないでください)

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:すべての問題を解決する最善の方法はありません。「モノを機能させる」という観点から、重要な不変条件を強制し、ミスからユーザーを保護する方法で抽象化を作成することに焦点を当てます。「将来を保証する」という観点から、コードのどの部分が拡張される可能性が高いかを念頭に置いてください。


1

私はgoを使用したことはありません。ac#プログラマーとして、おそらく次の行に進みます。このアーキテクチャがあなたのしていることに合うことを願っています。

実行するメイン関数を使用してそれぞれに小さなクラス/オブジェクトを作成します。それぞれが文字列表現を知っている必要があります。これにより、機能の数が増えるにつれて、プラグインが必要なように聞こえます。あなたが本当に必要としない限り、私は静的関数を使用しないことに注意してください。それらはあまり利点を与えません。

その後、実行時にこれらのクラスを発見し、異なるアセンブルなどからロードされるように変更する方法を知っているファクトリーがあります。これは、同じプロジェクトですべてを必要としないことを意味します。

したがって、テスト用にモジュール化され、コードのサイズが小さくなり、後で保守しやすくなります。


0

コマンド/機能をどのように選択しますか?

正しい関数を選択する「賢い」方法があれば、それが道です-コアロジックを変更せずに(おそらく外部ライブラリで)新しいサポートを追加できることを意味します。

また、個々の機能のテストは、膨大なswitchステートメントよりも簡単に分離できます。

最後に、1か所でしか使用されていません。50に達すると、異なる機能の異なるビットを再利用できることに気付くでしょう。


コマンドは一意の文字列です。必要に応じて、これらを整数にマッピングできます。
beatgammit

0

Goの仕組みはわかりませんが、ActionScriptで使用したアーキテクチャは、責任の連鎖として機能する二重リンクリストを持つことです。各リンクにはdeterminResponsibility関数があります(これはコールバックとして実装しましたが、あなたがやろうとしていることにうまく機能するなら、各リンクを別々に書くことができます)。リンクに責任があると判断された場合、meetResponsibility(再び、コールバック)が呼び出され、チェーンが終了します。責任がない場合は、チェーン内の次のリンクにリクエストを渡します。

既存のチェーンのリンク間(または既存のチェーンの最後)に新しいリンクを追加するだけで、さまざまなユースケースを簡単に追加および削除できます。

これは、関数のマップの考え方に似ていますが、微妙に異なります。良い点は、リクエストを渡すだけで、他に何もせずにそのことを行うということです。欠点は、値を返す必要がある場合にうまく機能しないことです。

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