iT邦幫忙

2026 iThome 鐵人賽

DAY 1
0
Software Development

ERP 架構師筆記:定義驅動的框架設計系列 第 1

Day 1:為什麼 ERP 需要 Definition-Driven 框架

  • 分享至 

  • xImage
  •  

Day 1:為什麼 ERP 需要 Definition-Driven 框架

這是「ERP 架構師筆記:定義驅動的框架設計」系列的 Day 1。

接下來三十天談的不是「怎麼用某個框架做一張表單」,而是怎麼設計一套能驅動上百張表單的框架:它該替使用它的人決定哪些事、哪些留給對方、每個機制為什麼長成那個樣子。案例固定用 Northwind 範例資料庫驗證,但案例是佐證,不是主角。

本篇說明:

  1. ERP 開發成本的三層結構:單次變更、規模、客製交付
  2. 這三層收斂出的三個老問題,以及為何工具解不掉
  3. 典範的分歧點:唯一真相是程式碼還是定義
  4. 對照實驗:同一個欄位在兩種典範下各改幾處

先交代我是誰

我二十多年來一直在 ERP/HRM 套裝軟體業擔任軟體架構師。套裝軟體的產品週期非常長,動輒十年、二十年起跳,這段時間裡系統不只是被維護,還得跟著前端技術的世代更替長出不同的樣子,我經歷過的就有桌面程式、Web、行動 App 三代,每一代來的時候問題都一樣:那幾百張畫面,要不要整批重做一次?在這種產品線上,架構決策不會在三年後被誰重寫掉,你會在十年後親眼看到它長成什麼樣子,然後繼續維護它。而我一直待在同一個職位上,沒有轉去管專案、管團隊而離開程式碼,當年那些決定的後果,一次也沒有錯過。

這套框架與這個系列

這些年累積的想法,有些在專案裡做過,更多是想過而沒有動手,一個人要把它們全部寫出來並不划算,而這個成本現在不一樣了。我用 Claude Code 把沒動手的那些做出來,成了一套開源框架 bee-library,後面的 Northwind 案例就跑在上面。

案例本身是另一個 repo,bee-northwind-avalonia:框架那一個裝的是機制,案例這一個只有應用自己的定義檔與程式,框架以套件引用進來。想知道套用這套框架做一個應用實際要準備哪些定義檔、還要寫哪些程式碼,看案例這一個比看框架清楚。

框架還在持續發展,該收斂成機制的東西會陸續加進來,介面也會跟著改,破壞性變更會發生。現階段要直接引用它的套件得先把這件事算進去,也可以整份 clone 回去改成自己需要的樣子。

這個系列不是它的使用教學,框架在這裡只有一個角色:證明這些想法不是紙上談兵。三十篇談的是它已經做出來的那些機制,遇到什麼問題、怎麼解、代價是什麼、為什麼選這一種,實作細節不逐項展開。你完全可以不用它,只把「讓定義當源頭」這個想法帶走,用你熟悉的語言和工具去實現。


整個系列的命題只有一句:

ERP 的複雜度不在單一功能有多難,而在同一件事被重述太多次、乘以太多張表、再乘以太多次客製。


一、單次變更成本:一個欄位落在幾個地方

需求:訂單要多一個「預計到貨日」。沒有計算、沒有連動、不影響既有邏輯。使用者說這是「加一個欄位而已,應該很簡單吧」,而且他說得對。

在一套典型的 Code-First 堆疊(ORM + Web API + 前端)裡,這個欄位會落到十三個地方:

# 位置 內容
1 Entity 類別 加一個屬性
2 型別對應設定 精度、允不允許 NULL、欄名對應
3 Migration 產生、審閱、進版控、上線時執行
4 查詢投影 清單查詢的 Select、報表的欄位清單
5 DTO Request 與 Response 各一次
6 Mapper 設定 Entity 與 DTO 的對應
7 驗證 必填、範圍、與訂單日期的先後關係
8 API 契約 OpenAPI schema,並重新發佈給消費端
9 前端型別 TypeScript interface 或 C# model
10 前端表單 控件、標籤、版面位置
11 前端清單 欄位、寬度、排序
12 多語系資源 每個語系一份
13 測試資料 fixture、seed、假資料產生器

先說公道話:其中好幾處可以自動產生(Migration 由工具產生、OpenAPI 由組件掃描產生、前端型別可由 OpenAPI 產生),真正要手寫的大約五、六處。

但重點不在手寫幾處:

「預計到貨日是一個 Date、必填、顯示在訂單日期後面」這一個事實,在這套堆疊裡被重述了十三次。

每一次重述都是一個可能不一致的點:

  • DTO 忘了加 → 欄位存進資料庫,前端拿不到
  • 驗證忘了加 → 使用者送出空值
  • 清單忘了加 → 使用者問「為什麼查詢畫面看不到」

這些不是「難」的問題,是「多」的問題,而且每一次不一致都要靠一次測試或一次使用者回報才會被發現。

這張表中間橫著一條團隊邊界

第 1 到 8 項在後端,第 9 到 13 項在前端。前後端若是兩個團隊,那條線正好穿過表格中間,而跨過邊界的每一處性質都會改變:從「編輯」變成「協調」。

一個加欄位的需求於是長成這樣:

  • 先開會確認契約,再各自排期
  • 順序相依:後端沒上,前端沒東西可接,要先動工就得自己刻 mock
  • 上線窗口:兩邊誰先上,都得處理對方還沒跟上的狀態
  • 聯調時才發現,一方把「必填」理解成前端擋,另一方理解成後端擋

上面這幾件事,沒有一件是在寫那個欄位。它們吃掉的不是工時,是等待:使用者算的是從開口到上線過了多久,工程師算的是自己動手多久,於是一邊問「你們改一個欄位要這麼久?」,另一邊覺得自己並沒有偷懶。兩邊都對,因為成本是真的,只是不落在敲鍵盤那一段。


二、規模成本:把上面那張表乘以一千

一套完整的套裝 ERP 涵蓋財務、採購、庫存、生產、銷售、人資,業務資料表動輒數千張。

ORM 的核心前提是物件與資料表的對應,所以:

數千張業務表
  → 數千個 Entity 類別
  → 加上 DTO(多半不只一個:List / Detail / Create / Update)
  → 加上 Mapper 設定
  → 加上 Repository

這些類別絕大多數彼此雷同。SupplierShipper 的 Entity 差在欄位名與數量,結構上是同一個模子。

這不是在說 ORM 不好。ORM 在它被設計的場景裡運作良好,問題出在數量:當同一個模式要被複製數千次,複製這件事本身就變成主要成本。

而關連把這個數量再放大一次:

  • ERP 的表本來就高度牽連:訂單指向客戶、業務員、出貨商;出貨單指向訂單、倉庫、批號
  • 每一條關連都要有人決定怎麼表達、怎麼載入、誰維護。一條一條看都合理,數千張全部攤開就沒有人裝得下整張圖。

把第一節那十三格乘以數千,連同那條團隊邊界一起乘。跨團隊協調不是每張表付一次,是每次需求付一次,而需求數量本身就跟表數量成正比。

這不是「麻煩」,是管不動。這些症狀,維護過大型系統的人都認得:

  • 沒有人敢動共用類別,因為不確定誰在用
  • 新人要很久才敢碰核心模組
  • 同一個概念在不同模組有好幾種寫法,因為它們由不同時期、不同人各自長出來

三、客製成本:改完之後怎麼交出去

前兩層講「改起來多費工」,第三層講更根本的問題。這一層繞不開,因為:

ERP 幾乎不存在「裝了就用」這回事。

每次導入都伴隨客製:這家的單號規則不同、那家的成本計算方式不同、這個部門要多兩個欄位、那張報表要換一種分組。差別只在客製透過什麼路徑交付。

Code-First 模式下,那十三處全部是程式碼,而程式碼只有一條交付路徑:

改 → 編譯 → 測試 → 打包 → 部署

同一條路徑在三種部署形態下痛成三種樣子:

部署形態 客製的痛點
單一組織內部系統 最單純,排一次維護視窗即可。但「把小數位數從 2 改成 4」仍要走完整 build / test / deploy
套裝賣 N 家,各自部署 只有兩條路:進主幹用開關控制(開關越積越多,最後沒人敢刪任何一個),或開分支(N 條分支各自演進,主幹修掉的問題要逐一同步)
多租戶 SaaS,共用組件 租戶 A 改一個計算式,卻要協調所有租戶的維護視窗、回歸測試所有租戶的功能,任何一個租戶出事就整批 rollback

