JavaRush /Blog Java /Random-PL /Zaimplementujmy Wzorzec Poleceń do pracy z botem. (Część ...
Roman Beekeeper
Poziom 35

Zaimplementujmy Wzorzec Poleceń do pracy z botem. (Część 1) - „Projekt Java od A do Z”

Opublikowano w grupie Random-PL
Witam wszystkich, drodzy przyjaciele. Dzisiaj zaimplementujemy szablon (szablon to wzór, w naszym kontekście to to samo) projektu Command na nasze potrzeby. Korzystając z tego szablonu, wygodnie i poprawnie będziemy pracować z przetwarzaniem poleceń naszego bota. „Projekt Java od A do Z”: Implementacja wzorca poleceń do pracy z botem.  Część 1 - 1
Przyjaciele, czy podoba Ci się projekt Javarush Telegram Bot ? Nie bądź leniwy: daj mu gwiazdkę . Dzięki temu będzie jasne, że jest interesujący i przyjemniej będzie go rozwijać!
Na początek dobrze byłoby porozmawiać o tym, jaki to wzór - Polecenie. Ale jeśli to zrobię, artykuł będzie bardzo duży i nieporęczny. Dlatego wybrałam materiały do ​​samodzielnej nauki:
  1. To jest mój artykuł sprzed 4 lat. Napisałem to, gdy byłem młodszy, więc nie oceniaj tego zbyt surowo.
  2. Film bardzo emocjonalnego i interaktywnego Szweda na YouTube. Gorąco polecam. Mówi pięknie, jego angielski jest jasny i zrozumiały. I ogólnie ma film o innych wzorcach projektowych.
  3. W komentarzach do mojego artykułu ktoś Nullptr35 polecił ten film .
To powinno wystarczyć, aby zanurzyć się w temacie i znaleźć się na tej samej stronie co ja. Cóż, ci, którzy są zaznajomieni z tym wzorcem projektowym, mogą bezpiecznie pominąć i przejść dalej.

Piszemy JRTB-3

Wszystko jest takie samo jak wcześniej:
  1. Aktualizujemy główną gałąź.
  2. W oparciu o zaktualizowaną gałąź główną tworzymy nowy JRTB-3 .
  3. Zaimplementujmy wzór.
  4. Tworzymy nowy commit opisujący wykonaną pracę.
  5. Tworzymy pull request, sprawdzamy go i jeśli wszystko jest w porządku, łączymy naszą pracę.
Nie będę pokazywać punktów 1-2: opisałem je bardzo dokładnie w poprzednich artykułach, więc przejdźmy od razu do wdrożenia szablonu. Dlaczego ten szablon jest dla nas odpowiedni? Tak, ponieważ za każdym razem, gdy wykonamy polecenie, przejdziemy do metody onUpdateReceived(Update update) i w zależności od polecenia wykonamy inną logikę. Bez tego wzorca mielibyśmy całą masę instrukcji if-else if. Coś takiego:
if (message.startsWith("/start")) {
   doStartCommand();
} else if(message.startsWith("/stop")) {
   doStopCommand();
} else if(message.startsWith("/addUser")) {
   doAddUserCommand();
}
...
else if(message.startsWith("/makeMeHappy")) {
   doMakeMeHappyCommand();
}
Co więcej, tam, gdzie jest elipsa, może być jeszcze kilkadziesiąt zespołów. I jak sobie z tym normalnie poradzić? Jak wspierać? Trudne i trudne. Oznacza to, że ta opcja nam nie odpowiada. Powinno to wyglądać mniej więcej tak:
if (message.startsWith(COMMAND_PREFIX)) {
   String commandIdentifier = message.split(" ")[0].toLowerCase();
   commandContainer.getCommand(commandIdentifier, userName).execute(update);
} else {
   commandContainer.getCommand(NO.getCommand(), userName).execute(update);
}
To wszystko! I niezależnie od tego, ile poleceń dodamy, ta sekcja kodu pozostanie niezmieniona. Co on robi? Pierwszy if zapewnia, że ​​wiadomość zaczyna się od przedrostka polecenia „/”. Jeśli tak jest, to wybieramy linię do pierwszej spacji i szukamy odpowiedniego polecenia w CommandContainerze, a gdy tylko je znajdziemy, uruchamiamy polecenie. I to wszystko...) Jeśli masz ochotę i czas, możesz wdrożyć pracę w zespołach, najpierw na jednych zajęciach na raz, z masą warunków i tak dalej, a potem z wykorzystaniem szablonu. Zobaczysz różnicę. Cóż to będzie za piękno! Najpierw utwórzmy pakiet obok pakietu bota, który będzie się nazywał polecenie . „Projekt Java od A do Z”: Implementacja wzorca poleceń do pracy z botem.  Część 1 - 2I już w tym pakiecie będą wszystkie klasy związane z implementacją polecenia. Potrzebujemy jednego interfejsu do pracy z poleceniami. W tym przypadku utwórzmy to:
package com.github.javarushcommunity.jrtb.command;

