JavaRush /Java Blog /Random-KO /봇과 함께 작동하도록 명령 패턴을 구현해 보겠습니다. (1부) - "Java 프로젝트 A부터 Z까지"
Roman Beekeeper
레벨 35

봇과 함께 작동하도록 명령 패턴을 구현해 보겠습니다. (1부) - "Java 프로젝트 A부터 Z까지"

Random-KO 그룹에 게시되었습니다
안녕하세요 여러분, 친애하는 친구 여러분. 오늘 우리는 우리의 필요에 맞는 Command 디자인의 템플릿(템플릿은 패턴이고 우리의 맥락에서는 같은 것입니다)을 구현해 보겠습니다. 이 템플릿을 사용하면 봇 명령 처리를 편리하고 정확하게 수행할 수 있습니다. "A부터 Z까지의 Java 프로젝트": 봇 작업을 위한 명령 패턴 구현.  파트 1 - 1
친구 여러분, Javarush Telegram Bot 프로젝트가 마음에 드시나요 ? 게으르지 마세요. 별점을 주세요 . 이렇게 하면 그가 흥미롭다는 것이 분명해지고 그를 발전시키는 것이 더 즐거울 것입니다!
우선, 이것이 어떤 패턴인지 이야기하는 것이 좋을 것입니다 - Command. 하지만 이렇게 하면 글이 너무 방대하고 번거로워질 것입니다. 그래서 저는 자율 학습 자료를 선택했습니다.
  1. 4년 전 제 글 입니다 . 제가 후배 때 쓴 글이니 너무 가혹하게 판단하지 마세요.
  2. 매우 감성적이고 상호작용이 활발한 스웨덴의 YouTube 동영상입니다. 나는 그것을 강력히 추천합니다. 그는 아름답게 말하며, 그의 영어는 명확하고 이해하기 쉽습니다. 그리고 일반적으로 그는 다른 디자인 패턴에 대한 비디오를 가지고 있습니다.
  3. 내 기사의 댓글에서 Nullptr35 가 이 동영상을 추천했습니다 .
이것은 주제에 몰입하고 나와 같은 생각을 하는 데 충분할 것입니다. 글쎄, 이 디자인 패턴에 익숙한 사람들은 건너뛰고 계속 진행할 수 있습니다.

JRTB-3을 쓴다

모든 것이 이전과 동일합니다.
  1. 우리는 메인 브랜치를 업데이트합니다.
  2. 업데이트된 메인 브랜치를 기반으로 새로운 JRTB-3을 생성합니다 .
  3. 패턴을 구현해보자.
  4. 완료된 작업을 설명하는 새로운 커밋을 만듭니다.
  5. 풀 요청을 생성하고 확인한 후 모든 것이 정상이면 작업을 병합합니다.