三種形態,同一個結論:

需求的大小,與交付的成本,徹底脫鉤。

不管需求是「把小數位數從 2 改成 4」還是動到整個模組,走的都是同一條交付路徑、承擔同一份部署風險。結果是小需求也得排進版本節奏,工程團隊被迫把所有變更都當大事處理。

脫鉤的根源是客製被表達成了程式碼。部署形態只決定這件事有多痛,不決定它痛不痛。

這一層才是定義驅動要解的問題:

定義是資料,不是程式碼。改資料不需要重新編譯,也不需要重新部署組件。


四、三個一直沒解的老問題

前三節的成本收斂起來就是這三個:

老問題 症狀
規格分散三層 同一個欄位在 UI、DTO、DB Migration 各定義一次,極易不一致
業務邏輯分散 不同模組由不同工程師維護,風格不一、重複開發
客製化難標準化 客製邏輯無法回饋到標準版,累積成無法治理的技術債

過去二十年的解法多半是加工具:程式碼產生器、專案樣板、更聰明的 mapper、更自動的 migration。這些工具確實讓每一次重述變便宜。

但它們沒有改變「同一件事被重述十三次」這個結構,只是讓重述變快。

第三層的客製成本,工具更幫不上忙:產生出來的程式碼還是程式碼,還是要編譯部署。第三個老問題拖了二十年沒解,根源就在這裡。


五、典範的分歧點

問題不在工具,在典範。只有一個問題要回答:

系統的唯一真相,是程式碼,還是定義?

  • Code-First:Entity 類別是源頭,資料庫由它衍生(Migration)、API 契約由它衍生(OpenAPI)、前端型別由它衍生。定義檔是附屬產物。
  • Definition-Driven:一份結構描述是源頭,UI、資料庫、CRUD SQL、驗證規則全部由它衍生。程式碼只在真正需要業務判斷的地方出現。

差別不只是檔案格式從 .cs 換成 .xml,而在源頭的性質:

程式碼是源頭 定義是源頭
上千張表的表現形式 上千個類別 上千份定義資料
要處理它們,你能用 編譯器、IDE、重構工具 加上:批次腳本、產生器、程式化檢核、AI
變更的交付路徑 編譯、打包、部署 換一份定義
客製的表現形式 分支或開關,散在原始碼裡 疊加在標準定義之上的一層資料
前端怎麼知道有哪些欄位 另一份手寫的型別,靠契約與人來同步 執行期取得同一份定義
換一代前端要付什麼 每張畫面重做一次 寫一個新的渲染層
誰能修改 開發者 開發者、顧問、工具,甚至使用者

其中兩列要多說幾句:

  • 「上千個類別」與「上千份定義資料」數量一樣,管理成本不同量級。上千份同構的 XML 可以寫一支腳本掃過去,檢查「所有金額欄位是否都設了正確小數位數」,幾分鐘的事;換成上千個類別,同樣的檢查要複雜得多。
  • 換代成本是我感受最深的一項。前端技術大概每十年換一代,畫面若一張張手寫,換代就是整批重做;畫面若從定義長出來,換代是寫一個新的渲染層。表單數量越多,差距越大。

這也是版面要獨立成一層定義的理由:結構穩定,跟著業務走;版面易變,跟著前端世代走。分開之後,換代時重寫的是渲染層,不是那上千份結構定義。


六、對照實驗:同一個欄位改幾處

回到「訂單加預計到貨日」。我拿本系列的案例應用實際數了一次,答案是三處定義修改,零行程式碼。

資料表結構:

<DbField FieldName="required_date" Caption="Required Date" DbType="Date" />

表單結構:

<FormField FieldName="required_date" Caption="Required Date" DbType="Date" />

版面:

<LayoutField FieldName="required_date" Caption="Required Date" />

改完之後自動跟上的有:資料庫欄位、表單控件、清單欄位、新增修改刪除的 SQL、跨層傳輸。

  • 沒有 Entity 類別要同步,因為 SQL 是依定義即時組出來的
  • 沒有 DTO 與 mapper 要同步,因為跨層傳的資料本身就帶著結構描述

這個數字視情況會多一處或少一處,但重點不在數字,是這幾處全都是定義資料。

那條團隊邊界呢

