Инкапсуляция состояний в объекты уменьшит сложность вашего чат-бота и улучшит его тестируемость.

Я хотел бы продемонстрировать, как повысить удобство обслуживания ваших чат-ботов, если вы решили реализовать своего чат-бота как машину состояний.

Мы будем работать с очень маленьким примером — всего 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 г.