在上一篇([08]如何微調FormFlow讓使用上更流暢)介紹完FormFlow之後,我們需要回來看一下目前最大的問題,也就是程式碼都寫在一隻RootDialog
裡面。
Bot Builder SDK有考慮到這件事情,因此内建用IDialog
來解決這個問題。
這篇來看看IDialog
怎麽做到SoC (Seperation of Concern)。
重新思考一下bot的操作模式
在進入IDialog之前,我們先來想一下目前的bot和以前的desktop程式或者web程式有什麽不同。
想象一下,如果今天我們現有功能是寫成一個Desktop的App。
每一個功能我們就會有一個頁面,因此,我們會有:
- 讓使用者輸入姓名的頁面
- 讓使用者查飯店的頁面
- 讓使用者訂房的頁面
而這些頁面之間的跳轉我們是透過按鈕點下去之後出現pop up的方式做完輸入之後,可能按下一個確認鍵把資料儲存起來然後又回到了程式的主頁。
剛剛上面提到的是傳統的app概念,那回到chatbot呢?其實是一樣的概念,只不過我們不是透過按下按鈕來切換功能,而是透過輸入文字、圖片和語音來跳轉(多媒體輸入之後在介紹)
剛剛是使用者面的感受,那從程式的角度呢?不管是desktop還是web,一個頁面就會有一隻獨立的cs檔案,除非你把所有功能都寫在一個頁面上面,要不然多多少少邏輯還是會拆分出來,不會造成一個頁面好多個邏輯,無形之中做到了 SoC。
但是在我們目前的chatbot來説,全部都還是在一起,那麽類似desktop或web每一種頁面一個邏輯的對應東西是什麽?那就是IDialog
重構詢問姓名的邏輯到IDialog
我們用一個例子來學習怎麽使用IDialog來切分邏輯。
我們第一個功能是取得使用者的名字,這個我們可以重構到另外一個IDialog,我稱之爲NameDialog
我們會經歷以下幾個步奏:
- 建立出一個NameDialog
- 把邏輯從RootDialog搬到NameDialog
- 調整RootDialog - 決定什麽時候進入NameDialog
建立出一個NameDialog
要建立一個IDialog
需要兩件事情:
- 要加入
[Serializable]
- 要實作
IDialog<T>
- 其中的T代表會回傳的内容形態
[Serializable]
public class NameDialog : IDialog<string>
{
public Task StartAsync(IDialogContext context)
{
throw new NotImplementedException();
}
}
把邏輯從RootDialog搬到NameDialog
邏輯很簡單,當這個Dialog觸發的時候,會詢問使用者的名稱,然後把使用者輸入的内容回傳回來。程式碼變成:
...
public async Task StartAsync(IDialogContext context)
{
await context.PostAsync("您的名字是?");
context.Wait(MessageReceivedAsync);
}
private async Task MessageReceivedAsync
(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
context.Done(message.Text);
}
...
StartAsync
自動分開了。
調整RootDialog - 決定什麽時候進入NameDialog
我們在RootDialog
還是會判斷有沒有取得過名字,沒有的話就觸發我們NameDialog
...
context.UserData.TryGetValue<string>("Name", out string name);
if(string.IsNullOrEmpty(name))
{
context.Call(new NameDialog(), GreetingAfterAsync);
}
...
我們可以看到,整個判斷問過名字了沒的部分都沒了,全部由NameDialog
處理了,然後我們會在那邊取得使用者輸入的姓名,將會在GreetingAfterAsync
的callback裡面做處理。
private async Task GreetingAfterAsync(IDialogContext context,
IAwaitable<string> result)
{
var name = await result;
context.UserData.SetValue<string>("Name", name);
await context.PostAsync($"{name} 您好,能夠幫助您什麽");
context.Wait(MessageReceivedAsync);
}
可以看到,NameDialog
取得了名稱,在這個callback裡面得到了之後,把這個名稱寫到UserData
裡面。
從上面的測試結果可以看出,我們保持了和以前一樣的流程,但是内部邏輯拆開了。我們有一個專門詢問名字的NameDialog
,至於取得的名字之後要做什麽,我們交由呼叫這個Dialog的程式去控制,達到SoC。
優化NameDialog
到目前爲止,NameDialog的邏輯和未重構前一樣,不過這邊少了一個防呆,也就是如果使用者沒有輸入任何名字怎麽辦?
我們應該要有個邏輯,做這個檢查:
private async Task MessageReceivedAsync
(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
if(string.IsNullOrEmpty(message.Text))
{
await context.PostAsync("請您輸入您的名字");
context.Wait(MessageReceivedAsync);
}
context.Done(message.Text);
}
不過這個時候會遇到另外一個問題,如果使用者一直輸入不符合條件的姓名,他會無限回圈在這裡面。因此,這種類型的重試,都記得要做所謂的只嘗試幾次,避免使用者卡在這個Dialog。
private int Attempt = 3;
private async Task MessageReceivedAsync
(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
if(string.IsNullOrEmpty(message.Text))
{
Attempt = Attempt - 1;
if (Attempt > 0)
{
await context.PostAsync("請您輸入您的名字");
context.Wait(MessageReceivedAsync);
}
else
{
context.Fail(new TooManyAttemptsException("取不到名字"));
}
}
context.Done(message.Text);
}
RootDialog
,不止邏輯看起來複雜,無法做單元測試,同樣邏輯還不能夠重複使用。因此,好好使用Dialog對整個chatbot開發是關鍵。
結語
程式設計師最需要注意的就是保持程式碼的“乾净”,而好好使用IDialog就是讓chatbot程式碼乾净的好幫手。
到目前爲止,chatbot的重要觀念都已經介紹完了,細節的部分還有很多,不過以目前所知要開發出一個chatbot已經不是什麽太難的事情了。
在要轉換文章重心之前,要對目前的code做一次大重構,把這篇所學的IDialog概念都使用上。
下一篇([10]用IDialog全部重構 - 階段性總結)將是一個階段性的總結。