前端不再自己宣告一次欄位,它在執行期向後端要那份定義,然後照著渲染:

前端啟動 → 向後端要那份定義 → 依定義長出控件、清單欄、驗證
  • 契約不需要「談」,因為契約就是那份定義本身,沒有第二份可以對不起來
  • 不需要等對方上線,後端改完定義,前端下次取得就是新的
  • 第一節那串協調、排期、順序相依、上線窗口,多半直接消失

剩下的只有第一步的一半:這個欄位在業務上叫什麼、放哪裡、要不要必填。而這件事本來就該有人決定,它是業務判斷,不是溝通成本。

但這條邊界沒有消失,只是移動了。

  • 前端仍要有一個「懂得讀定義」的渲染層,這一層要有人做。差別在它做一次,之後每張表單、每個欄位都免費。
  • 團隊邊界從「每個欄位」下移到「渲染層」,對話頻率從「每次需求」變成「框架能力有變動時」
  • 真正不能宣告成定義的東西仍然要溝通。一條「這張單據在什麼狀況下不准出貨」的規則,該由誰擋、擋在哪一層、訊息怎麼寫,換什麼架構都逃不掉。定義驅動能消滅的是結構的重述,不是語意的協商。

改完之後

還有兩個現實問題,框架都得處理掉:

  • 實體資料庫還沒有那個欄位 → 框架比對定義與實際結構,把欄位補上,不必人手寫 migration(Day 6 展開)
  • 系統正在跑,怎麼知道定義變了 → 改掉定義的那個節點要能通知其他節點,讓它們手上的快取失效(Day 12 展開)

重點只有一句:改定義到生效,中間沒有編譯、沒有打包、沒有部署組件。


七、這三十天的地圖

主題
一(Day 1–3) 定義驅動的起點
二(Day 4–8) 定義層設計
三(Day 9–12) 資料存取與快取
四(Day 13–18) 業務邏輯與 API
五(Day 19–21) 多租戶與客製化
六(Day 22–25) 傳輸、安全與稽核
七(Day 26–28) 資料語意正確性
八(Day 29–30) 案例回訪與全圖

每一篇的寫法大致相同:先講這件事要考量什麼,再談框架怎麼設計、為什麼這樣設計,能拿案例驗證的就用 Northwind 的實際定義片段收尾。

兩件事先講清楚:

  • 重點在長期成本,不在開發速度。短期看 Code-First 幾乎必勝,工具成熟、生態完整、上手快。定義驅動的成本集中在前期,回報要到第三年、第五年才顯現。如果你的系統做完就結案、三年後整套重寫,這個系列的價值有限;它是寫給知道自己要維護很久的人。
  • 取捨與代價會寫得跟好處一樣清楚。一套框架的價值不在它宣稱能做什麼,而在它明確知道自己不做什麼、以及為什麼。高度動態的 UI、儀表板、報表、非表單型功能,硬套這個模式只會更痛。

一個伏筆

明天談框架該替使用者決定哪些事,後天讓案例正式登場。

案例用 Northwind 做成一套完整的應用:八張表單、主檔明細、三處跨表關連、驗證都在裡面,而其中只有一張需要自己寫程式。那一張裡裝了什麼、以及有哪些地方我們選擇不走定義,系列收尾前會把整套應用攤開來逐項對帳。

ERP 的敵人不是複雜,是重述。而重述之所以無法用工具消滅,是因為它是典範的產物。只要程式碼還是源頭,同一個事實就注定要被說很多次。


本系列同步發表於 HackMD,完整目錄


下一篇
Day 2:Definition-Driven 架構總覽
系列文
ERP 架構師筆記:定義驅動的框架設計3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Wolke
iT邦研究生 4 級 ‧ 2026-08-17 12:21:33

把「預計到貨日」從 UI、DTO 到 DB 要重述那麼多次,真的一看就懂為什麼 ERP 會卡在重複上;你把同一欄位在 Code-First 裡要碰十三處、到 Definition-Driven 只剩三個定義檔,差距很有感。還有那條前後端團隊邊界,從每次需求協調變成只管渲染層,這個切法也很俐落。手邊有多的 Lovable 額度想送給有緣人,有興趣可從連結看看我的系列。 https://ithelp.ithome.com.tw/articles/10401174

我要留言

立即登入留言