import org.telegram.telegrambots.meta.api.objects.Update;

/**
* Command interface for handling telegram-bot commands.
*/
public interface Command {

   /**
    * Main method, which is executing command logic.
    *
    * @param update provided {@link Update} object with all the needed data for command.
    */
   void execute(Update update);
}
W tym momencie nie musimy implementować operacji odwrotnej polecenia, więc pominiemy tę metodę (niewykonanie). W metodzie wykonywania obiekt Update występuje jako argument - dokładnie ten sam, który przychodzi do naszej głównej metody w bocie. Obiekt ten będzie zawierał wszystko, co potrzebne do przetworzenia polecenia. Następnie dodamy wyliczenie, w którym będą przechowywane wartości poleceń (start, stop itd.). Dlaczego tego potrzebujemy? Abyśmy mieli tylko jedno źródło prawdy dotyczące nazw drużyn. Tworzymy go również w naszym pakiecie poleceń . Nazwijmy to CommandName :
package com.github.javarushcommunity.jrtb.command;

/**
* Enumeration for {@link Command}'s.
*/
public enum CommandName {

   START("/start"),
   STOP("/stop");

   private final String commandName;

   CommandName(String commandName) {
       this.commandName = commandName;
   }

   public String getCommandName() {
       return commandName;
   }

}
Potrzebujemy także usługi, która będzie wysyłać wiadomości poprzez bota. W tym celu obok pakietu poleceń utworzymy pakiet usług , do którego dodamy wszystkie niezbędne usługi. W tym miejscu warto skupić się na tym, co w tym przypadku rozumiem pod pojęciem usługi. Jeśli rozważamy aplikację, często dzieli się ją na kilka warstw: warstwę do pracy z punktami końcowymi – kontrolerami, warstwę logiki biznesowej – usługi oraz warstwę do pracy z bazą danych – repozytorium. Zatem w naszym przypadku usługa jest klasą implementującą jakąś logikę biznesową. Jak poprawnie stworzyć usługę? Najpierw utwórz dla niego interfejs i implementację. Dodaj implementację za pomocą adnotacji `@Service` do kontekstu aplikacji naszej aplikacji SpringBoot i, jeśli to konieczne, dokręć ją za pomocą adnotacji `@Autowired`. Dlatego tworzymy interfejs SendBotMessageService (w usługach nazewniczych zwykle dodają Service na końcu nazwy):
package com.github.javarushcommunity.jrtb.service;

/**
* Service for sending messages via telegram-bot.
*/
public interface SendBotMessageService {

   /**
    * Send message via telegram bot.
    *
    * @param chatId provided chatId in which messages would be sent.
    * @param message provided message to be sent.
    */
   void sendMessage(String chatId, String message);
}
Następnie tworzymy jego implementację:
package com.github.javarushcommunity.jrtb.service;

import com.github.javarushcommunity.jrtb.bot.JavarushTelegramBot;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;

/**
* Implementation of {@link SendBotMessageService} interface.
*/
@Service
public class SendBotMessageServiceImpl implements SendBotMessageService {

   private final JavarushTelegramBot javarushBot;

   @Autowired
   public SendBotMessageServiceImpl(JavarushTelegramBot javarushBot) {
       this.javarushBot = javarushBot;
   }

   @Override
   public void sendMessage(String chatId, String message) {
       SendMessage sendMessage = new SendMessage();
       sendMessage.setChatId(chatId);
       sendMessage.enableHtml(true);
       sendMessage.setText(message);

       try {
           javarushBot.execute(sendMessage);
       } catch (TelegramApiException e) {
           //todo add logging to the project.
           e.printStackTrace();
       }
   }
}
Tak wygląda realizacja. Najważniejsza magia jest tam, gdzie powstaje projektant. Używając adnotacji @Autowired w konstruktorze, SpringBoot będzie szukać obiektu tej klasy w kontekście aplikacji. A on już tam jest. Działa to tak: w naszej aplikacji, gdziekolwiek możemy uzyskać dostęp do bota i coś zrobić. Ta usługa jest odpowiedzialna za wysyłanie wiadomości. Abyśmy nie pisali za każdym razem i w każdym miejscu czegoś takiego:
SendMessage sendMessage = new SendMessage();
sendMessage.setChatId(chatId);
sendMessage.setText(message);

