Инкапсуляция состояний в объекты уменьшит сложность вашего чат-бота и улучшит его тестируемость.
Я хотел бы продемонстрировать, как повысить удобство обслуживания ваших чат-ботов, если вы решили реализовать своего чат-бота как машину состояний.
Мы будем работать с очень маленьким примером — всего 3 состояния. Какие бы входные данные ни получил чат-бот, он сначала спросит, хотите ли вы поговорить с оператором или посмотреть демонстрацию. Если вы отправили боту строку «demo», он запустит демо. Он вызывает «Поговорить с оператором» в любом другом случае.
возиться с if/else
Когда мы получили первый заказ на создание чат-бота, наш клиент попросил нас реализовать очень простой чат-бот. Мы решили начать программировать напрямую и наблюдать, какие закономерности будут появляться. Вся логика принятия решений была реализована с помощью простого if/else. Как вы понимаете, код начал бардак раньше, чем мы ожидали. Был еще один побочный эффект — условия стали излишне сложными. Посмотрите, как выглядел код до рефакторинга:
async function onMessageReceived(webhookData) { if(webhookData === 'demo' && this.lastState === states.OPERATOR_OR_DEMO ){ return await chatbot.startDemo(webhookData) } if(this.lastState === states.OPERATOR_OR_DEMO){ return await chatbot.talkToOperator(webhookData) } if(...) return await chatbot.doSomethingElse() if(...) return await chatbot.doSomethingElse2() // more ifs ... }
Мы чувствовали, что должны инкапсулировать состояния. Для следующего чтения особенно запомните условия, содержащие this.lastState === states.OPERATOR_OR_DEMO
.
Укрощение государственной машины
Я считаю, что если вы начнете думать о том, как улучшить дизайн своего чат-бота, первое, что придет вам в голову, это Шаблон состояния.
Для наших целей Chatbot будет представлять контекст. Штаты, ну, они будут штатами. ;) Или другими словами, класс State — это ситуация, в которой может оказаться чат-бот.
Как это работает? По сути, каждый раз, когда вы получаете сообщение, вы позволяете чат-боту решить, какое состояние выполнять дальше.
async function onMessageReceived(webhookData) {
await chatbot.runNextState(webhookData)
}
Каждое состояние будет хранить ссылки на состояния, в которые оно может перейти. Вот как родительское состояние выглядит в коде:
class State { constructor() { this.nextStates = [] } execute(webhookData) { throw new Error('It must be implemented in a child state.') } async findNextState(webhookData) { for (const {state, condition} of this.nextStates) { const canUseState = await condition(webhookData) if (canUseState) return state } } addNextState(nextState, condition) { this.nextStates.push({ state: nextState, condition }) } }
Реализации конкретных состояний определяют, в какие состояния может перейти чат-бот из текущего состояния. В state.execute()
состояние определяет, что оно будет делать при выполнении. Он может делать несколько вещей одновременно. Например, он может получать данные от третьей стороны, печатать сообщение с кнопками и отправлять уведомления о наборе текста за один раз.
Ниже приведена реализация дочернего состояния:
class DemoOrOperatorState extends State { constructor(){ super() this.addNextState(startDemoState, webhookData => { webhookData === 'demo' }) this.addNextState(operatorState, () => true) } async execute(channelId, contactId, webhookData){ const message = 'Do you want to see demo or talk to one of us?' await amioApi.sendMessage(message) } }
После рефакторинга
Рефакторинговый код инкапсулирует состояния в объекты. Каждое состояние описывает, что он делает и в какие следующие состояния может перейти бот. Теперь проще повторно использовать и добавлять новые состояния.
Вероятно, самым большим преимуществом инкапсуляции является то, что каждое условие транзитивности содержит только логику, необходимую для перехода из состояния A в состояние B. Прежде чем использовать шаблон состояния, мы должны были принять во внимание все возможные побочные эффекты, которые могут вызвать нежелательное состояние. Например, чтобы быть уверенным, что получение «демо» в сообщении вызовет только StartDemoState
, нам нужно было addthis.lastState === states.OPERATOR_OR_DEMO
. Это больше не нужно.
Состояния как объекты также легче тестировать. Вы можете просто протестировать state.execute()
и все его state.nextStates
условия изолированно.
Прощание
Мы разработали простую структуру для Node.js. Мы используем его в наших проектах чат-ботов. Я надеюсь, что мы опубликуем его в ближайшее время. Он также будет охватывать перехватчики, обратные передачи и некоторые тестовые утилиты. А пока вы можете сохранить ссылку на наш GitHub, где мы будем публиковать наши ресурсы и фреймворк будет одним из них.
Дайте нам знать, что вы думаете, и помог ли вам этот пост!
Команда Amio.io
Если вы не можете дождаться выпуска фреймворка бота, проверьте наш API. ;)
Избранное фото от rawpixel
Первоначально опубликовано на blog.amio.io 7 июня 2018 г.