JavaRush /จาวาบล็อก /Random-TH /ลองใช้ Command Pattern เพื่อทำงานกับบอท (ตอนที่ 1) - "โคร...
Roman Beekeeper
ระดับ

ลองใช้ Command Pattern เพื่อทำงานกับบอท (ตอนที่ 1) - "โครงการ Java จาก A ถึง Z"

เผยแพร่ในกลุ่ม
สวัสดีทุกคนเพื่อนรัก วันนี้เราจะใช้เทมเพลต (เทมเพลตคือรูปแบบ ในบริบทของเรา มันเป็นสิ่งเดียวกัน) ของการออกแบบ Command ตามความต้องการของเรา การใช้เทมเพลตนี้จะทำให้เราทำงานกับการประมวลผลคำสั่งของบอทได้อย่างสะดวกและถูกต้อง "โปรเจ็กต์ Java จาก A ถึง Z": การใช้รูปแบบคำสั่งสำหรับการทำงานกับบอท  ตอนที่ 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);
}
นั่นคือทั้งหมด! และไม่ว่าเราจะเพิ่มคำสั่งไปกี่คำสั่ง โค้ดส่วนนี้ก็ยังคงไม่เปลี่ยนแปลง เขากำลังทำอะไร? อันดับแรกถ้าต้องแน่ใจว่าข้อความขึ้นต้นด้วยคำนำหน้าคำสั่ง "/" หากเป็นกรณีนี้ เราจะเลือกบรรทัดจนถึงช่องว่างแรกและค้นหาคำสั่งที่เกี่ยวข้องใน CommandContainer ทันทีที่เราพบ เราก็รันคำสั่ง แค่นั้นเอง...) หากคุณมีความปรารถนาและเวลา คุณสามารถปรับใช้การทำงานกับทีมได้ โดยเริ่มแรกในชั้นเรียนเดียวพร้อมเงื่อนไขมากมายและทั้งหมดนั้น จากนั้นจึงใช้เทมเพลต คุณจะเห็นความแตกต่าง มันจะสวยงามขนาดไหน! ขั้นแรก เรามาสร้างแพ็คเกจถัดจากแพ็คเกจบอท ซึ่งจะเรียกว่าcommand "โปรเจ็กต์ Java จาก A ถึง Z": การใช้รูปแบบคำสั่งสำหรับการทำงานกับบอท  ส่วนที่ 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มาเป็นอาร์กิวเมนต์- เป็นอาร์กิวเมนต์ที่มาพร้อมกับวิธีการหลักของเราในบอท ออบเจ็กต์นี้จะมีทุกสิ่งที่จำเป็นในการประมวลผลคำสั่ง ต่อไป เราจะเพิ่ม enum ที่จะเก็บค่าคำสั่ง (start, stop และอื่นๆ) ทำไมเราถึงต้องการสิ่งนี้? เพื่อให้เรามีแหล่งความจริงเพียงแหล่งเดียวสำหรับชื่อทีม เรายังสร้างมันขึ้นมาใน แพ็คเกจ คำสั่ง ของเรา ด้วย ลองเรียกมันว่า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 ซึ่งเราจะเพิ่มบริการที่จำเป็นทั้งหมด ที่นี่คุ้มค่าที่จะมุ่งเน้นไปที่สิ่งที่ฉันหมายถึงโดยบริการคำในกรณีนี้ หากเราพิจารณาแอปพลิเคชัน มักจะแบ่งออกเป็นหลายเลเยอร์: เลเยอร์สำหรับการทำงานกับอุปกรณ์ปลายทาง - ตัวควบคุม, เลเยอร์ของตรรกะทางธุรกิจ - บริการ และเลเยอร์สำหรับการทำงานกับฐานข้อมูล - ที่เก็บข้อมูล ดังนั้นในกรณีของเรา บริการคือคลาสที่ใช้ตรรกะทางธุรกิจบางประเภท จะสร้างบริการอย่างถูกต้องได้อย่างไร? ขั้นแรก สร้างอินเทอร์เฟซสำหรับอินเทอร์เฟซและการใช้งาน เพิ่มการใช้งานโดยใช้คำอธิบายประกอบ `@Service` ลงในบริบทแอปพลิเคชันของแอปพลิเคชัน SpringBoot ของเรา และหากจำเป็น ให้กระชับให้รัดกุมโดยใช้คำอธิบายประกอบ `@Autowired` ดังนั้นเราจึงสร้างอินเทอร์เฟซ SendBotMessageService (ในบริการตั้งชื่อพวกเขามักจะเพิ่มบริการที่ท้ายชื่อ):
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 ใน Constructor เรากรอกแผนที่ที่ไม่เปลี่ยนรูปหนึ่งครั้งและเข้าถึงได้ตลอดการทำงานของแอปพลิเคชัน วิธีการหลักและวิธีเดียวสำหรับการทำงานกับคอนเทนเนอร์คือgetrecommand(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: "โปรเจ็กต์ Java จาก A ถึง Z": การใช้รูปแบบคำสั่งสำหรับการทำงานกับบอท  ส่วนที่ 1 - 3ตอนนี้เราต้องตรวจสอบว่าคำสั่งทำงานตามที่คาดไว้หรือไม่ ฉันตรวจสอบทีละขั้นตอน:
  • หยุดคำสั่ง;
  • เริ่มคำสั่ง;
  • คำสั่งช่วยเหลือ;
  • ไม่มีคำสั่ง;
  • คำสั่งที่ไม่รู้จัก
นี่คือสิ่งที่เกิดขึ้น: "โปรเจ็กต์ Java จาก A ถึง Z": การใช้รูปแบบคำสั่งสำหรับการทำงานกับบอท  ส่วนที่ 1 - 4บอททำงานได้ตรงตามที่เราคาดไว้ ติดตามต่อได้ตามลิงค์ครับ

รายการเนื้อหาทั้งหมดในซีรีส์นี้อยู่ที่ตอนต้นของบทความนี้

ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION