ifステートメントまたはスイッチの長いチェーン以外に、これを行うためのよりインテリジェントな方法はありますか?


20

メッセージを受信するIRCボットを実装し、そのメッセージをチェックして、呼び出す関数を決定しています。これを行うより賢い方法はありますか?20個のコマンドが好きになった後、すぐに手に負えなくなるようです。

おそらくこれを抽象化するより良い方法がありますか?

 public void onMessage(String channel, String sender, String login, String hostname, String message){

        if (message.equalsIgnoreCase(".np")){
//            TODO: Use Last.fm API to find the now playing
        } else if (message.toLowerCase().startsWith(".register")) {
                cmd.registerLastNick(channel, sender, message);
        } else if (message.toLowerCase().startsWith("give us a countdown")) {
                cmd.countdown(channel, message);
        } else if (message.toLowerCase().startsWith("remember am routine")) {
                cmd.updateAmRoutine(channel, message, sender);
        }
    }

14
この詳細レベルで重要な言語は何ですか。
マッテンツ14年

3
@mattnzは、Javaに精通している人なら誰でも、提供するコードサンプルでそれを認識します。
jwenting

6
@jwenting:有効なC#構文でもあり、他にも言語があると思います。
フレネル14年

4
@phresnelはい、しかしそれらは文字列に対してまったく同じ標準APIを持っていますか?
14年

3
@jwenting:それは関連性がありますか?ただし、次の場合でも:Java / C#-Interop Helper Libraryなどの有効な例を作成するか、.netのJavaを参照してください:ikvm.net。言語は常に関連しています。質問者は特定の言語を探していないかもしれません、構文エラーを犯しているかもしれません(例えばJavaをC#に誤って変換しているかもしれません)、新しい言語が発生するかもしれません:私の以前のコメントは気まずい、ごめんなさい。
フレネル14年

回答:


45

ディスパッチテーブルを使用します。これは、ペア(「メッセージ部分」、pointer-to-function)を含むテーブルです。ディスパッチャーは次のようになります(擬似コードで):

for each (row in dispatchTable)
{
    if(message.toLowerCase().startsWith(row.messagePart))
    {
         row.theFunction(message);
         break;
    }
}

equalsIgnoreCase以前のどこかで特別なケースとして扱うことができます。または、これらのテストが多数ある場合は、2番目のディスパッチテーブルを使用します)。

もちろん、どのpointer-to-functionように見えるかはプログラミング言語に依存します。ここで CまたはC ++の例です。JavaまたはC#では、おそらくそのためにラムダ式を使用するか、コマンドパターンを使用して「関数へのポインター」をシミュレートします。無料のオンラインブック「Higher Order Perl」には、Perlを使用したディスパッチテーブルに関する完全な章があります。


4
ただし、これに関する問題は、一致メカニズムを制御できないことです。OPの例では、彼はequalsIgnoreCase「現在プレイ中」に使用toLowerCase().startsWithしていますが、他の人には使用しています。
mrjink 14年

5
@mrjink:私はこれを「問題」とは見ていません。異なる長所と短所を持つ異なるアプローチにすぎません。ソリューションの「長所」:個々のコマンドには、個々のマッチングメカニズムがあります。私の「長所」:コマンドは、独自の一致メカニズムを提供する必要はありません。OPは、彼に最も適したソリューションを決定する必要があります。ところで、私もあなたの答えを支持しました。
ドックブラウン

1
参考までに、「機能へのポインター」はデリゲートと呼ばれるC#言語機能です。Lambdaは、渡すことができる式オブジェクトのようなものです。デリゲートを呼び出すことができるように、ラムダを「呼び出す」ことはできません。
user1068 14年

1
toLowerCase操作をループから引き上げます。
zwol

1
@HarrisonNguyen:docs.oracle.com/javase/tutorial/java/javaOO/…をお勧めします。ただし、Java 8を使用していない場合は、「一致」部分なしでmrinkのインターフェイス定義を使用できます。これは100%同等です(つまり、「コマンドパターンを使用して関数へのポインターをシミュレートする」という意味です)。コメントは、異なるプログラミング言語での同じ概念の異なるバリアントの異なる用語についてのコメントです。
Doc Brown

31

私はおそらく次のようなことをするでしょう:

public interface Command {
  boolean matches(String message);

  void execute(String channel, String sender, String login,
               String hostname, String message);
}

その後、すべてのコマンドでこのインターフェイスを実装し、メッセージと一致したときにtrueを返すことができます。

List<Command> activeCommands = new ArrayList<>();
activeCommands.add(new LastFMCommand());
activeCommands.add(new RegisterLastNickCommand());
// etc.

for (Command command : activeCommands) {
    if (command.matches(message)) {
        command.execute(channel, sender, login, hostname, message);
        break; // handle the first matching command only
    }
}

メッセージを他の場所で解析する必要がない場合(答えであると思います)Command、それがいつ自分自身を呼び出すべきかを知っている場合、これは私のソリューションよりも望ましいです。コマンドのリストが巨大な場合、わずかなオーバーヘッドが発生しますが、それはおそらく無視できます。
jhr 14年

1
このパターンは、コンソールコマンドのパラダイムにうまくフィットする傾向がありますが、それは、コマンドロジックをイベントバスからきちんと分離し、新しいコマンドが将来追加される可能性があるためです。これは、if ... elseifの長いチェーンがある状況の解決策ではないことに留意すべきだと思います
ニール

「ライト」コマンドパターンを使用するための+1!非文字列を扱う場合、たとえば、ロジックの実行方法を知っているインテリジェントな列挙型を使用できることに注意してください。これにより、forループも節約できます。
LastFreeNickname 14年

3
あなたがオーバーライドすることを抽象クラスコマンドを作る場合はむしろループよりも、私たちはマップとしてこれを使用することができますequalsし、はhashCode、コマンド表す文字列と同じになるように
たけ

これは素晴らしく、理解しやすいです。提案をありがとう。それはまさに私が探していたものであり、将来のコマンドの追加のために管理できるようです。
ハリソングエン14年

15

あなたはJavaを使用している-だからそれを美しくする;-)

