口述/孫培然‧彙整/CIO編輯室
瞭解以微服務架構打造 HIS 的基本步驟後,接下來就是要認識怎麼把單體變成微服務的概念。讓單體透過 MVC、MVVM、SOLID 及 DCI 破冰轉成微服務架構,也就是先解耦再整合的概念。
長久以來有一種物件導向的概念-MVC,M 就是 Model,也就是商業邏輯,並執行料庫存取相關功能;V 是 View,就是使用者實際看到並操作系統的前端畫面;C 是 Control,就是在負責 View 跟 Model 之間的溝通。要怎麼從單體變成微服務,MVC 架構是非常重要的概念。
MVC 延伸的 MVVM 架構
如 Google 的開源 Web App 框架 Angular 就有分 Template(View)、Service(Model),再透過Property/Event(Control)來做互動溝通的設計概念。Template 就是用 HTML 所開發出來的畫面,Service 透過依賴注入的方式注入到 Control,這就是前端的 MVC 架構。
如果再進一步延伸前端也就是 View,讓 Web App 要的資料會透過 API 去呼叫到 Web API,API 這邊有個 API Control,會去跟後端裝著商業邏輯的 Model溝通,再透過 SQL Server的Stored Procedures 去呼叫應用,就是一個從前端到後端甚至到資料庫的 MVC 架構。
[ 2022年度CIO大調查報告下載 ]
微軟後來將 MVC 架構發展延伸出另一個稱為 MVVM 的架構。M 一樣還是 Model,V 也一樣是View,VM 則是 ViewModel,也就是描述 Model 及 View 進行溝通時,所使用資料實體類別(class ),也就是說,View 跟 Model 之間的相關資料,要透過 ViewModel 來互相溝通。
MVVM 架構的優點,首先是可以降低模組之間的耦合力,可以很容易維護與擴充。其次就是可以讓開發人員專注於前端或者後端的設計,前後端的溝通就是通過 ViewModel,可以很方便的去整合及測試。
物件導向的五大原則
我從開始撰寫醫療資訊程式以來,始終認為一個物件導向的類別到底要多大多小是靠經驗,甚至要有一點藝術的概念。但自從認識了物件導向的五大原則以後,我只要跟隨這個概念,我就可以很清楚一個class到底要多大多小的範圍邊界(Boundary),可以寫出一個很漂亮的一個class。
物件導向的五大原則簡稱為 SOLID。SRP 指的是單一職責(Single Responsibility Principle),也就是說 class 不要做的太複雜,應該且僅有一個原因引起類別的變更,讓類別只有一個職責。比如說給號功能就只有一個給號的類別,關於計價或是收費,就不在這個類別,只做單純的給號工作。
秘訣在於關注點分離,不要太貪心,不要把所有類別搞在一起,就像一個人只有兩隻手,如果要求他要打電腦,又要打電話,又要做運動,又要煮菜等這麼多職責,就會造成什麼事情都做不好。
單一職責其實就像 MVC 架構,Model 只做企業邏輯規則,View 只負責前端,Control 只負責 Model 跟 View 之間的溝通,這就是單一職責的概念。
OCP 是開放封閉原則(Open Closed Principle),就是對軟體中的 class,若要擴展採取自由開放的,對於修改則是嚴禁封閉的。因為單體最常被詬病的地方,就是因為程式會改來改去,改了 A 會動到 B,改了 C 又動到 D。所以開放封閉原則就是要求程式一旦寫好了,就盡量不要修改,如果需要什麼新增功能再擴充。
很多人剛開始會以為不可能,但如果不這麼做,就沒辦法確認服務的品質。舉例而言,使用者說他要一個加法,如果程式設計者沒有遵循開放封閉原則,可能就直接會寫加法類別。但可能過了幾天,使用者說我要多加一個減法,或許就要去修改程式,要再加一個減法進去,但再過幾天,使用者又說要乘法,就要不斷的改來改去,每改一次就會增加不穩定的風險。
所以開放封閉原則,就是要求程式設計者現在如果要寫一個加法,不要直接寫一個加法,而是要先把它抽象化,變成一個運算類別,再寫一個加法類別直接 Plug-in 進去,如果日後使用者要減法,再寫一個減法 Plug-in 進去,就不會去改寫原來的加法,而是一直擴充。
LSP 是里氏替換原則(Liskov Substitution Principle),就是說父類別所設計的功能,子類別都要可以繼承,如果子類別沒辦法繼承這個功能,就不能把這個功能放在父類別裡,簡單地說就是子類別必須完全實現父類別的功能。如果鳥類是父類別,企鵝是鳥,麻雀也是鳥,所以企鵝跟麻雀都是鳥的子類別,但是企鵝因為不會飛,鳥這個父類別就不可以有飛的功能,這就是里氏替換原則。
ISP 是介面隔離原則(Interface Segregation Principle),就是不要把所有的東西都寫在一起。最淺顯的例子就是 All-in-one 電腦,一旦螢幕壞掉了,CPU、記憶體就不能使用,而一般的個人電腦,螢幕壞掉就只需要換螢幕。所以介面隔離就是希望透過介面去隔離不同的功能,不要混在一起,儘量細分。
[ 加入 CIO Taiwan 官方 LINE 與 Facebook ,與全球CIO同步獲取精華見解 ]
但細分介面的前提是,不能細到違反單一職責。以智慧型手機為例,可能要同時能夠發簡訊、收送郵件及通電話,如果這三個功能寫在一起,一旦某一個功能壞掉,其他兩個功能就不能用了。所以透過介面隔離,就要寫成三個介面,再組合成一個要用的應用,這樣就算某一個功能壞掉,也不會被影響。
DIP 是依賴倒轉原則(Dependency Inversion Principle),高階模組不應該依賴低階模組,兩者應該要依賴其抽象,抽象不要依賴細節,細節要依賴抽象,就是不要把程式碼寫死在某種實作上。
舉一個很簡單的例子,假設你告訴你的女朋友,情人節的時候要送她一束花,但當天卻因為加班到很晚,等到下班以後花店都關了,根本買不到花,你只好去超商買一盒巧克力,女朋友可能就會不高興,因為說好送花結果卻送了巧克力。但如果你只是告訴女朋友,要送給她一個神秘禮物,也就是說本來要送的花,把它依賴倒轉,抽象化成一個禮物,以後要送花、巧克力或是鑽戒都可以,反正都是神秘禮物。
依賴倒轉原則有點像前面所講的開放封閉,本來是要寫死成加法,把它抽象化以後變成運算式,再 Plug-in 加法或減法的概念。所以 SOLID 其實都在講同一件事情,就是要讓寫的程式碼可以有再用性、穩定性、擴充性及維護性更好,達成高內聚力、低耦合力的概念。
用 DCI 解決 DDD 的問題
比如說以前是用磁帶備份,因為科技的發展,現在可能要改用光碟、USB 甚至雲端備份。如果一開始寫好的程式是寫磁帶備份,每換一種設備就要改寫一次程式,但如果把它抽象化變成備份,到時候注入的是磁帶,就是磁帶備份,注入的是雲端,就是雲端備份。也就是軟體工程要有「依賴注入」的概念,所以要先用 MVC、MVVM 甚至 SOLID 的概念,先把單體解耦,再重新組合變成一個介面,這就是從單體變成微服務的關鍵,也就是說你要怎麼去讓 class 更有彈性,具有更高的延展性。
此外,由於傳統的 DDD(Domain-Driven Design)在對行為進行建模方面,存在著不足,進而導致所謂的貧血模型(Anemic Domain Model)和充血模型(Rich Domain Model)之爭。而 DCI 架構(Data、Context、Interactive)的出現,就是巧妙的解決了 DDD 充血模型中上帝類和模組間的耦合問題。
DCI 是一個物件導向的軟體架構模式,總結一句話就是物件領域(Domain Object)在不同的場景(Context)中扮演(Cast)不同的角色(Role),角色之間通過互動(Interactive)來完成具體的業務邏輯。
這種角色扮演的模型,其實現實的世界裡隨處可見。比如在電影世界裡面,常常會看到演員在某一部電影扮演英雄,也可以在另一部電影扮演反派的角色,這就是不同的演員(物件領域)依照不同的場景扮演不同的角色。
透過 DCI,就可以根據現在的場景給出相關行為,就不需要將這個角色所有的行為加注在 class 裡面。如一個人在家扮演的角色,可能只有吃飯、睡覺、滑手機;放在學校的場景,就可能要扮演學生,就要讀書、考試;如果放在公司的場景,就要扮演員工,就會有上下班、開會;放在遊樂場所就是遊玩者,要有購票遊戲的規則。
如果用 DCI 架構的概念來看現在的 HIS 系統,以員工這個角色為例,可能有行政人員,有護理人員、有醫師,有放射師、檢驗師…等等。如果員工的基本檔用 DDD 的概念來建立,勢必要把行政人員、護理人員、檢驗師、放射師、醫師的特性跟職能,都要寫在基本檔裡面。
但如果先把一個員工最基本的類別抽離出來,然後如果是行政人員,就再附加行政人員的職能跟場景,如果是醫師,就再附加醫師不同的職能及場景,護理人員再附加護理職能及場景,就不會形成 DDD 所謂的上帝類。
這其實就是延伸到前面講的里氏替換原則,就是子類別要完成繼承父類別的概念。所以這些概念都是息息相關的。當然 DCI 也不是萬能的,在行為較少的業務模型中,使用 DCI 來建模就比較不適合。
先微分解構 再積分重構
我們之所以要把單體變成微服務,是因為現在的世界,已經證實電腦再厲害,也不見得有辦法抵擋瞬間的高爆發量。也就是說,用單體做垂直整合,如果遇到雙十一購物節,系統會崩潰,遇到五倍券、遇到 COVID-19 疫苗預約,系統都一樣會崩潰,因為系統就是沒辦法承受到那麼大的高爆發量。
所以我們必須要把一些功能解耦,要從垂直整合變成水平擴充。本來的單體會把很多程式寫在一起,現在要把它分開,每一個功能就是單一職責,批價就是批價,給號就是給號,收費就是收費,也就是乾坤大挪移,從 Y 軸解構後水平擴充到 X 軸。若本來一台機器可以服務 100 個人,碰到 200 個人,就 以2台機器,300 個人就 3 台機器,如果是 1000 個人我就給他 10 台機器來服務,這種方式我稱它為螞蟻雄兵精神。也就是從Y 軸轉移到 X 軸,再延伸到 Z 軸的概念,可以讓系統很平穩的去服務高爆發量的狀況,可以確保服務可以繼續正常的維運。
要將單體重構成微服務,首先要「先用加法別用減法」。第一個步驟叫停止挖掘(Stop Digging),向外擴充,也就是原本的單體程式,現在要新加一個功能,就必須先停止挖掘,不要在這邊擴充。如果要向外擴充,就是要重新寫一個以微服務概念所設計的程式,也就是加外掛。
第二個步驟是前後端分離(Split Frontend and Backend),本來的單體是把前端、後端寫在一起,現在要把它前後分離,前端就負責前端,後端就負責後端,這個其實就是 MVC 架構的概念。
第三步驟是要做提取服務(Extract Service)。單體裡面的不同功能要重構重寫,各自產生新的微服務,再互相溝通。也就是要把單體慢慢的抽絲剝繭成新的微服務架構。
這也就是為什麼微服務可以循序漸進的去改變原本的系統。也就是說,原本的 HIS 系統並不用全部一次改寫,而是透過一套方法論的法則,先從比較重要或者比較常常出問題的地方先改寫,就可以馬上享受微服務架構的好處。
比如說掛號系統的給號功能,以前在不同平台的掛號系統,如掛號櫃台、語音、櫃員機、手機及網頁都要各寫一個給號規則,現在把掛號轉成微服務架構後,不管是在那一個平台的掛號系統,都只要呼叫給號服務就可以了,而且兩者也不會衝突,不會說那個給號方式給錯了,所以後面給的號碼都會錯,可以維持程式的穩定度。
有了微服務,個別服務不僅可以個別部署,還能夠個別擴充。因為它們能夠精確地只擴充有需要的元件,而不是像單體式應用程式需要擴充整個應用程式。
(本文授權非營利轉載,請註明出處:CIO Taiwan)