try {
   javarushBot.execute(sendMessage);
} catch (TelegramApiException e) {
   //todo add logging to the project.
   e.printStackTrace();
}
Przenieśliśmy tę logikę do osobnej klasy i będziemy z niej korzystać, jeśli zajdzie taka potrzeba. Teraz musimy zaimplementować trzy polecenia: StartCommand, StopCommand i UnknownCommand. Potrzebujemy ich, abyśmy mieli czym wypełnić nasz kontener na polecenia. Na razie teksty będą suche i mało informacyjne, ale na potrzeby tego zadania nie jest to zbyt istotne. Zatem StartCommand:
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import org.telegram.telegrambots.meta.api.objects.Update;

/**
* Start {@link Command}.
*/
public class StartCommand implements Command {

   private final SendBotMessageService sendBotMessageService;

   public final static String START_MESSAGE = "Привет. Я Javarush Telegram Bot. Я помогу тебе быть в курсе последних " +
           "статей тех авторов, котрые тебе интересны. Я еще маленький и только учусь.";

   // Здесь не добавляем сервис через получение из Application Context.
   // Потому что если это сделать так, то будет циклическая зависимость, которая
   // ломает работу Aplikacje.
   public StartCommand(SendBotMessageService sendBotMessageService) {
       this.sendBotMessageService = sendBotMessageService;
   }

   @Override
   public void execute(Update update) {
       sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), START_MESSAGE);
   }
}
Prosimy o uważne zapoznanie się z komentarzami przed projektantem. Zależność cykliczna ( zależność cykliczna ) może wystąpić z powodu nieprawidłowej architektury. W naszym przypadku zadbamy o to, aby wszystko działało i było poprawnie. Rzeczywisty obiekt z Kontekstu Aplikacji zostanie dodany podczas tworzenia polecenia już w CommandContainerze. Polecenie zatrzymania:
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import org.telegram.telegrambots.meta.api.objects.Update;

/**
* Stop {@link Command}.
*/
public class StopCommand implements Command {

   private final SendBotMessageService sendBotMessageService;

   public static final String STOP_MESSAGE = "Деактивировал все ваши подписки \uD83D\uDE1F.";

   public StopCommand(SendBotMessageService sendBotMessageService) {
       this.sendBotMessageService = sendBotMessageService;
   }

   @Override
   public void execute(Update update) {
       sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), STOP_MESSAGE);
   }
}
I Nieznane polecenie. Dlaczego tego potrzebujemy? Dla nas jest to ważne polecenie, które odpowie, gdybyśmy nie mogli znaleźć polecenia, które zostało nam wydane. Będziemy także potrzebować NoCommand i HelpCommand.
  • NoCommand - będzie odpowiedzialny za sytuację, gdy wiadomość w ogóle nie zaczyna się od polecenia;
  • HelpCommand będzie przewodnikiem dla użytkownika, swego rodzaju dokumentacją.
Dodajmy HelpCommand:
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import org.telegram.telegrambots.meta.api.objects.Update;

import static com.github.javarushcommunity.jrtb.command.CommandName.*;

/**
* Help {@link Command}.
*/
public class HelpCommand implements Command {

   private final SendBotMessageService sendBotMessageService;

   public static final String HELP_MESSAGE = String.format("✨<b>Дотупные команды</b>✨\n\n"

                   + "<b>Начать\\закончить работу с ботом</b>\n"
                   + "%s - начать работу со мной\n"
                   + "%s - приостановить работу со мной\n\n"
                   + "%s - получить помощь в работе со мной\n",
           START.getCommandName(), STOP.getCommandName(), HELP.getCommandName());

   public HelpCommand(SendBotMessageService sendBotMessageService) {
       this.sendBotMessageService = sendBotMessageService;
   }

   @Override
   public void execute(Update update) {
       sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), HELP_MESSAGE);
   }
}
Brak polecenia:
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import org.telegram.telegrambots.meta.api.objects.Update;

/**
* No {@link Command}.
*/
public class NoCommand implements Command {

   private final SendBotMessageService sendBotMessageService;

   public static final String NO_MESSAGE = "Я поддерживаю команды, начинающиеся со слеша(/).\n"
           + "Coбы посмотреть список команд введите /help";

   public NoCommand(SendBotMessageService sendBotMessageService) {
       this.sendBotMessageService = sendBotMessageService;
   }

   @Override
   public void execute(Update update) {
       sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), NO_MESSAGE);
   }
}
Do tego zadania jest jeszcze UnknownCommand:
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import org.telegram.telegrambots.meta.api.objects.Update;

/**
* Unknown {@link Command}.
*/
public class UnknownCommand implements Command {

   public static final String UNKNOWN_MESSAGE = "Не понимаю вас \uD83D\uDE1F, напишите /help чтобы узнать что я понимаю.";

