
昨天結尾說今天回到案例,把這些機制逐項歸位。
Day 1 說整套 Northwind 的八張表單裡只有一張需要自己寫程式,並答應系列收尾前把整套應用攤開逐項對帳;那一張裡裝了什麼,Day 3 已經交代過。今天把每一個行為拆開,看每一段由誰負責。
本篇說明:
系統裡的每一個行為拆開之後,每一段都落在下面三個層次之一:
| 層次 | 判準 |
|---|---|
| 定義 | 這個行為由一份定義檔決定,改那份檔案就會變 |
| 框架內建 | 這一段是框架的程式在跑,應用沒有為它寫程式 |
| 應用程式碼 | 有人手寫了 C#,換一套 ERP 就得重寫一次 |
以明細的金額為例,拆開來分屬三個層次:
| 這一段 | 層次 |
|---|---|
| 金額的算式 | 定義,寫在欄位的 ValueExpression 上 |
| 捨到幾位 | 定義,查幣別主檔 |
| 什麼時候重算、什麼時候捨入 | 框架內建 |
| 把各列金額加總寫回主檔 | 應用程式碼 |
所以下一節的表上,有幾列寫的是「定義加框架內建」。
後面幾張表記的是案例現在的樣子,不一定是該在的位置。該在哪裡,用 Day 2 的判別法判斷:全世界的 ERP 做起來是不是都一樣,一樣的由框架做,不一樣的留給應用。上面那個加總就是例子,每一家都一樣,現在卻還寫在應用程式碼裡。
共用與紀錄兩個分類的 TableSchema 直接用框架的預設;部門、員工、稽核規則與權限那幾張,是照 Day 4 的做法從框架預設複製過來,員工另外加了兩個欄位。
Day 2 列過由定義衍生的東西:多數是產生,另一種是拿宣告的規則與算式去求值。實際跟著一張表單出現的行為,比那份清單多幾項:
| 行為 | 落在哪個層次 | 案例上的樣子 |
|---|---|---|
| 欄位、型別、允不允許 NULL、唯一性 | 定義 | 一份 FormSchema 宣告完 |
| 資料表建立與後續升級 | 定義加框架內建 | 定義是每張實體表一份 TableSchema 加 DbCategorySettings 上一列;比對現況與下 DDL 是框架的 |
| 單筆、清單與開窗選取的版面 | 定義加框架內建 | 單筆表單讀應用寫的八份 FormLayout,清單與開窗選取由 FormSchema 的欄位集現排 |
| 增修刪查的語句 | 框架內建 | 查詢依這一次用得到的欄位現組,含跨表 JOIN;新增與修改帶整張表的欄位 |
| 跨表關連 | 定義加框架內建 | 一段宣告同時決定編輯器、選完回填、JOIN 與存進去的值 |
| 跨層傳輸的資料容器 | 框架內建 | 沒有實體類別、沒有 DTO、沒有對應設定 |
| 驗證規則與計算欄 | 定義 | 全應用只有訂單宣告了 FormRule 與一個 ValueExpression |
| 新增一列的預設值 | 框架內建 | 依欄位型別填,日期欄填的是使用者時區的今天 |
| 對外開放的動作 | 框架內建 | 八張表單共用同一組,差別只有 ProgId |
| 選單與程式註冊 | 定義 | MenuSettings 一項、ProgramSettings 一列 |
| 定義之間對不對得起來 | 框架內建 | 隨套件出貨的建置期診斷,案例伺服端建置時就會檢查 |
加一張表單要碰的地方全部落在定義那個層次:一份 FormSchema、每張實體資料表一份 TableSchema、一份 FormLayout、DbCategorySettings 一列、ProgramSettings 一列、MenuSettings 一項。前三項是新檔案,後三項各加一行。建表的程式也不必動,它照 DbCategorySettings 登錄的表逐一建立或升級。
定義那個層次也會出錯,而且不會有人報錯。明細的 discount 只宣告了型別、沒有給位數,於是它建成一個小數位數為零的欄位;案例用的 SQLite 會原樣保存小數,折扣又全部是零,所以從來沒踩到。
至於應用程式碼那個層次:八張表單裡七張是零行。ProgramSettings 上那七列只寫了 ProgId 與顯示名稱,代表整條流程走預設。
下面這些事每一次呼叫都會發生,最後一欄是應用為它付出的:
| 一次呼叫會發生的事 | 落在哪個層次 | 應用付出的 |
|---|---|---|
| 端點、請求解析、方法派發 | 框架內建 | 一個空的控制器子類 |
| 序列化與傳輸型別綁定 | 框架內建 | 伺服端零行;除了桌面,其餘三個前端的建置檔上各留了一筆放行 |
| 壓縮、加密、金鑰推導 | 框架內建 | 開機那幾行:主金鑰種子、payload 選項、自動建立旗標 |
| 存取控制、API 金鑰檢查、權限的三軸 | 框架內建 | 沒有另外宣告 |
| 登入、session、使用者時區與語言 | 框架內建 | 程式零行,時區與語言讀的是使用者那一列種子資料 |
| 錯誤契約與使用者看得到的訊息 | 框架內建 | 訊息文字寫在 FormRule 的屬性上,訂單的程式則寫在丟出的例外裡 |
| 稽核 | 框架內建 | SystemSettings 上一段開關、DbCategorySettings 上幾列,程式零行 |
| 時區換算 | 框架內建 | 程式零行,應用自己那兩個時間欄位都是日曆日 |
| 數值捨入 | 框架內建 | 兩個欄位標了 NumberKind,另外部署幣別主檔、公司本幣設為 USD |
| 定義快取與跨節點失效 | 框架內建 | DbCategorySettings 上一列通知表 |
另外有一組機制,案例用得很少:
它們要等到「第二個」出現才划得來:第二家公司、第二種語言、第二個角色、第二種幣別。所以這一組不能算進「框架替應用省下的」,它們是這套設計比較貴的部分,在第二個出現以前,付出去的成本看起來像純粹的浪費。
OrderBO 只覆寫了框架切出來的其中兩個步驟:取一筆新資料,以及存檔之前。後者長這樣:
protected override void DoBeforeSave(SaveContext context)
{
base.DoBeforeSave(context); // FormRule 與 ValueExpression 先在這裡跑完
var dataSet = context.DataSet;
var master = OrderDataSet.MasterRow(dataSet);
OrderDataSet.RequireAtLeastOneDetail(dataSet);
EnforceStatusRules(dataSet, master);
AssignOrderNumber(master);
OrderDataSet.ComputeTotal(dataSet);
}
這裡是四件,第五件在取新資料那一步:
| 這一件 | 為什麼它還在程式裡 |
|---|---|
| 狀態轉換是否合法 | 真正的業務判斷。哪些狀態能跳、確認之後還能不能改明細,每一家訂的都不一樣 |
| 產生訂單編號 | 制式行為,該由框架收斂而還沒收斂 |
| 至少要有一列明細 | 跨列聚合。逐列求值的規則模型看得到一列的欄位,看不到這張單有幾列 |
| 明細加總寫回主檔 | 同上。框架已經負責每一列什麼時候被捨入,把捨過的值加起來這一步還在外面 |
| 訂單日期預設今天 | 框架本來就做了,取的是使用者時區的今天;這一行取的是伺服器的今天 |
表單自訂的邏輯與客製,會沿著同一條路往框架收:
| 步驟 | 發生什麼 | 案例上的樣子 |
|---|---|---|
| 應用先寫 | 框架還沒有對應的機制,寫在覆寫 BO 的步驟或業務外掛裡 | 明細金額、必填檢查與上表五件,一開始都寫在 OrderBO |
| 判斷要不要收 | 用 Day 2 的判別法:每一家做起來是不是都一樣 | 狀態轉換每一家都不同,停在應用 |
| 框架接手 | 能宣告的寫進定義,不必宣告的由框架內建 | 明細金額改成 ValueExpression、必填檢查改成 FormRule、日期預設今天由框架內建;訂單編號與跨列加總還沒有對應的機制 |
| 應用刪掉舊程式 | 已經被接手的那一段從 BO 拿掉 | 金額與必填檢查已經刪了,訂單日期那一行還在 |
伺服端除了訂單,其餘程式都在描述這個部署,跟表單無關:
DatabaseSettings 各有一筆連線,目前都指向同一個檔案。要拆成不同資料庫只改這份設定,換一家資料庫再加上驅動的註冊,表單定義都不必動。前端同樣跟表單無關:
MenuSettings 長出來。表單從八張變八十張,這一側不用改。FormLayout 描述的是欄位怎麼排,不是一個儀表板該長什麼樣這幾塊是邊界,不是待辦。待辦是框架還沒做完的部分,例如訂單編號與跨列加總。
八張表單裡七張是零行,應用外殼與前端都跟表單有幾張無關。
三個層次跟表單數量的關係各不相同。定義那個層次線性成長,而且它該這樣長,因為每一張表單本來就有自己的欄位、自己的關連、自己要顯示什麼。框架內建那個層次是常數,一次呼叫要做的事不會因為表單變多而變多。應用程式碼那個層次跟的是規則,不是表單:框架還沒接住的每一條,每一張用到它的表單都得寫一次,最後該留下的只有每一家各自不同的那幾條。
訂單那五件事裡,有四件遲早可以離開應用:訂單編號等框架收斂,至少一列明細與明細加總等定義能跨列計算,訂單日期那一行直接刪掉。狀態轉換會一直留下,因為每一家都不同。Day 3 結尾說的「只在真正需要判斷的地方寫」,在這個案例上指的就是它。
明天是最後一天,把三十天的東西收成全圖。
本系列同步發表於 HackMD,完整目錄