1~2번 항목은 보여드리지 않겠습니다. 이전 글에서 아주 자세하게 설명했으므로 바로 템플릿 구현을 진행하겠습니다. 이 템플릿이 우리에게 적합한 이유는 무엇입니까? 예, 명령을 실행할 때마다 onUpdateReceived(Update update) 메서드 로 이동 하고 명령에 따라 다른 논리를 실행하기 때문입니다. 이 패턴이 없다면 우리는 수많은 if-else if 문을 가지게 될 것입니다. 이 같은:
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);
}
그게 다야! 그리고 얼마나 많은 명령을 추가하더라도 이 코드 섹션은 변경되지 않습니다. 그는 무엇을 하고 있나요? 첫 번째 if는 메시지가 명령 접두사 "/"로 시작하는지 확인합니다. 이 경우 첫 번째 공백까지 줄을 선택하고 CommandContainer에서 해당 명령을 찾습니다. 발견하자마자 명령을 실행합니다. 그게 전부입니다...) 원하는 바와 시간이 있다면 먼저 한 수업에서 여러 가지 조건과 모든 것을 갖춘 다음 템플릿을 사용하여 팀 작업을 구현할 수 있습니다. 당신은 차이점을 볼 수 있습니다. 얼마나 아름다울까요! 먼저 봇 패키지 옆에 command 라는 패키지를 생성해 보겠습니다 . "A부터 Z까지의 Java 프로젝트": 봇 작업을 위한 명령 패턴 구현.  파트 1 - 2그리고 이미 이 패키지에는 명령 구현과 관련된 모든 클래스가 있습니다. 명령 작업을 위해서는 하나의 인터페이스가 필요합니다. 이 경우에는 다음을 생성해 보겠습니다.
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);
}
이 시점에서는 명령의 역방향 작업을 구현할 필요가 없으므로 이 메서드(실행 취소)를 건너뜁니다. 실행 메소드에서 Update 객체는 인수로 제공됩니다 . 이는 봇의 기본 메소드에 제공되는 것과 정확히 같습니다. 이 개체에는 명령을 처리하는 데 필요한 모든 것이 포함됩니다. 다음으로 명령 값(시작, 중지 등)을 저장할 열거형을 추가하겠습니다. 왜 이것이 필요합니까? 따라서 팀 이름에 대한 정보 소스는 단 하나뿐입니다. 또한 명령 패키지 에서도 이를 생성합니다 . 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;
   }

}
또한 봇을 통해 메시지를 보내는 서비스도 필요합니다. 이를 위해 package 명령 옆에 서비스 패키지를 생성 하고 여기에 필요한 모든 서비스를 추가합니다. 이 경우 서비스라는 단어가 의미하는 바에 초점을 맞출 가치가 있습니다. 애플리케이션을 고려하면 엔드포인트 작업을 위한 레이어(컨트롤러), 비즈니스 로직 레이어(서비스), 데이터베이스 작업을 위한 레이어(저장소) 등 여러 레이어로 나누어지는 경우가 많습니다. 따라서 우리의 경우 서비스는 일종의 비즈니스 로직을 구현하는 클래스입니다. 서비스를 올바르게 만드는 방법은 무엇입니까? 먼저 이에 대한 인터페이스와 구현을 만듭니다. SpringBoot 애플리케이션의 애플리케이션 컨텍스트에 `@Service` 주석을 사용하여 구현을 추가하고, 필요한 경우 `@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는 애플리케이션 컨텍스트에서 이 클래스의 객체를 찾습니다. 그리고 그는 이미 거기에 있습니다. 이는 다음과 같이 작동합니다. 애플리케이션에서 어디에서나 봇에 액세스하여 작업을 수행할 수 있습니다. 그리고 이 서비스는 메시지 전송을 담당합니다. 그래서 우리는 언제 어디서나 다음과 같은 글을 쓰지 않습니다.
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.
   // Потому что если это сделать так, то будет циклическая зависимость, которая
   // ломает работу applications.
   public StartCommand(SendBotMessageService sendBotMessageService) {
       this.sendBotMessageService = sendBotMessageService;
   }

   @Override
   public void execute(Update update) {
       sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), START_MESSAGE);
   }
}
디자이너님의 의견을 잘 읽어보시기 바랍니다. 순환 종속성( 순환 종속성 )은 올바르지 않은 아키텍처로 인해 발생할 수 있습니다. 우리의 경우 모든 것이 작동하고 올바른지 확인합니다. CommandContainer에 이미 명령을 생성할 때 응용 프로그램 컨텍스트의 실제 개체가 추가됩니다. 중지명령:
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);
   }
}
그리고 알 수 없는 명령. 왜 필요한가요? 우리에게 이것은 주어진 명령을 찾을 수 없을 때 응답할 중요한 명령입니다. NoCommand와 HelpCommand도 필요합니다.
  • NoCommand - 메시지가 명령으로 전혀 시작되지 않는 상황을 담당합니다.
  • HelpCommand는 일종의 문서로서 사용자를 위한 가이드가 될 것입니다.
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);
   }
}
명령 없음:
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);
   }
}
그리고 이 작업에는 여전히 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 클래스에서 애플리케이션을 시작합니다. "A부터 Z까지의 Java 프로젝트": 봇 작업을 위한 명령 패턴 구현.  파트 1 - 3이제 명령이 예상대로 작동하는지 확인해야 합니다. 단계별로 확인합니다.
  • 정지명령;
  • 시작명령;
  • 도움말명령;
  • 명령 없음;
  • 알 수 없는 명령.
일어난 일은 다음과 같습니다. "A부터 Z까지의 Java 프로젝트": 봇 작업을 위한 명령 패턴 구현.  파트 1 - 4봇은 우리가 예상한 대로 정확히 작동했습니다. 링크를 통해 계속됩니다 .

시리즈의 모든 자료 목록은 이 기사의 시작 부분에 있습니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION