除了一连串的if语句或switch之外,还有其他更智能的方法吗?


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
在这种详细程度下,哪种语言至关重要。
mattnz 2014年

3
@mattnz任何熟悉Java的人都会在他提供的代码示例中识别它。
jwenting 2014年

6
@jwenting:这也是有效的C#语法,我敢打赌还会有更多的语言。
phresnel 2014年

4
@phresnel是的,但是它们是否具有与String完全相同的标准API?
jwenting 2014年

3
@jwenting:相关吗?但是,即使:可以构造有效的示例,例如Java / C#-Interop Helper库,或者看看.net的Java:ikvm.net。语言始终是相关的。发问者可能未在寻找特定语言,他/她可能犯了语法错误(例如,无意中将Java转换为C#),可能出现了新语言(或在野外冒出巨龙)–编辑:我以前的评论很古怪,对不起。
phresnel 2014年

Answers:


45

使用调度表。这是一个包含对(“消息部分”,pointer-to-function)的表。然后,调度程序将如下所示(使用伪代码):

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

equalsIgnoreCase可以在以前的某个地方处理特殊情况,或者如果您有很多测试,则可以使用第二个分派表进行处理)。

当然,pointer-to-function外观必须取决于您的编程语言。是C或C ++中的示例。在Java或C#中,您可能会为此目的使用lambda表达式,或者通过使用命令模式来模拟“指向函数的指针”。免费的在线书籍“ Higher Order Perl ”有完整的章节,介绍了使用Perl的调度表。


4
但是,这样做的问题是您将无法控制匹配机制。在OP的示例中,他equalsIgnoreCase用于“正在播放”,但toLowerCase().startsWith用于其他人。
mrjink 2014年

5
@mrjink:我不认为这是一个“问题”,它只是具有不同优点和缺点的不同方法。解决方案的“优点”:单独的命令可以具有单独的匹配机制。我的“专家”:命令不必提供自己的匹配机制。OP必须决定哪种解决方案最适合他。顺便说一句,我也赞成你的回答。
布朗

1
仅供参考-“函数指针”是一种称为委托的C#语言功能。Lambda更像是一个可以传递的表达式对象-您不能像调用委托那样“调用” lambda。
2014年

1
toLowerCase操作提升到循环之外。
zwol

1
@HarrisonNguyen:我推荐docs.oracle.com/javase/tutorial/java/javaOO/…。但是,如果您不使用Java 8,则可以使用mrink的接口定义而不包含“ matches”部分,即100%等效(这就是我所说的“通过使用命令模式模拟指向函数的指针”的意思)。评论仅仅是一个关于在不同的编程语言同一概念的不同变体的不同术语表示的。
布朗博士

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 2014年

1
这种模式倾向于很好地适合于控制台命令范式,但这仅是因为该模式将命令逻辑与事件总线巧妙地分开了,并且因为将来可能会添加新命令。我认为应该指出的是,对于任何情况下,如果您的if ... elseif连锁店比较长,这都不是解决方案
Neil

+1用于使用“ light”命令模式!应该注意的是,当您处理非字符串时,可以使用例如知道如何执行其逻辑的智能枚举。这甚至可以节省您的for循环。
LastFreeNickname 2014年

3
如果您将Command设置为一个覆盖equalshashCode与表示命令的字符串相同的抽象类,则可以将其用作映射,而不是使用循环
Cruncher 2014年

这很棒而且易于理解。谢谢你的建议。这几乎就是我一直在寻找的东西,对于将来添加命令似乎是可以管理的。
Harrison Nguyen

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尝试匹配每个字符串,然后调用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,尽管这是一个很好的解决方案。
尼尔2014年

没错-代码看起来非常像Eclipse自动格式化的Java代码...但是您可以使用C#和C ++进行相同操作,并且可以使用一些聪明的宏来模拟注释
Falco 2014年

很有可能是Java,但是在将来,我建议您避免使用语言不明确的解决方案。只是友好的建议。
尼尔

感谢您提供此解决方案-您没错,尽管我未指定,但我仍在提供的代码中使用Java,这看起来确实不错,我一定会试一试。抱歉,我无法选择两个最佳答案!
Harrison Nguyen

5

如果定义一个接口,说说IChatBehaviour有一个调用了一个方法Execute并接受message一个cmd对象的方法,该怎么办?

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);
}

以上应该减少您拥有的代码量。通过上述方法,您还可以向bot类提供其他行为,而无需修改bot类本身(按照Strategy Design Pattern)。

如果您一次只希望触发一种行为,则可以将execute方法的签名更改为yield true(该行为已触发)或false(该行为未触发),然后将上述循环替换为以下内容:

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

由于您需要创建和传递所有额外的类,因此上述实现和初始化会更加繁琐,但是,由于所有行为类都将被封装并且希望彼此独立,因此它应该使您的机器人易于扩展和修改。


1
在哪儿if去吧?即,您如何确定对命令执行的行为?
mrjink 2014年

2
@mrjink:对不起,我不好。是否执行决定取决于行为(我错误地省略if了行为中的部分)。
npinti 2014年

我认为我更喜欢检查给IChatBehaviour定的命令是否可以处理给定的命令,因为它允许调用者执行更多操作,例如在没有命令匹配的情况下处理错误,尽管这实际上只是个人喜好。如果不需要它,那么毫无必要地使代码复杂化。
尼尔2014年

您仍然需要一种方法来生成正确的类的实例以执行它,它仍然具有相同的ifs或大量switch语句的长链……
jwenting 2014年

1

“智能”可以是(至少)三件事:

更高的性能

调度表(及其等效表)建议是一个很好的建议。这样的表在过去几年中被称为“无法添加;甚至无法尝试”。但是,请考虑使用注释来帮助新手维护者如何管理所述表。

可维护性

“让它变得美丽”绝非空谈。

而且,经常被忽视...

弹性

toLowerCase的使用存在一个陷阱,即某些语言的某些文本在微妙和微妙的转换之间必须经过痛苦的重组。不幸的是,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没有那么糟糕。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.