JavaRush /Java Blog /Random EN /Let's implement the Command Pattern for working with the ...

Let's implement the Command Pattern for working 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 (a template is a pattern, in our context this is the same thing) of Command design for our needs. Using this template, we will conveniently and correctly work with the processing of commands from our bot. "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: put a star . So it will be clear that it is interesting, and it will be more pleasant to develop it!
To begin with, it would be good to talk about what kind of pattern it is - Command. But if I do this, the article will be very large and unwieldy. Therefore, I chose materials for self-study:
  1. This is my post from 4 years ago. I wrote it when I was a junior, so do not judge strictly.
  2. Video of a very emotional and interactive Swede on YouTube. I highly recommend. He speaks chic, English speech is clear and understandable. And in general, he has videos 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 wavelength with me. Well, those who are familiar with this design pattern can safely skip and move on.

Writing JRTB-3

Everything is the same as before:
  1. Update 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 with a description of the work done.
  5. We create a pull request, check it, and if everything is ok, we merge our work.
I will not show points 1-2: I described them very carefully in previous articles, so let's proceed immediately to the implementation of the template. Why is this template suitable for us? Yes, because every time we execute some 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 bunch 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();
}
And where there are three dots, there may be several dozen more commands. And how to process it normally? How to support? Difficult and hard. So this is not an option for us. It needs to 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);
}
And that's it! And no matter how many commands we add, this piece of code will remain unchanged. What is he doing? The first if makes sure that the message starts with the command prefix "/". If this is the case, then we isolate the string up to the first space and look for the corresponding command from the CommandContainer, as soon as we find it, we launch the command. And that's all ...) If there is a desire and time, you can implement work with teams at first immediately in one class, with a bunch of conditions and all that, and then using a template. You will see the difference. What a beauty! 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 to work with commands. For this case, let's create it:
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);
}
At this stage, we do not need to implement the reverse operation of the command, so we will skip this method (unexecute). In the execute method, the Update object comes as an argument - just the one that comes to our main method in the bot. This object will contain everything needed to process the command. Next, let's add an enum that will store command values ​​(start, stop, and so on). Why do we need it? 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.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;
   }

}
We also need a service that will send messages through the bot. To do this, let's 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 the application, then it is often divided into several layers: the layer of work with endpoints - controllers, the layer of business logic - services, and the layer of work with the database - the repository. Therefore, in our case, a service is a class that implements some kind of business logic. How to create a service? First, create an interface to it and an implementation. Add the implementation using the `@Service` annotation to the Application Context of our SpringBoot application, and, if necessary, pull it up using the `@Autowired` annotation. Therefore, we create the SendBotMessageService interface (in the naming of services, they usually add it at the end of the Service name):
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);
}
Next, we create its implementation:
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();
       }
   }
}
This is what the implementation looks like. The most important magic is where the constructor 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 do not write every time in every place something like this:
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();
}
We 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; within the framework of this task, this is not very important. So 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.
   // Потому что если это сделать так, то будет циклическая зависимость, которая
   // ломает работу applications.
   public StartCommand(SendBotMessageService sendBotMessageService) {
       this.sendBotMessageService = sendBotMessageService;
   }

   @Override
   public void execute(Update update) {
       sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), START_MESSAGE);
   }
}
Read the comments before the constructor carefully. A circular dependency ( circular dependency ) can happen due to not quite correct architecture. In our case, we will do everything so that everything works and is correct. The real object from the Application Context will be added when the command is created already in the 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);
   }
}
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.
Add 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"
           + "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 an 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);
   }
}
Next, let's add a container for our commands. It will store the objects of our commands, and upon request, we expect to receive the necessary command. Let's call it 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);
   }

}
As you can see, everything is done simply. We have an immutable map with a key in the form of a command value and with a value in the form of a command object of type Command. In the constructor, we fill the immutable map once and refer to it all the time the application is running. The main and only method for working with a container is retrieveCommand(String commandIdentifier) ​​. There is a command UnknownCommand which is responsible for cases when we cannot find the corresponding command. Now we are ready to inject the container into our bot class - in CodeGymTelegramBot: This is how our bot class looks now:
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;
   }
}
And that's it, the changes in the code are finished. How to check it? You need to start the bot and check that everything works. To do this, I update the token in application.properties, set it to 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 they should. Step by step check:
  • 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 at the link ."Java project from A to Z": Implementing a Command Pattern for working with a bot.  Part 1 - 5

List of all materials in the series at the beginning of this article.

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