私はおそらく注釈を使用してこれを行うでしょう:

  1. カスタムメソッドアノテーションを作成する

    @IRCCommand( String command, boolean perfectmatch = false )
  2. クラス内のすべての関連メソッドに注釈を追加します。例

    @IRCCommand( command = ".np", perfectmatch = true )
    doNP( ... )
  3. コンストラクターでReflectionsを使用して、クラス内のすべての注釈付きメソッドからメソッドのHashMapを作成します。

    ...
    for (Method m : getDeclaredMethods()) {
    if ( isAnnotationPresent... ) {
        commandList.put(m.getAnnotation(...), m);
        ...
  4. あなたにonMessage方法だけをループを行うcommandList各1上の文字列に一致するようにしようと呼び出してmethod.invoke()、それが収まるところ。

    for ( @IRCCommand a : commanMap.keyList() ) {
        if ( cmd.equalsIgnoreCase( a.command )
             || ( cmd.startsWith( a.command ) && !a.perfectMatch ) {
            commandMap.get( a ).invoke( this, cmd );

彼がJavaを使用していることは明らかではありませんが、それはエレガントなソリューションです。
ニール

あなたは正しい-コードは日食自動フォーマットのJavaコードに非常に似ていました...しかし、C#でもC ++でも同じことができ、いくつかの巧妙なマクロで注釈をシミュレートできました
Falco

おそらくJavaかもしれませんが、将来的には、言語が明確に示されていない場合は、言語固有のソリューションを避けることをお勧めします。ただフレンドリーなアドバイス。
ニール

このソリューションに感謝します-指定しなかったとしても、提供されたコードでJavaを使用していることは正しかったです。2つのベストアンサーを選ぶことはできません。
ハリソングエン

5

インターフェースを定義すると、IChatBehaviour1つのメソッドが呼び出さExecuteれ、1つmessagecmdオブジェクトと1つのオブジェクトが取り込まれます。

public Interface IChatBehaviour
{
    public void execute(String message, CMD cmd);
}

次に、コードでこのインターフェイスを実装し、必要な動作を定義します。

public class RegisterLastNick implements IChatBehaviour
{
    public void execute(String message, CMD cmd)
    {
        if (message.toLowerCase().startsWith(".register"))
        {
            cmd.registerLastNick(channel, sender, message);
        }
    }
}

残りの部分も同様です。

メインクラスには、IRCボットが実装する動作のリスト(List<IChatBehaviour>)があります。その後、ifステートメントを次のようなものに置き換えることができます。

for(IChatBehaviour behaviour : this.behaviours)
{
    behaviour.execute(message, cmd);
}

上記はあなたが持っているコードの量を減らすはずです。上記のアプローチでは、ボットクラス自体を変更することなく、ボットクラスに追加の動作を提供することもできます(をStrategy Design Pattern参照)。

一度に1つの動作のみを起動する場合、executeメソッドのシグネチャを変更してtrue(動作が起動した)またはfalse(動作が起動しなかった)を生成し、上記のループを次のように置き換えることができます。

for(IChatBehaviour behaviour : this.behaviours)
{
    if(behaviour.execute(message, cmd))
    { 
         break;
    }
}

余分なクラスをすべて作成して渡す必要があるため、上記の実装と初期化はより面倒ですが、すべての動作クラスがカプセル化され、できれば相互に独立しているため、ボットを簡単に拡張および変更できるようにする必要があります。


1
ifsはどこに行きましたか?つまり、コマンドに対して動作が実行されることをどのように決定しますか?
mrjink

2
@mrjink:ごめんなさい。実行するかどうかの決定は、振る舞いに委任されます(動作のif一部を誤って省略しました)。
npinti 14年

IChatBehaviour特定のコマンドを処理できるかどうかのチェックを好むと思います。これは、実際には個人的な好みにすぎませんが、コマンドが一致しない場合にエラーを処理するなど、呼び出し元がそれをさらに処理できるようにするためです。それが必要でなければ、コードを不必要に複雑にすることはありません。
ニール

あなたはまだ...まだIFSまたは大規模なswitch文の同じ長鎖を持つことになり、それを実行するために正しいクラスのインスタンスを生成する方法が必要と思います
jwenting

1

「インテリジェント」には、(少なくとも)3つのことがあります。

より高い性能

ディスパッチテーブル(およびそれに相当するもの)の提案は良いものです。このようなテーブルは、「追加できません;試してさえいません」という意味で、過去何年も「CADET」と呼ばれていました。ただし、このテーブルを管理する方法について初心者のメンテナーを支援するコメントを検討してください。

保守性

「美しくする」というのは、怠慢ではありません。

そして、しばしば見落とされます...

弾力性

toLowerCaseの使用には落とし穴があります。一部の言語の一部のテキストは、magisculeとminisculeを切り替える際に苦痛を伴う再構築を行う必要があります。残念ながら、toUpperCaseにも同じ落とし穴があります。注意してください。


0

すべてのコマンドに同じインターフェースを実装させることができます。次に、メッセージパーサーは、実行するだけの適切なコマンドを返すことができます。

public interface Command {
    public void execute(String channel, String message, String sender) throws Exception;
}

public class MessageParser {
    public Command parseCommandFromMessage(String message) {
        // TODO Put your if/switch or something more clever here
        // e.g. return new CountdownCommand();
    }
}

public class Whatever {
    public void onMessage(String channel, String sender, String login, String hostname, String message) {
        Command c = new MessageParser().parseCommandFromMessage(message);
        c.execute(channel, message, sender);
    }
}

単なるコードのように見えます。はい、まだ実行するコマンドを知るためにメッセージを解析する必要がありますが、今では適切に定義されたポイントにあります。他の場所で再利用できます。(MessageParserを挿入することもできますが、それは別の問題です。また、作成する予定の数によっては、Flyweightパターンがコマンドに適している場合があります。)


これはデカップリングとプログラム編成に適していると思いますが、if ... elseifステートメントが多すぎるという問題に直接対処するとは思いません。
ニール

0

私がすることはこれです:

  1. 持っているコマンドをグループにグループ化します。(現在、少なくとも20があります)
  2. 最初のレベルでは、グループ別に分類します。そのため、ユーザー名に関連するコマンド、歌のコマンド、カウントコマンドなどがあります。
  3. 次に、各グループのメソッドに進みます。今回は元のコマンドを取得します。

これにより、これがより管理しやすくなります。「else if」の数が増えすぎると、より多くのメリットが得られます。

もちろん、これらの「他の場合」を持っていることは大きな問題ではないでしょう。20はそれほど悪いとは思いません。

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