Чи є більш розумний спосіб зробити це, крім довгого ланцюжка, якщо заяви або перемикання?


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 #, і я думаю, що є більше мов.
френель

4
@phresnel так, але чи мають такий самий стандартний API для String?
jwenting

3
@jwenting: Це актуально? Але навіть якщо: Можна сконструювати дійсні приклади, наприклад, бібліотеку помічників Java / C # -Interop або подивитися на 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
Проблема в цьому полягає в тому, що ви не зможете контролювати механізм узгодження. У прикладі ОП він використовує equalsIgnoreCaseдля "тепер грає", але toLowerCase().startsWithдля інших.
mrjink

5
@mrjink: Я не вважаю це "проблемою", це лише інший підхід з різними плюсами та мінусами. "Про" вашого рішення: окремі команди можуть мати індивідуальні механізми узгодження. Моє "профі": Команди не повинні забезпечити власний механізм узгодження. ОП має вирішити, яке рішення йому найбільше підходить. До речі, я також підтримав вашу відповідь.
Док Браун

1
FYI - "Покажчик на функцію" - це функція мови C #, яку називають делегатами. Лямбда - це більше об'єкт вираження, який можна передавати навколо - ви не можете "викликати" лямбда, як ви можете викликати делегата.
користувач1068

1
Піднімайте toLowerCaseоперацію з петлі.
zwol

1
@HarrisonNguyen: Я рекомендую docs.oracle.com/javase/tutorial/java/javaOO/… . Але якщо ви не використовуєте Java 8, ви можете просто використовувати визначення інтерфейсу mrink без частини "match", це на 100% еквівалент (саме це я мав на увазі під "моделюванням покажчика на функцію за допомогою команди команд). І user1068's коментар - це лише зауваження про різні терміни для різних варіантів однієї і тієї ж концепції в різних мовах програмування
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

1
Цей візерунок, як правило, добре відповідає парадигмі команди консолі, але лише тому, що він чітко відокремлює логіку команд від шини подій і тому, що в майбутньому, ймовірно, будуть додані нові команди. Я думаю, що слід зазначити, що це не є рішенням для будь-яких обставин, коли у вас довгий ланцюжок, якщо ... elseif
Ніл

+1 за використання "легкого" шаблону команд! Слід зазначити, що, маючи справу з не-струнними, ви можете скористатися, наприклад, розумними перелічниками, які вміють виконувати свою логіку. Це навіть економить вам цикл for.
LastFreeNickname

3
Замість циклу ми можемо використовувати це як карту, якщо зробити команду абстрактним класом, який переосмислює 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намагаючись узгодити String на кожному з них і зателефонуйте, 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 в наданому коді, хоча я не вказував, це виглядає дуже приємно, і я обов'язково сфотографую його. Вибачте, що не можу вибрати дві найкращі відповіді!
Harrison Nguyen

5

Що робити, якщо ви визначаєте інтерфейс, скажіть, IChatBehaviourякий має один метод, Executeякий називається, який приймає в a 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методу, щоб отримати true(поведінка запустилася) або false(поведінка не спрацювала) та замінити вищевказаний цикл чимось таким:

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

Вищезазначене було б більш виснажливим для впровадження та ініціалізації, оскільки вам потрібно створити та пройти всі додаткові класи, однак це повинно зробити ваш бот легко розширюваним та модифікованим, оскільки всі ваші класи поведінки будуть інкапсульовані та, сподіваємось, незалежними один від одного.


1
Куди ifпішли? Тобто, як ви вирішите, що поведінка виконується для команди?
mrjink

2
@mrjink: Вибачте мене погано. Рішення про те, чи слід виконувати чи ні, делегується поведінці (я помилково опустив свою ifчастину в поведінці).
npinti

Я думаю, що я віддаю перевагу перевірці, чи IChatBehaviourможе дана команда обробляти дану команду, оскільки вона дозволяє абоненту робити більше з нею, як помилки обробки, якщо жодна команда не відповідає, хоча це дійсно лише особиста перевага. Якщо це не потрібно, то немає жодного сенсу ускладнювати код.
Ніл

вам все одно знадобиться спосіб згенерувати екземпляр правильного класу для того, щоб його виконати, який би все одно мав би такий самий довгий ланцюжок ifs або масивний оператор переключення ...
jwenting

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 може бути хорошою ідеєю для команд, залежно від того, скільки ви очікуєте створити.)


Я думаю, що це добре для роз'єднання та організації програми, хоча я не думаю, що це безпосередньо вирішує проблему з тим, що занадто багато, якщо ... elseif заяв.
Ніл

0

Що я б робив, це:

  1. Згрупуйте команди, які у вас є, по групах. (зараз у вас принаймні 20)
  2. На першому рівні класифікуйте за групою, щоб у вас були команди, пов’язані з іменем користувача, команди пісні, рахувати команди тощо.
  3. Потім ви переходите до методу кожної групи, на цей раз ви отримаєте оригінальну команду.

Це зробить це більш керованим. Більше користі, коли кількість «ще якщо» зростає занадто багато.

Звичайно, іноді таке "якщо інакше" не було б великою проблемою. Я не думаю, що 20 - це так погано.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.