目录
封装变动之处 (Encapsulate what varies)
萃取共同行为 (Abstract common behaviors)
委派/複合 (Delegation / Composition)
前言
大家好,我是水球潘。
今天我们要来介绍一个 GoF 软体设计模式中的一位狠角色,责任链模式 (Chain Of Responsibility Pattern),简称 CoR。责任链模式是我最喜欢的模式之一,因为他能够允许开发者持续地往一个类别中添加新的行为,使该类别得以组合各式各样的需求。
我喜欢借由「完整的实战演练」来谈论软体设计模式,而非「过度简化的案例」。如此一来才能让大家感受到设计理论与实务上之间的紧密配合。因此,我会遵照以下步骤来介绍每一个模式:
- 先上一份「需求」
- 对着需求做「面向对象分析 (OOA)」,绘制出初版类别图
- 照着初版类别图实作出初版程序码
- 察觉程序码中的 Forces
- 提出我们要解决的 Problem,寻找对应的软体设计模式
- 套用软体设计模式来解决 Problem (Forces),绘製第二版类别图
- 照着第二版类别图重构出第二版程式码
- 软体设计模式六大元素总结
你收到了一份需求
今天我们的实战演练就是在知名社群平台 Discord 上开发一款多功能的 Discord 机器人!
你要为水球软体学院开发一个 Discord 机器人 (WaterballBot)。这个机器人支援许多功能,社群成员可以在 Discord 频道中发布讯息 (Message) 来请求某一道功能。而当机器人收到讯息指令时,就会做相对应功能的处理 (Handle)。
举例来说,如果你想要看看货币汇率,你可以在频道中发布讯息 "currency",然后学院机器人就会立刻回复你汇率资讯。
目前支援以下三者讯息指令:查看机器人有哪些讯息指令可用。
- "help":查看机器人有哪些讯息指令可用。
- "currency":查看目前货币汇率。
- "dcard":查看最新 Dcard(知名论坛) 的文章。
讯息指令不区分大小写,不管你是打成大写开头的 Help 或是 全大写的 HELP,机器人全都能辨认。每一个讯息指令只会对应一个功能。如果讯息无法匹配到任何功能的话,机器人就会无视这则讯息。
再来,稍微说一下 Discord 的推播机制,由于这是第三方的技术,并不是本文的重点,各位稍微有个印象就好。我们能够简单地使用 Discord API,来让机器人连线 (Connect) 且登入至 Discord 之 中。登入后就能开始侦听 (Listen) 各种事件 (Event),而我们主要想侦听的事件是 MessageCreateEvent 也就是新讯息事件。每当有人在某频道中传讯息时,Discord 会通知机器人说哪一个文字频道 (Message Channel) 有了一则新讯息 (Message)。
满实用又满有挑战性的,对吧。
走吧!先进行面向对象分析。
面向对象分析 (OOA)
好,这就是我们分析出来的初版类别图了,灰色区块的部分为第三方的 Discord API 中的类别。我们先专注在学院领域模型中的 WaterballBot 类别:当呼叫 WaterballBot 的 connect operation 时,WaterballBot 会透过 Discord API 连线和登入,之后 WaterballBot 会向 Discord API 侦听MessageCreateEvent。每一次有新讯息 (Message) 时,Discord 会推播新讯息给 WaterballBot 并告知此讯息是来源自哪一个文字频道 (MessageChannel),此时 WaterballBot 就会处理 (handle) 这则讯息,讯息处理方法 (handle 方法) 的行为如同粉红色便条纸所示;主要有三种不同的讯息指令,各对应到不同的处理。
接着,我们来实作初版的程式码~
初版程式实作
WaterballBot 类别的实作分成两部分,第一部分是第三方套件的使用细节:connect 方法中撰写着我们如何透过 Discord API 让机器人连线、登入和侦听新讯息的事件:
public class WaterballBot {
// 透過 Discord API 讓機器人連線、登入和偵聽新訊息的事件
public void connect() throws IOException {
String token = getDiscordToken();
DiscordClient client = DiscordClient.create(token);
GatewayDiscordClient gateway = client.login().block();
gateway.on(MessageEvent.class).subscribe(e -> {
if (e instanceof MessageCreateEvent) {
MessageCreateEvent mce = (MessageCreateEvent) e;
Message message = mce.getMessage();
MessageChannel channel = message.getChannel().block();
handle(message, channel);
}
});
gateway.onDisconnect().block();
}
private static String getDiscordToken() throws IOException {
try (InputStream in = currentThread().getContextClassLoader().getResourceAsStream("token.properties")) {
Properties properties = new Properties();
properties.load(in);
return String.valueOf(properties.get("TOKEN"));
}
}
...
这部分大略阅读过去就行,直接来看第二部分今天的重点,机器人该如何做 message handling:
public void handle(Message message, MessageChannel channel) {
if ("help".equalsIgnoreCase(message.getContent())) {
channel.createMessage("▋ HELP ▋").block();
channel.createMessage("Commands: dcard, currency").block();
} else if ("dcard".equalsIgnoreCase(message.getContent())) {
String dcardBody = crawlDcardBody();
channel.createMessage("▋ DCARD ▋").block();
channel.createMessage(dcardBody).block();
} else if ("currency".equalsIgnoreCase(message.getContent())) {
String currencyBody = crawlCurrencyBody();
channel.createMessage("▋ CURRENCY ▋").block();
channel.createMessage(currencyBody).block();
}
}
private String crawlDcardBody() { /* 實作 Dcard 爬蟲 */ }
private String crawlCurrencyBody() { /* 實作匯率資訊爬蟲 */}