Есть ли более разумный способ сделать это, кроме длинной цепочки операторов 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
3
@mattnz любой, кто знаком с Java, узнает его в приведенном им примере кода.
jwenting
6
@jwenting: Это также правильный синтаксис C #, и я уверен, что есть больше языков.
phresnel
4
@phresnel да, но имеют ли они точно такой же стандартный API для String?
апреля
3
@jwenting: это актуально? Но даже если: можно создать правильные примеры, например, например, библиотеку Java / C # -Interop Helper, или взглянуть на Java для .net: ikvm.net. Язык всегда актуален. Возможно, спрашивающий не ищет определенные языки, возможно, он / она допустил синтаксические ошибки (например, случайно конвертировал Java в C #), могут возникнуть новые языки (или возникли в дикой природе, где есть драконы) - править : Мои предыдущие комментарии были чокнутыми, извините.
Френель

Ответы:

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 # вы, вероятно, будете использовать лямбда-выражения для этой цели, или вы будете имитировать «указатель на функции», используя шаблон команды. В бесплатной онлайн-книге « Perl высшего порядка » есть полная глава о таблицах отправки с использованием Perl.

Док Браун
источник
4
Однако проблема в том, что вы не сможете управлять механизмом сопоставления. В примере OP он использует equalsIgnoreCaseдля «сейчас играет», но toLowerCase().startsWithдля других.
mrjink
5
@mrjink: я не вижу в этом «проблемы», это всего лишь другой подход с разными плюсами и минусами. «За» вашего решения: отдельные команды могут иметь индивидуальные механизмы сопоставления. Мой «за»: Команды не должны предоставлять собственный механизм сопоставления. ОП должен решить, какое решение подходит ему лучше всего. Кстати, я также проголосовал за ваш ответ.
Док Браун
1
К вашему сведению - «Указатель на функцию» - это функция языка C #, называемая делегатами. Лямбда - это скорее объект выражения, который можно передавать - вы не можете «вызвать» лямбду, как вы можете вызвать делегата.
user1068
1
Поднимите toLowerCaseоперацию из цикла.
zwol
1
@HarrisonNguyen: я рекомендую docs.oracle.com/javase/tutorial/java/javaOO/… . Но если вы не используете Java 8, вы можете просто использовать определение интерфейса mrink без части «совпадения», это на 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
    }
}
mrjink
источник
Если сообщения не нужно анализировать в другом месте (я предполагал, что в моем ответе) это действительно предпочтительнее, чем мое решение, поскольку Commandоно более автономно, если оно знает, когда вызывать себя. Это приводит к небольшим издержкам, если список команд огромен, но это, вероятно, незначительно.
JHR
1
Этот шаблон хорошо подходит для парадигмы консоли, но только потому, что он аккуратно отделяет командную логику от шины событий и потому что новые команды, вероятно, будут добавлены в будущем. Я думаю, что следует отметить, что это не решение для любых обстоятельств, когда у вас есть длинная цепь, если ... иначе
Нил
+1 за использование «легкого» шаблона Команды! Следует отметить, что когда вы имеете дело с не-строками, вы можете использовать, например, интеллектуальные перечисления, которые знают, как выполнять свою логику. Это даже спасет вас от петли.
LastFreeNickname
3
Вместо цикла мы могли бы использовать это как карту, если вы сделаете Command абстрактным классом, который переопределяет equalsи hashCodeбудет таким же, как строка, представляющая команду
Cruncher
Это удивительно и легко понять. Спасибо за предложение. Это в значительной степени именно то, что я искал, и кажется, что это возможно для будущего добавления команд.
Харрисон Нгуен
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 );
Falco
источник
Не ясно, что он использует Java, хотя это элегантное решение.
Нил
Вы правы - код выглядел очень похожим на автоматически форматированный Java-код eclipse ... Но вы могли бы сделать то же самое с C #, а с C ++ вы могли бы имитировать аннотации с помощью некоторых умных макросов
Falco
Это вполне может быть Java, однако в будущем я предлагаю вам избегать языковых решений, если язык не указан четко. Просто дружеский совет.
Нил
Спасибо за это решение - вы были правы, что я использую Java в предоставленном коде, хотя я и не указал, это выглядит очень мило, и я обязательно попробую. Извините, я не могу выбрать два лучших ответа!
Харрисон Нгуен
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);
        }
    }
}

И так далее для всего остального.

В вашем основном классе у вас есть список поведений ( List<IChatBehaviour>), которые реализует ваш IRC-бот. Затем вы можете заменить свои ifзаявления чем-то вроде этого:

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

Выше должно уменьшить количество кода, который у вас есть. Вышеуказанный подход также позволит вам предоставить дополнительные варианты поведения вашему классу ботов без изменения самого класса ботов (согласно Strategy Design Pattern).

Если вы хотите, чтобы одновременно запускалось только одно поведение, вы можете изменить сигнатуру executeметода на yield true( falseповедение сработало ) или (поведение не сработало) и заменить приведенный выше цикл следующим образом:

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

Вышеприведенное было бы более утомительным для реализации и инициализации, поскольку вам нужно создавать и передавать все дополнительные классы, однако это должно сделать вашего бота легко расширяемым и модифицируемым, поскольку все ваши классы поведения будут инкапсулированы и, будем надеяться, независимы друг от друга.

npinti
источник
1
Куда делись if? Т.е. как вы решаете, что поведение выполняется для команды?
mrjink
2
@mrjink: простите, мой плохой. Решение о том, выполнять или нет, делегируется поведению (я ошибочно пропустил ifроль в поведении).
npinti
Я думаю, что я предпочитаю проверку того, IChatBehaviourможет ли данное лицо обрабатывать данную команду, поскольку она позволяет вызывающей стороне делать больше с ней, например, обрабатывать ошибки, если ни одна из команд не соответствует, хотя на самом деле это всего лишь личное предпочтение. Если это не нужно, то нет смысла бесполезно усложнять код.
Нил
вам все еще нужен способ сгенерировать экземпляр правильного класса, чтобы выполнить его, который по-прежнему имел бы ту же самую длинную цепочку операторов
if
1

«Интеллигентным» может быть (как минимум) три вещи:

Более высокая производительность

Таблица рассылки (и ее эквиваленты) является хорошим предложением. Такая таблица называлась "CADET" в прошлые годы для "Не могу добавить; даже не пытается". Однако, рассмотрите комментарий, чтобы помочь начинающему сопровождающему только о том, как управлять упомянутой таблицей.

Ремонтопригодность

«Сделай это красиво» - не пустое наставление.

и часто упускают из виду ...

упругость

Использование 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 может быть хорошей идеей для команд, в зависимости от того, сколько вы ожидаете создать.)

JHR
источник
Я думаю, что это хорошо для развязки и организации программ, хотя я не думаю, что это напрямую решает проблему с наличием слишком большого количества выражений if ... elseif.
Нил
0

Что бы я сделал, это:

  1. Сгруппируйте имеющиеся у вас команды в группы. (у вас есть по крайней мере 20 прямо сейчас)
  2. На первом уровне классифицируйте по группам, чтобы у вас была команда, связанная с именем пользователя, команды песни, команды подсчета и т. Д.
  3. Затем вы идете в метод каждой группы, на этот раз вы получите оригинальную команду.

Это сделает это более управляемым. Больше пользы, когда число «еще, если» растет слишком сильно.

Конечно, иногда иметь это «если бы еще» не было большой проблемой. Я не думаю, что 20 это так плохо.

InformedA
источник