Моделирование с Kotlin — размеченные союзы
С тех пор, как Kotlin стал первоклассным гражданином платформы Android, я наслаждаюсь упрощенной церемонией разработки Android. Исходя из функционального языка, такого как F #, я вижу некоторые тонкости, которые есть у Kotlin, например сопоставление с образцом, типы суммыфункции как значения и т. д., хотя он не так функционален по сравнению с функциональными языками, такими как F# или Haskell, мы возьмем то, что получим.
Я работал над недавним проектом, который использует IBM Уотсонкак чат-бот, для запуска и завершения отдельных бизнес-процессов: когда пользователь приложения общается с чат-ботто бот интерпретирует сообщения пользователя, чтобы распознать намерения пользователя в приложении, которые затем преобразуются в бизнес-процесс для завершения пользователем.
Например, пользователь говорит что-то вроде «Я хочу открыть учетную запись», бот отвечает намерением пользователя, например «open_account», затем приложение запускает Открытый счет бизнес-процесс.
Проблема
Было немного запутанно рассуждать о том, как унифицировать функциональность этой функции в однородной модели. И функциональный человек внутри меня начал жаловаться на то, как, если это было фа# это будет просто вопрос сумма и продукт типов, и тут меня осенило! лампочкаКотлин когда выражения может соответствовать иерархии классов (типы суммы), затем я начал искать, как можно смоделировать эту функцию с помощью иерархии классов, и вот мое решение:
Прежде всего, эту функцию нужно было понять, прежде чем ее можно было обобщить в модель. Вот поток:
- Пользователь обсуждает с чат-ботом, пока бот не сможет понять, что пользователь хочет сделать.
- Бот инициирует команду в приложении, чтобы запустить бизнес-процесс или предоставить параметры для того, какой дочерний процесс действительно нужен пользователю.
- Пользователь следует инструкциям по завершению запущенного бизнес-процесса.
например
- пользователь: «Я хочу открыть счет»
- бот: «Я понимаю, что вы хотите открыть счет, пожалуйста, выберите нужный счет»
- бот: триггеры Открытый счет бизнес-процесс, чтобы показать варианты Сберегательный счет или проверка аккаунта
- приложение: показывает пользователю предлагаемые варианты открытия счета
- пользователь: выбирает Сберегательный счет или «сберегательный счет»
- бот: продолжается Открытый счет бизнес-процесс с Сберегательный счет как выбранный дочерний процесс.
- приложение: инициирует процесс открытия сберегательного счета
- пользователь: следует инструкциям до завершения процесса.
- приложение: показывает пользователю сообщение о завершении
Это просто основная идея того, как это будет работать. Потратив некоторое время на размышления об этом, я смог прийти к трем основным объектам, необходимым для того, чтобы эта функция работала.
Идея состоит в том, что у каждого действия есть поддействия или процессы, которые также являются взаимодействиями сами по себе, это означает, что бизнес-процесс может запускаться (например, open_account), но процесс может иметь дочерние процессы в качестве ответвлений (например, open Saving_account или check_account), тогда каждый дочерний процесс имеет набор взаимодействий, которые приводят пользователя к его цели.
В конце концов я смоделировал это так, что верхние уровни называются Рабочие процессы (действия пользователя корневого уровня) и каждый Рабочий процесс содержит Процессы (действия пользователя второго уровня) и каждый Процесс содержит Шаги (взаимодействия с пользователем для завершения определенного Процесс).
- Рабочий процесс: служит родительским процессом для каждого из бизнес-процессов (например, Открытый счет)
- Процесс: это служит дочерним процессом для определенного Рабочий процесс (например Сберегательный счет)
- Шаг: это служит определенным шагом в Процесс (например fill_personal_information)
Вот видео как это выглядит
Код
Вот основная часть кода, связанная с моделированием этой функции.
- ЧатБотАктивность: Сначала у вас есть ЧатБотАктивность который служит родительским интерфейсом
- ЧатВиевМодель: Во-вторых, у вас есть ЧатВиевМодель который обеспечивает Рабочий процесс как LiveData к ЧатБотАктивность.
- Рабочий процесс: Третий у вас есть класс Рабочий процесс который служит база класс для всех рабочих процессов, предоставляя абстрактные методы для реализации.
- Реализации рабочего процесса: В-четвертых, у вас есть конкретные реализации Рабочий процесс базовый класс (например, OpenAccountWorkflow или PayBillsWorkflow).
- ЧатАдаптер: Последнее, что у вас есть ЧатАдаптер отвечает за отображение как серверных, так и клиентских сообщений в представлении ресайклера.
ЧатБотАктивность
Это действие, которое отвечает за общение с чат-ботом на сервере. Самое интересное, что происходит в btnОтправить щелкните прослушиватель, и начатьChatBot метод.
Сначала посмотрите, как выглядит код, а потом я объясню, что происходит.
class ChatBotActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_chat_bot)
binding.btnSend.setOnClickListener {
val userMessage = binding.userMessage.text.toString()
adapter.addItem(ClientMessage(message = userMessage))
binding.userMessage.setText("")
val message = UserAPIMessage(userMessage, context)
service.sendMessage(message, this::handleServerResponse, this::handleError)
}
startChatBot()
}
private fun startChatBot() {
viewModel = ViewModelProviders.of(this).get(ChatBotViewModel::class.java)
viewModel.getWorkflow().observe(this, Observer { workflow ->
workflow?.start(this)
})
service = ChatBotService()
service.startConversation(Object(), this::handleServerResponse, this::handleError)
}
private fun handleServerResponse(response: ServerAPIMessage) {
val serverMessage = response.outputText.reduce { acc, s -> "$acc. \n $s."}
adapter.addItem(ServerMessage(message = serverMessage))
when (response.topIntent) {
"" -> {
}
else -> {
val process: IntentEntity? = when(response.entities.size) {
0 -> null
else -> response.entities[0]
}
viewModel.startWorkflow(response.topIntent, process?.value)
}
}
context = response.context
}
private fun handleError(message: String) {
Log.e("ChatBotActivity", message)
}
}
в btnОтправить click listener, я сначала получаю строковое значение того, что ввел пользователь, и отправляю его в бот для обработки также прикрепляю мои обратные вызовы для ответов onsuccess и onfailure.
начатьChatBot метод сначала получает и наблюдает рабочий процесс liveData ViewModel действия реагирует на изменения в liveData, вызывая метод начинать метод нового рабочий процессчтобы запустить свои процессы.
Метод handleSuccess является успешным обратным вызовом для ответа сервера, он добавляет сообщение сервера в представление и проверяет, рабочий процесс и процесс были возвращены сервером, а затем передают их модели представления для обработки.
Далее мы поговорим о том, что делает модель представления.
ЧатВиевМодель
Чтобы понять, что делает чат viewModel, вы должны помнить, что сервер сообщает о намерениях пользователя через закодированные строки, поэтому все, что нужно сделать, это использовать закодированную строку для преобразования намерений пользователя в рабочий процесс.
Вот как выглядит код, объясню после:
class ChatBotViewModel: ViewModel() {
private val workflow = MutableLiveData<Workflow>()
fun getWorkflow(): LiveData<Workflow> = workflow
fun startWorkflow(tag: String, process: String? = null) {
when(tag) {
TAG_WORKFLOW_PAY_BILLS -> { workflow.postValue(PayBillsWorkflow(process)) }
TAG_WORKFLOW_OPEN_ACCOUNT -> { workflow.postValue(OpenAccountWorkflow(process)) }
else -> {}
}
}
companion object {
const val TAG_WORKFLOW_OPEN_ACCOUNT = "create_account"
const val TAG_PROCESS_OPEN_SAVINGS = "savings"
const val TAG_PROCESS_OPEN_CURRENT = "current"
const val TAG_WORKFLOW_PAY_BILLS = "pay_bills"
const val TAG_PROCESS_PAY_POWER_BILL = "Power"
const val TAG_PROCESS_PAY_WATER_BILL = "Water"
const val TAG_PROCESS_PAY_CABLE_BILL = "cable"
}
}
Ниже методов экземпляра ViewModel, в его сопутствующий объект— это статические закодированные строки, которые помогают преобразовывать предполагаемые действия пользователя в рабочие процессы конкретного приложения.
Метод первого экземпляра получитьрабочий процесс возвращает LiveData, которые может наблюдать наблюдатель, в моем случае ЧатБотАктивность.
Метод второго экземпляра startWorkflow отвечает за перевод закодированного строкового ответа сервера в смоделированное приложение рабочий процесс путем создания рабочего процесса с выбранным процесс.
Далее следует то, что представляет собой каждый рабочий процесс.
Рабочие процессы
Рабочий процесс должен быть сердцем смоделированного решения, поэтому я абстрагировал его в базовый класс, который содержит необходимое для того, чтобы дочерний класс назывался рабочий процесси вот как это выглядит.
abstract class Workflow(process: String?) {
protected var completed = false
fun isComplete() = completed
abstract fun nextStep(context: Context)
abstract fun start(context: Context)
}
Первое, на что следует обратить внимание, это конструктор, который принимает процесс параметр. Назначение этого параметра — определить, какой дочерний процесс следует инициировать при запуске рабочего процесса, то есть если указанный процесс не является нулевым.
Два абстрактных метода учитывают контекст, потому что некоторые рабочие процессы требуют расширения пользовательского интерфейса или манипулирования им, поэтому я передаю контекст Activity при вызове методов, предоставляя рабочему процессу доступ к контексту пользовательского интерфейса.
начинать метод отвечает за запуск текущего шага для определенного процесса в рабочем процессе, а следующий шаг метод отвечает за изменение и запуск следующего шага в рабочем процессе.
Реализация рабочего процесса
Код ниже показывает оплатить счета реализации рабочего процесса, помните, что рабочий процесс требует еще двух вещей процесс и Шаги. Итак, здесь я сначала определяю Шагии каждый Процессчто необходимо для заполнения PayBills Рабочий процесс.
abstract class BillType {
class WaterBill: BillType()
class PowerBill: BillType()
class CableBill: BillType()
}
abstract class PayBillSteps {
class ShowBillOptions: PayBillSteps()
class ShowBillForm(val type: BillType): PayBillSteps()
class ShowCompletionMessage: PayBillSteps()
}
class PayBillsWorkflow(process: String?) : Workflow(process) {
private var currentStep: PayBillSteps
private lateinit var billType: BillType
init {
when (process) {
ChatBotViewModel.TAG_PROCESS_PAY_WATER_BILL -> {
billType = BillType.WaterBill()
currentStep = PayBillSteps.ShowBillForm(billType)
}
ChatBotViewModel.TAG_PROCESS_PAY_CABLE_BILL -> {
billType = BillType.CableBill()
currentStep = PayBillSteps.ShowBillForm(billType)
}
ChatBotViewModel.TAG_PROCESS_PAY_POWER_BILL -> {
billType = BillType.PowerBill()
currentStep = PayBillSteps.ShowBillForm(billType)
}
else -> {
currentStep = PayBillSteps.ShowBillOptions()
}
}
}
override fun nextStep(context: Context) {
currentStep = when (currentStep) {
is PayBillSteps.ShowBillOptions -> PayBillSteps.ShowBillForm(billType)
is PayBillSteps.ShowBillForm -> PayBillSteps.ShowCompletionMessage()
else -> throw IllegalStateException("currentState is invalid")
}
start(context)
}
override fun start(context: Context) {
when (currentStep) {
is PayBillSteps.ShowBillOptions -> this.showBillOptions(context)
is PayBillSteps.ShowBillForm -> this.showBillForm(context, billType)
is PayBillSteps.ShowCompletionMessage -> this.showCompletionMessage()
else -> throw IllegalStateException("currentState is invalid")
}
}
private fun showBillOptions(context: Context) {
this.nextStep(context)
}
private fun showBillForm(context: Context, billType: BillType) {
this.nextStep(context)
}
private fun showCompletionMessage(context: Context) {
this.completed = true
this.nextStep(context)
}
}
Это, прежде всего, то, где Дискриминированный союз или Типы сумм вступить в игру в моем предложенном решении. Абстрактные классы служат базовым типом, а внутренние подклассы служат конкретными типами для каждого абстрактного класса. Например, WaterBill и PowerBill являются BillTypes.
Приведенный выше код определяет простой рабочий процесс для оплаты счетов за коммунальные услуги, он по сути определяет типы счетов, которые можно оплатить, и шаги для завершения оплаты счета.
Здесь у нас есть три типа счетов, каждый из которых определяет процесс, инициированный сервером, и используется для того, чтобы узнать, какие компоненты пользовательского интерфейса отображать пользователю, а затем у нас есть три шаги которые ведут пользователя от начала до конца инициированного платежные процессы.
начинать метод сначала вызывается для запуска этого рабочего процесса, который выполняется ЧатБотАктивность в начатьChatBot метод.
Что за начинать метод делает это, он использует Сопоставление с образцом чтобы просмотреть текущий шаг и вызвать соответствующую функцию-член, чтобы начать этот шаг в частности. С этого момента в конце каждой функции-члена, такой как showBillForm, например, следующий шаг метод будет вызываться, чтобы обозначить завершение этого шага и инициировать переход к следующему шагу.
следующий шаг метод по существу отвечает за переход к следующему шагу, он также использует Сопоставление с образцом чтобы посмотреть, что такое текущий шаг, а затем он просто перемещает текущий шаг к следующему шагу, затем вызывает метод запуска, чтобы начать это следующий шаг.
И это почти все волшебство, красота этого подхода заключается в ясности и читабельности кода. Вам не нужно знать Kotlin, чтобы понять, чего я пытаюсь здесь достичь, потому что большая часть кода подразумевается сама собой и читается как английский.
ЧатАдаптер
Эта последняя часть просто дополнительная, она просто показывает, как сообщения отображаются в представлении ресайклера, для этого я использую этот chatAdapter.
ChatAdapter отвечает за отображение двух типов сообщений, одного от сервера и одного от клиента, а перечисление помогает определить целочисленное значение для типа сообщения для целей увеличения представления.
Классы ChatMessage и ServerMessage помогают определить, какое сообщение было создано, а интерфейс ChatMessageModel помогает сохранить однородный список сообщений адаптера, поскольку его реализуют как серверная, так и клиентская модели сообщений, а адаптеру просто требуется список моделей, реализующих интерфейс.
enum class MessageType(private val type: Int) {
ServerMessage(0),
ClientMessage(1)
fun getType(): Int = type
}
interface ChatMessageModel {
fun getType(): Int
}
class ClientMessage(val message: String): ChatMessageModel {
override fun getType() = MessageType.ClientMessage.getType()
}
class ServerMessage(val message: String): ChatMessageModel {
override fun getType() = MessageType.ServerMessage.getType()
}
class ChatBotAdapter: RecyclerView.Adapter<ChatViewHolder>() {
private val list = mutableListOf<ChatMessageModel>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatViewHolder {
return when(viewType) {
MessageType.ClientMessage.getType() -> {
val layout = LayoutInflater
.from(parent.context)
.inflate(R.layout.list_item_message_client, parent, false)
ClientChatViewHolder(layout)
}
MessageType.ServerMessage.getType() -> {
val layout = LayoutInflater
.from(parent.context)
.inflate(R.layout.list_item_message_server, parent, false)
ServerChatViewHolder(layout)
}
else -> throw IllegalArgumentException("viewType is not amongst defined view types")
}
}
override fun getItemCount() = list.size
override fun onBindViewHolder(holder: ChatViewHolder, position: Int) {
val item = list[position]
holder.bindView(item)
}
override fun getItemViewType(position: Int) = list[position].getType()
fun addItem(model: ChatMessageModel) {
val position = list.size
list.add(model)
notifyItemInserted(position)
}
fun remove(model: ChatMessageModel) {
val index = list.indexOf(model)
list.removeAt(index)
notifyItemRemoved(index)
}
}
abstract class ChatViewHolder(view: View): RecyclerView.ViewHolder(view) {
abstract fun bindView(model: ChatMessageModel)
}
class ServerChatViewHolder(view: View): ChatViewHolder(view) {
private val text: TextView = view.findViewById(R.id.messageBody)
override fun bindView(model: ChatMessageModel) {
text.text = (model as ServerMessage).message
}
}
class ClientChatViewHolder(view: View): ChatViewHolder(view) {
private val text: TextView = view.findViewById(R.id.messageBody)
override fun bindView(model: ChatMessageModel) {
text.text = (model as ClientMessage).message
}
}
Заключение
Функция сопоставления с образцом в сочетании с размеченными союзами очень эффективна в kotlin по трем основным направлениям:
Это позволяет вам определить ваше решение в унифицированную модель, которая не только показывает вам различные ветки или состояния для вашего приложения (например, перечисления), но он также может собирать данные для каждого конкретного состояния (в отличие от перечисления) как BillStep Показатьформу счета выше, который принимает BillType в качестве параметра конструктора.
Он читается как английский, как, по моему мнению, должно выглядеть большинство кодовых блоков, так же просто, как читать английские предложения: «когда тип счета является PowerBill -> showBillForm(PowerBill)».
Исчерпывающее сопоставление, еще одна замечательная функция, которая заставляет вас, разработчика, учитывать все возможные ситуации для шаблона. То есть, если вы не сопоставите все возможные шаблоны BillTypes выше, компилятор будет жаловаться. Это более эффективно, когда требования к приложению изменяются и вы добавляете новый BillType, например MortgageBill, а затем забываете сопоставить его в рабочем процессе, компилятор покажет ошибку, и проект не будет построен.
В общем, я люблю выражать свои идеи и решения в простых формах, и это действительно волнует меня как разработчика. Так что знакомство с одной из моих любимых функций функционального программирования в Android было удивительным путешествием, которым я должен был поделиться. Надеюсь, вам будет интересно, как и мне.