Всем привет, дорогие друзья. Сегодня будем реализовывать шаблон (шаблон — паттерн, в нашем контексте это одно и тоже) проектирования Command для наших нужд. При помощи этого шаблона мы будем удобно и правильно работать с обработкой команд нашего бота.
!["Java-проект от А до Я": Реализуем Command Pattern для работы с ботом. Часть 1 - 1]()
Для начала хорошо было бы поговорить о том, что это за паттерн — 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.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);
}
На данном этапе нам не нужно реализовывать обратную операцию команды, поэтому пропустим этот метод (unexecute).
В методе execute в качестве аргумента приходит объект Update — как раз тот, который приходит в наш главный метод в боте. Этот объект будет содержать все нужное для обработки команды.
Далее добавим enum, в котором будут храниться значения команд (start, stop и так далее). Зачем нам это нужно? Чтобы у нас был только один источник истины для названий команд. Создаем его также в нашем пакете command.
Назовем его 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;
}
}
Также нам нужен сервис, который будет заниматься отправкой сообщений через бота.
Для этого дела создадим рядом с пакетом command — пакет service, в который будем добавлять все нужные сервисы.
Здесь стоит заострить внимание на том, что я подразумеваю под словом сервис в данном случае. Если рассмотреть приложение, то зачастую оно делится на несколько слоев: слой работы с эндпоинтами — контроллеры, слой бизнес логики — сервисы, и слой работы с БД — репозиторий.
Поэтому в нашем случае сервис — это класс, который осуществляет какую-то бизнес-логику.
Как правильно создавать сервис? Вначале создать интерфейс к нему и реализацию. Реализацию при помощи аннотации `@Service` добавить в Application Context нашего SpringBoot приложения, и уже при необходимости подтягивать его при помощи аннотации `@Autowired`.
Поэтому создаем интерфейс SendBotMessageService (в именовании сервисов обычно добавляют в конце имени Service):
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);
}
Далее создаем его реализацию:
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();
}
}
}
Вот так выглядит реализация. Самая главная магия находится там, где создается конструктор. При помощи аннотации @Autowired при конструктор, SpringBoot будет искать у себя в Application Context объект этого класса. А он уже там находится.
Получается так работа: в нашем приложении в любом месте мы можем получить доступ к боту и что-то сделать. И вот этот сервис отвечает за то, чтобы отправлять сообщения. Чтобы мы не писали каждый раз в каждом месте что-то типа такого:
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();
}
Мы эту логику вынесли в отдельный класс и при необходимости будем ею пользоваться.
Теперь нам нужно реализовать три команды: StartCommand, StopCommand и UnknownCommand. Нужны они для того, чтобы нам было чем заполнять наш контейнер для команд. Тексты пока что будут сухие и малоинформативные, в рамках этой задачи это не сильно важно.
Итак, 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.
// Потому что если это сделать так, то будет циклическая зависимость, которая
// ломает работу приложения.
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.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);
}
}
И UnknownCommand. Зачем он нам нужен? Для нас это важная команда, которая будет отвечать в случае, если мы не смогли найти ту команду, которую нам передали.
А еще нам нужна будет NoCommand и HelpCommand.- NoCommand — будет отвечать за ситуацию, когда сообщение начинается вовсе не с команды;
- 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);
}
}
NoCommand:
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"
+ "Чтобы посмотреть список команд введите /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.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);
}
}
Далее добавим контейнер для наших команд. В нем будут храниться объекты наших команд, и по запросу мы ожидаем получить необходимую команду. Назовем его 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);
}
}
Как видно, сделано все просто. У нас есть неизменяемая мапа с ключом в виде значения команды и со значением в виде объекта команды типа Command. В конструкторе мы заполняем неизменяемую мапу один раз и все время работы приложения к ней обращаемся.
Главный и единственный метод для работы с контейнером — retrieveCommand(String commandIdentifier).
Есть команда UnknownCommand, которая отвечает за случаи, когда мы не можем найти соответствующую команду.
Теперь мы готовы внедрить контейнер в наш класс с ботом — в JavaRushTelegramBot:
Вот так теперь выглядит наш класс бота:
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;
}
}
И все, изменения в коде закончены.
Как это проверить?
Нужно запустить бота и проверить, что все работает.
Для этого в application.properties обновляю токен, ставлю правильный и в классе JavarushTelegramBotApplication запускаю приложение:
- StopCommand;
- StartCommand;
- HelpCommand;
- NoCommand;
- UnknownCommand.

ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