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.
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:
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ć! |
- To jest mój artykuł sprzed 4 lat. Napisałem to, gdy byłem młodszy, więc nie oceniaj tego zbyt surowo.
- 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.
- W komentarzach do mojego artykułu ktoś Nullptr35 polecił ten film .
Piszemy JRTB-3
Wszystko jest takie samo jak wcześniej:- Aktualizujemy główną gałąź.
- W oparciu o zaktualizowaną gałąź główną tworzymy nowy JRTB-3 .
- Zaimplementujmy wzór.
- Tworzymy nowy commit opisujący wykonaną pracę.
- Tworzymy pull request, sprawdzamy go i jeśli wszystko jest w porządku, łączymy naszą pracę.
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 . I 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ą.
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: Teraz pozostaje sprawdzić, czy polecenia działają zgodnie z oczekiwaniami. Sprawdzam to krok po kroku:
- Zatrzymaj polecenie;
- Uruchom polecenie;
- PomocPolecenie;
- Brak polecenia;
- Nieznane polecenie.
GO TO FULL VERSION