   private final SendBotMessageService sendBotMessageService;

   public UnknownCommand(SendBotMessageService sendBotMessageService) {
       this.sendBotMessageService = sendBotMessageService;
   }

   @Override
   public void execute(Update update) {
       sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), UNKNOWN_MESSAGE);
   }
}
Następnie dodajmy kontener na nasze polecenia. Będzie przechowywać nasze obiekty poleceń i na żądanie oczekujemy otrzymania wymaganego polecenia. Nazwijmy to CommandContainer :
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import com.google.common.collect.ImmutableMap;

import static com.github.javarushcommunity.jrtb.command.CommandName.*;

/**
* Container of the {@link Command}s, which are using for handling telegram commands.
*/
public class CommandContainer {

   private final ImmutableMap<String, Command> commandMap;
   private final Command unknownCommand;

   public CommandContainer(SendBotMessageService sendBotMessageService) {

       commandMap = ImmutableMap.<string, command="">builder()
               .put(START.getCommandName(), new StartCommand(sendBotMessageService))
               .put(STOP.getCommandName(), new StopCommand(sendBotMessageService))
               .put(HELP.getCommandName(), new HelpCommand(sendBotMessageService))
               .put(NO.getCommandName(), new NoCommand(sendBotMessageService))
               .build();

       unknownCommand = new UnknownCommand(sendBotMessageService);
   }

   public Command retrieveCommand(String commandIdentifier) {
       return commandMap.getOrDefault(commandIdentifier, unknownCommand);
   }

}
Jak widać, wszystko zostało zrobione prosto. Mamy niezmienną mapę z kluczem w postaci wartości polecenia i wartością w postaci obiektu polecenia typu Command. W konstruktorze raz wypełniamy niezmienną mapę i mamy do niej dostęp przez cały czas działania aplikacji. Główną i jedyną metodą pracy z kontenerem jest retrieveCommand(String CommandIdentifier) ​​​​. Istnieje polecenie o nazwie UnknownCommand, które odpowiada w przypadkach, gdy nie możemy znaleźć odpowiedniego polecenia. Teraz jesteśmy gotowi zaimplementować kontener w naszej klasie bota - w JavaRushTelegramBot: Tak wygląda teraz nasza klasa bota:
package com.github.javarushcommunity.jrtb.bot;

import com.github.javarushcommunity.jrtb.command.CommandContainer;
import com.github.javarushcommunity.jrtb.service.SendBotMessageServiceImpl;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
import org.telegram.telegrambots.meta.api.objects.Update;

import static com.github.javarushcommunity.jrtb.command.CommandName.NO;

/**
* Telegram bot for Javarush Community from Javarush community.
*/
@Component
public class JavarushTelegramBot extends TelegramLongPollingBot {

   public static String COMMAND_PREFIX = "/";

   @Value("${bot.username}")
   private String username;

   @Value("${bot.token}")
   private String token;

   private final CommandContainer commandContainer;

   public JavarushTelegramBot() {
       this.commandContainer = new CommandContainer(new SendBotMessageServiceImpl(this));
   }

   @Override
   public void onUpdateReceived(Update update) {
       if (update.hasMessage() && update.getMessage().hasText()) {
           String message = update.getMessage().getText().trim();
           if (message.startsWith(COMMAND_PREFIX)) {
               String commandIdentifier = message.split(" ")[0].toLowerCase();

               commandContainer.retrieveCommand(commandIdentifier).execute(update);
           } else {
               commandContainer.retrieveCommand(NO.getCommandName()).execute(update);
           }
       }
   }

   @Override
   public String getBotUsername() {
       return username;
   }

   @Override
   public String getBotToken() {
       return token;
   }
}
I tyle, zmiany w kodzie zostały zakończone. Jak mogę to sprawdzić? Musisz uruchomić bota i sprawdzić, czy wszystko działa. W tym celu aktualizuję token w application.properties, ustawiam właściwy i uruchamiam aplikację w klasie JavarushTelegramBotApplication: „Projekt Java od A do Z”: Implementacja wzorca poleceń do pracy z botem.  Część 1 - 3Teraz pozostaje sprawdzić, czy polecenia działają zgodnie z oczekiwaniami. Sprawdzam to krok po kroku:
  • Zatrzymaj polecenie;
  • Uruchom polecenie;
  • PomocPolecenie;
  • Brak polecenia;
  • Nieznane polecenie.
Oto co się stało: „Projekt Java od A do Z”: Implementacja wzorca poleceń do pracy z botem.  Część 1 - 4Bot działał dokładnie tak, jak oczekiwaliśmy. Ciąg dalszy poprzez link .

Lista wszystkich materiałów wchodzących w skład serii znajduje się na początku artykułu.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION