JavaRush /Blogue Java /Random-PT /Vamos implementar o Command Pattern para trabalhar com o ...
Roman Beekeeper
Nível 35

Vamos implementar o Command Pattern para trabalhar com o bot. (Parte 1) - "Projeto Java de A a Z"

Publicado no grupo Random-PT
Olá a todos, queridos amigos. Hoje vamos implementar um template (template é um padrão, no nosso contexto é a mesma coisa) de design de Comando para as nossas necessidades. Usando este modelo, trabalharemos de maneira conveniente e correta com o processamento dos comandos do nosso bot. "Projeto Java de A a Z": Implementando um Padrão de Comando para trabalhar com um bot.  Parte 1 - 1
Amigos, vocês gostam do projeto Javarush Telegram Bot ? Não tenha preguiça: dê uma estrela . Assim ficará claro que ele é interessante e será mais prazeroso desenvolvê-lo!
Para começar, seria bom falar sobre que tipo de padrão é esse - Comando. Mas se eu fizer isso, o artigo ficará muito grande e complicado. Portanto, escolhi materiais para autoestudo:
  1. Este é o meu artigo de 4 anos atrás. Eu escrevi quando era júnior, então não julgue com muita severidade.
  2. Vídeo de um sueco muito emocionante e interativo no YouTube. Eu recomendo. Ele fala lindamente, seu inglês é claro e compreensível. E, em geral, ele tem um vídeo sobre outros padrões de design.
  3. Nos comentários do meu artigo, alguém Nullptr35 recomendou este vídeo .
Isso deve ser suficiente para mergulhar no assunto e estar na mesma página que eu. Bem, aqueles que estão familiarizados com esse padrão de design podem pular com segurança e seguir em frente.

Nós escrevemos JRTB-3

Tudo está como antes:
  1. Atualizamos o branch principal.
  2. Com base no branch principal atualizado, criamos um novo JRTB-3 .
  3. Vamos implementar o padrão.
  4. Criamos um novo commit descrevendo o trabalho realizado.
  5. Criamos um pull request, verificamos e se estiver tudo bem, mesclamos nosso trabalho.
Não vou mostrar os pontos 1-2: eu os descrevi com muito cuidado em artigos anteriores, então vamos prosseguir direto para a implementação do modelo. Por que este modelo é adequado para nós? Sim, porque toda vez que executarmos um comando, iremos para o método onUpdateReceived(Update update) e dependendo do comando executaremos uma lógica diferente. Sem esse padrão, teríamos uma série de instruções if-else if. Algo assim:
if (message.startsWith("/start")) {
   doStartCommand();
} else if(message.startsWith("/stop")) {
   doStopCommand();
} else if(message.startsWith("/addUser")) {
   doAddUserCommand();
}
...
else if(message.startsWith("/makeMeHappy")) {
   doMakeMeHappyCommand();
}
Além disso, onde há reticências, pode haver várias dezenas de equipes a mais. E como lidar com isso normalmente? Como apoiar? Difícil e difícil. Isso significa que esta opção não nos convém. Deve ser algo assim:
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);
}
Isso é tudo! E não importa quantos comandos adicionemos, esta seção do código permanecerá inalterada. O que ele está fazendo? O primeiro if garante que a mensagem comece com o prefixo de comando "/". Se for esse o caso, então selecionamos a linha até o primeiro espaço e procuramos o comando correspondente no CommandContainer; assim que o encontrarmos, executamos o comando. E isso é tudo...) Se você tiver vontade e tempo, você pode implementar o trabalho em equipe, primeiro em uma aula de uma vez, com um monte de condições e tudo mais, e depois usando um template. Você verá a diferença. Que beleza será! Primeiro, vamos criar um pacote próximo ao pacote do bot, que será chamado de command . "Projeto Java de A a Z": Implementando um Padrão de Comando para trabalhar com um bot.  Parte 1 - 2E já neste pacote estarão todas as classes relacionadas à implementação do comando. Precisamos de uma interface para trabalhar com comandos. Para este caso, vamos criá-lo:
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);
}
Neste ponto, não precisamos implementar a operação reversa do comando, então pularemos este método (desexecutar). No método execute, o objeto Update vem como argumento - exatamente aquele que vem para o nosso método principal no bot. Este objeto conterá tudo o que é necessário para processar o comando. A seguir, adicionaremos um enum que armazenará os valores do comando (iniciar, parar e assim por diante). Por que nós precisamos disso? Para que tenhamos apenas uma fonte de verdade para os nomes dos times. Também o criamos em nosso pacote de comandos . Vamos chamá-lo de 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;
   }

}
Também precisamos de um serviço que envie mensagens por meio de um bot. Para fazer isso, criaremos um pacote de serviços próximo ao comando package , ao qual adicionaremos todos os serviços necessários. Aqui vale a pena focar no que quero dizer com a palavra serviço neste caso. Se considerarmos um aplicativo, ele geralmente é dividido em várias camadas: uma camada para trabalhar com endpoints - controladores, uma camada de lógica de negócios - serviços e uma camada para trabalhar com o banco de dados - um repositório. Portanto, no nosso caso, um serviço é uma classe que implementa algum tipo de lógica de negócio. Como criar um serviço corretamente? Primeiro, crie uma interface para isso e uma implementação. Adicione a implementação usando a anotação `@Service` ao contexto do aplicativo de nosso aplicativo SpringBoot e, se necessário, aperte-a usando a anotação `@Autowired`. Portanto, criamos a interface SendBotMessageService (na nomenclatura de serviços geralmente adicionam Service no final do nome):
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);
}
A seguir, criamos sua implementação:
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();
       }
   }
}
É assim que se parece a implementação. A magia mais importante é onde o designer é criado. Usando a anotação @Autowired no construtor, o SpringBoot procurará um objeto desta classe em seu Contexto de Aplicação. E ele já está lá. Funciona assim: na nossa aplicação, em qualquer lugar podemos acessar o bot e fazer alguma coisa. E este serviço é responsável pelo envio de mensagens. Para que não escrevamos algo assim sempre e em todos os lugares:
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();
}
Movemos essa lógica para uma classe separada e a usaremos se necessário. Agora precisamos implementar três comandos: StartCommand, StopCommand e UnknownCommand. Precisamos deles para termos algo para preencher nosso contêiner de comandos. Por enquanto, os textos serão áridos e pouco informativos; para os propósitos desta tarefa, isso não é muito importante. Então, 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);
   }
}
Por favor, leia os comentários com atenção antes do designer. A dependência circular ( circular dependency ) pode ocorrer devido a uma arquitetura que não está muito correta. No nosso caso, garantiremos que tudo funcione e esteja correto. O objeto real do Contexto da Aplicação será adicionado ao criar o comando já no CommandContainer. Comando Parar:
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);
   }
}
E comando desconhecido. Por que precisamos disso? Para nós, este é um comando importante que responderá caso não consigamos encontrar o comando que nos foi dado. Também precisaremos de NoCommand e HelpCommand.
  • NoCommand - será responsável pela situação em que a mensagem não comece com nenhum comando;
  • HelpCommand será um guia para o usuário, uma espécie de documentação.
Vamos adicionar 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);
   }
}
Nenhum comando:
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);
   }
}
E para esta tarefa ainda existe o 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);
   }
}
A seguir, vamos adicionar um contêiner para nossos comandos. Ele armazenará nossos objetos de comando e, mediante solicitação, esperamos receber o comando necessário. Vamos chamá-lo de 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);
   }

}
Como você pode ver, tudo foi feito de forma simples. Temos um mapa imutável com uma chave na forma de um valor de comando e um valor na forma de um objeto de comando do tipo Command. No construtor, preenchemos o mapa imutável uma vez e o acessamos durante toda a operação da aplicação. O principal e único método para trabalhar com o contêiner é retrieveCommand(String commandIdentifier) ​​​​. Existe um comando chamado UnknownCommand, que é responsável pelos casos em que não conseguimos encontrar o comando correspondente. Agora estamos prontos para implementar o contêiner em nossa classe de bot - em JavaRushTelegramBot: Esta é a aparência de nossa classe de bot agora:
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;
   }
}
E pronto, as alterações no código estão concluídas. Como posso verificar isso? Você precisa iniciar o bot e verificar se tudo funciona. Para fazer isso, atualizo o token em application.properties, defino o correto e inicio o aplicativo na classe JavarushTelegramBotApplication: "Projeto Java de A a Z": Implementando um Padrão de Comando para trabalhar com um bot.  Parte 1 - 3Agora precisamos verificar se os comandos funcionam conforme o esperado. Eu verifico passo a passo:
  • PararComando;
  • IniciarComando;
  • Comando de ajuda;
  • Nenhum comando;
  • Comando desconhecido.
Eis o que aconteceu: "Projeto Java de A a Z": Implementando um Padrão de Comando para trabalhar com um bot.  Parte 1 - 4o bot funcionou exatamente como esperávamos. Continuação através do link .

Uma lista de todos os materiais da série está no início deste artigo.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION