Всім привіт, дорогі друзі. Сьогодні реалізовуватимемо шаблон (шаблон — патерн, у нашому контексті це одне й теж) проектування Command для наших потреб. За допомогою цього шаблону ми будемо зручно та правильно працювати з обробкою команд нашого бота.
Для початку добре було б поговорити про те, що це за патерн Command. Але якщо я зроблю це, стаття буде дуже великою і громіздкою. Тому я вибрав матеріали для самостійного вивчення:
Друзі, подобається проект Javarush Telegram Bot ? Не лінуйтеся: ставте зірку . Так буде зрозуміло, що він цікавий, і його буде приємніше розвивати! |
- Це моя стаття 4-річної давнини. Писав її ще коли був джуніором, тож не судіть суворо.
- Відео дуже емоційного та інтерактивного шведа на ютубі. Дуже раджу. Розповідає шикарно, англійська мова чиста та зрозуміла. І взагалі у нього є відео про інші патерни проектування.
- У коментарях до моєї статті хтось Nullptr35 радив це відео .
Пишемо JRTB-3
Так само, як і раніше:- Обновляємо main гілку.
- На основі оновленої гілки main створюємо нову JRTB-3 .
- Реалізуємо патерн.
- Створюємо новий коміт з описом виконаної роботи.
- Створюємо пул-реквест, перевіряємо, і якщо все ок — мержемо нашу роботу.
if (message.startsWith("/start")) {
doStartCommand();
} else if(message.startsWith("/stop")) {
doStopCommand();
} else if(message.startsWith("/addUser")) {
doAddUserCommand();
}
...
else if(message.startsWith("/makeMeHappy")) {
doMakeMeHappyCommand();
}
Причому там, де три крапки, може бути ще кілька десятків команд. І як це обробляти нормально? Як підтримувати? Складно та важко. А отже, нам такий варіант не підходить. Потрібно, щоб це виглядало десь так:
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);
}
І все! І скільки б команд ми не додавали, ця ділянка коду буде незмінною. Що він робить? Перший ІФ дивиться, що повідомлення починається з префікса команди "/". Якщо це так, то відокремлюємо рядок до першого пробілу і шукаємо відповідну команду CommandContainer, як тільки знайшли її - запускаємо команду. І все ...) Якщо буде бажання і час, можна реалізувати роботу з командами спочатку відразу в одному класі, з купою умов і всього такого, а потім - за допомогою шаблону. Ви побачите різницю. Яка буде краса! Спершу створимо пакет поруч із пакетом bot, який і називатиметься command . І вже в цьому пакеті будуть усі класи, які відносяться до реалізації команди. А нам потрібен один інтерфейс для роботи з командами. Для цієї справи створимо його:
package com.github.codegymcommunity.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);
}
На цьому етапі нам не потрібно реалізовувати зворотну операцію команди, тому пропустимо цей метод (unexecute). У методі execute як аргумент приходить об'єкт Update - саме той, який приходить у наш головний метод у боті. Цей об'єкт міститиме все необхідне для обробки команди. Далі додамо enum, у якому зберігатимуться значення команд (start, stop тощо.). Навіщо нам це потрібне? Щоб ми мали лише одне джерело істини для назв команд. Створюємо його також у нашому пакеті command . Назвемо його CommandName :
package com.github.codegymcommunity.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;
}
}
Також нам потрібний сервіс, який займатиметься відправкою повідомлень через робота. І тому справи створимо поруч із пакетом command — пакет service , у якому додаватимемо всі необхідні послуги. Тут варто загострити увагу на тому, що я маю на увазі під словом сервіс у даному випадку. Якщо розглянути додаток, то часто воно ділиться на кілька шарів: шар роботи з ендпоінтами - контролери, бізнес-логіки - сервіси, і шар роботи з БД - репозиторій. Тому в нашому випадку сервіс – це клас, який здійснює якусь бізнес-логіку. Як правильно робити сервіс? Спочатку створити інтерфейс щодо нього і реалізацію. Реалізацію за допомогою інструкції `@Service` додати в Application Context нашого SpringBoot додатки, і вже за необхідності підтягувати його за допомогою інструкції `@Autowired`. Тому створюємо інтерфейс SendBotMessageService (у назві сервісів зазвичай додають наприкінці імені Service):
package com.github.codegymcommunity.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);
}
Далі створюємо його реалізацію:
package com.github.codegymcommunity.jrtb.service;
import com.github.codegymcommunity.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 codegymBot;
@Autowired
public SendBotMessageServiceImpl(JavarushTelegramBot codegymBot) {
this.codegymBot = codegymBot;
}
@Override
public void sendMessage(String chatId, String message) {
SendMessage sendMessage = new SendMessage();
sendMessage.setChatId(chatId);
sendMessage.enableHtml(true);
sendMessage.setText(message);
try {
codegymBot.execute(sendMessage);
} catch (TelegramApiException e) {
//todo add logging to the project.
e.printStackTrace();
}
}
}
Отак виглядає реалізація. Найголовніша магія знаходиться там, де створюється конструктор. За допомогою анотації @Autowired при конструкторі, SpringBoot шукатиме у себе в Application Context об'єкт цього класу. А він уже там перебуває. Виходить так робота: у нашому додатку будь-де ми можемо отримати доступ до бота і щось зробити. І ось цей сервіс відповідає за те, щоб надсилати повідомлення. Щоб ми не писали щоразу в кожному місці щось на кшталт такого:
SendMessage sendMessage = new SendMessage();
sendMessage.setChatId(chatId);
sendMessage.setText(message);
try {
codegymBot.execute(sendMessage);
} catch (TelegramApiException e) {
//todo add logging to the project.
e.printStackTrace();
}
Ми цю логіку винесли в окремий клас і за потреби будемо нею користуватися. Тепер нам потрібно реалізувати три команди: StartCommand, StopCommand та UnknownCommand. Потрібні вони для того, щоб було чим заповнювати наш контейнер для команд. Тексти поки що будуть сухі та малоінформативні, в рамках цього завдання це не дуже важливо. Отже, StartCommand:
package com.github.codegymcommunity.jrtb.command;
import com.github.codegymcommunity.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.
// Потому что если это сделать так, то будет циклическая зависимость, которая
// ломает работу програми.
public StartCommand(SendBotMessageService sendBotMessageService) {
this.sendBotMessageService = sendBotMessageService;
}
@Override
public void execute(Update update) {
sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), START_MESSAGE);
}
}
Уважно прочитайте коментарі перед конструктором. Циклічна залежність ( кругова залежність ) може статися через не зовсім правильну архітектуру. У нашому випадку ми зробимо все так, щоб усе працювало та було правильним. Реальний об'єкт із Application Context буде доданий при створенні команди вже CommandContainer. StopCommand:
package com.github.codegymcommunity.jrtb.command;
import com.github.codegymcommunity.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);
}
}
І UnknownCommand. Навіщо він нам потрібний? Для нас це важлива команда, яка відповідатиме у випадку, якщо ми не змогли знайти ту команду, яку нам передали. А ще нам потрібна буде NoCommand та HelpCommand.
- NoCommand - відповідатиме за ситуацію, коли повідомлення починається зовсім не з команди;
- HelpCommand – буде путівником для користувача, своєрідною документацією.
package com.github.codegymcommunity.jrtb.command;
import com.github.codegymcommunity.jrtb.service.SendBotMessageService;
import org.telegram.telegrambots.meta.api.objects.Update;
import static com.github.codegymcommunity.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);
}
}
NoCommand:
package com.github.codegymcommunity.jrtb.command;
import com.github.codegymcommunity.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"
+ "Щобы посмотреть список команд введите /help";
public NoCommand(SendBotMessageService sendBotMessageService) {
this.sendBotMessageService = sendBotMessageService;
}
@Override
public void execute(Update update) {
sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), NO_MESSAGE);
}
}
І для цього завдання залишився ще UnknownCommand:
package com.github.codegymcommunity.jrtb.command;
import com.github.codegymcommunity.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);
}
}
Далі додамо контейнер для наших команд. У ньому зберігатимуться об'єкти наших команд, і на запит ми очікуємо отримати необхідну команду. Назвемо його CommandContainer :
package com.github.codegymcommunity.jrtb.command;
import com.github.codegymcommunity.jrtb.service.SendBotMessageService;
import com.google.common.collect.ImmutableMap;
import static com.github.codegymcommunity.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);
}
}
Як видно, все зроблено просто. У нас є незмінна карта з ключем у вигляді значення команди та значенням у вигляді об'єкта команди типу Command. У конструкторі ми заповнюємо незмінну карту один раз і весь час додатку до неї звертаємося. Головний та єдиний метод для роботи з контейнером - retrieveCommand(String commandIdentifier) . Існує команда UnknownCommand, яка відповідає за випадки, коли ми не можемо знайти відповідну команду. Тепер ми готові впровадити контейнер у наш клас з ботом - в CodeGymTelegramBot: Ось так тепер виглядає наш клас бота:
package com.github.codegymcommunity.jrtb.bot;
import com.github.codegymcommunity.jrtb.command.CommandContainer;
import com.github.codegymcommunity.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.codegymcommunity.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;
}
}
І все, зміни у коді закінчено. Як це перевірити? Потрібно запустити робота і перевірити, що все працює. Для цього application.properties оновлюю токен, ставлю правильний і в класі JavarushTelegramBotApplication запускаю програму: Тепер потрібно перевірити, що команди працюють як потрібно. Поетапно перевіряю:
- StopCommand;
- StartCommand;
- HelpCommand;
- NoCommand;
- UnknownCommand.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