JavaRush /Java Blog /Random EN /Let's implement the Command Pattern to work with the bot....

Let's implement the Command Pattern to work with the bot. (Part 1) - "Java project from A to Z"

Published in the Random EN group
Hello everyone, dear friends. Today we will implement a template (template is a pattern, in our context it is the same thing) of Command design for our needs. Using this template, we will conveniently and correctly work with the processing of our bot’s commands. "Java project from A to Z": Implementing a Command Pattern for working with a bot.  Part 1 - 1
Friends, do you like the Javarush Telegram Bot project ? Don't be lazy: give it a star . This way it will be clear that he is interesting, and it will be more pleasant to develop him!
To begin with, it would be good to talk about what kind of pattern this is - Command. But if I do this, the article will be very large and cumbersome. Therefore, I chose materials for self-study:
  1. This is my article from 4 years ago. I wrote it when I was a junior, so don’t judge it too harshly.
  2. Video of a very emotional and interactive Swede on YouTube. I highly recommend it. He speaks beautifully, his English is clear and understandable. And in general, he has a video about other design patterns.
  3. In the comments to my article, someone Nullptr35 recommended this video .
This should be enough to immerse yourself in the topic and be on the same page as me. Well, those who are familiar with this design pattern can safely skip and move on.

We write JRTB-3

Everything is the same as before:
  1. We update the main branch.
  2. Based on the updated main branch, we create a new JRTB-3 .
  3. Let's implement the pattern.
  4. We create a new commit describing the work done.
  5. We create a pull request, check it, and if everything is ok, we merge our work.
I won’t show points 1-2: I described them very carefully in previous articles, so let’s proceed straight to implementing the template. Why is this template suitable for us? Yes, because every time we execute a command, we will go to the onUpdateReceived(Update update) method , and depending on the command we will execute different logic. Without this pattern, we would have a whole host of if-else if statements. Something like this:
if (message.startsWith("/start")) {
   doStartCommand();
} else if(message.startsWith("/stop")) {
   doStopCommand();
} else if(message.startsWith("/addUser")) {
   doAddUserCommand();
}
...
else if(message.startsWith("/makeMeHappy")) {
   doMakeMeHappyCommand();
}
Moreover, where there is an ellipsis, there may be several dozen more teams. And how to handle this normally? How to support? Difficult and difficult. This means that this option does not suit us. It should look something like this:
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);
}
That's all! And no matter how many commands we add, this section of code will remain unchanged. What is he doing? The first if makes sure that the message begins with the command prefix "/". If this is the case, then we select the line up to the first space and look for the corresponding command in the CommandContainer; as soon as we find it, we run the command. And that’s all...) If you have the desire and time, you can implement working with teams, first in one class at once, with a bunch of conditions and all that, and then using a template. You will see the difference. What beauty it will be! First, let's create a package next to the bot package, which will be called command . "Java project from A to Z": Implementing a Command Pattern for working with a bot.  Part 1 - 2And already in this package there will be all the classes that relate to the implementation of the command. We need one interface for working with commands. For this case, let's create it:
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);
}
At this point, we don't need to implement the command's reverse operation, so we'll skip this method (unexecute). In the execute method, the Update object comes as an argument - exactly the one that comes to our main method in the bot. This object will contain everything needed to process the command. Next, we’ll add an enum that will store the command values ​​(start, stop, and so on). Why do we need this? So that we have only one source of truth for team names. We also create it in our command package . Let's call it 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;
   }

}
We also need a service that will send messages through a bot. To do this, we will create a service package next to the command package , to which we will add all the necessary services. Here it is worth focusing on what I mean by the word service in this case. If we consider an application, it is often divided into several layers: a layer for working with endpoints - controllers, a layer of business logic - services, and a layer for working with the database - a repository. Therefore, in our case, a service is a class that implements some kind of business logic. How to create a service correctly? First, create an interface for it and an implementation. Add the implementation using the `@Service` annotation to the Application Context of our SpringBoot application, and, if necessary, tighten it using the `@Autowired` annotation. Therefore, we create the SendBotMessageService interface (in naming services they usually add Service at the end of the name):
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);
}
Next, we create its implementation:
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();
       }
   }
}
This is what the implementation looks like. The most important magic is where the designer is created. Using the @Autowired annotation on the constructor, SpringBoot will look for an object of this class in its Application Context. And he is already there. It works like this: in our application, anywhere we can access the bot and do something. And this service is responsible for sending messages. So that we don’t write something like this every time in every place:
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();
}
We have moved this logic into a separate class and will use it if necessary. Now we need to implement three commands: StartCommand, StopCommand and UnknownCommand. We need them so that we have something to fill our container for commands. For now, the texts will be dry and uninformative; for the purposes of this task, this is not very important. So, 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.
   // Потому что если это сделать так, то будет циклическая зависимость, которая
   // ломает работу applications.
   public StartCommand(SendBotMessageService sendBotMessageService) {
       this.sendBotMessageService = sendBotMessageService;
   }

   @Override
   public void execute(Update update) {
       sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), START_MESSAGE);
   }
}
Please read the comments carefully before the designer. Circular dependency ( circular dependency ) can occur due to an architecture that is not quite right. In our case, we will make sure that everything works and is correct. The real object from the Application Context will be added when creating the command already in the 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);
   }
}
And UnknownCommand. Why do we need it? For us, this is an important command that will respond if we could not find the command that was given to us. We will also need NoCommand and HelpCommand.
  • NoCommand - will be responsible for the situation when the message does not begin with a command at all;
  • HelpCommand will be a guide for the user, a kind of documentation.
Let's add 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"
           + "Whatбы посмотреть список команд введите /help";

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

   @Override
   public void execute(Update update) {
       sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), NO_MESSAGE);
   }
}
And for this task there is still 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);
   }
}
Next, let's add a container for our commands. It will store our command objects and upon request we expect to receive the required command. Let's call it 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);
   }

}
As you can see, everything was done simply. We have an immutable map with a key in the form of a command value and a value in the form of a command object of type Command. In the constructor, we fill out the immutable map once and access it throughout the application’s operation. The main and only method for working with the container is retrieveCommand(String commandIdentifier) ​​. There is a command called UnknownCommand, which is responsible for cases when we cannot find the corresponding command. Now we are ready to implement the container into our bot class - in JavaRushTelegramBot: This is what our bot class now looks like:
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;
   }
}
And that's it, the changes to the code are completed. How can I check this? You need to launch the bot and check that everything works. To do this, I update the token in application.properties, set the correct one, and launch the application in the JavarushTelegramBotApplication class: "Java project from A to Z": Implementing a Command Pattern for working with a bot.  Part 1 - 3Now we need to check that the commands work as expected. I check it step by step:
  • StopCommand;
  • StartCommand;
  • HelpCommand;
  • NoCommand;
  • UnknownCommand.
Here's what happened: "Java project from A to Z": Implementing a Command Pattern for working with a bot.  Part 1 - 4The bot worked exactly as we expected. Continued via link .

A list of all materials in the series is at the beginning of this article.

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