架構設計準則
傳統的 MVC(Model, View, Controller) 框架,當 Controller 收到請求之後,我們會在 Controller 內直接透過 Model 去撈取資料庫的資料,並在 Controller 做資料驗證、整合、快取、商業邏輯判斷...等等的工作。
當系統越來越大,會發現很多類似的商業邏輯的程式都散在各地,沒有辦法重複再利用,當程式需要異動或修改的時候,就要去搜尋所有程式碼,把許多相同商業邏輯的程式碼去做異動,但需要修改的地方若太多,往往會東漏西漏,導致系統出現錯誤,並造成往後開發的時間成本增加。
所以我們會想要做到減少重複的程式碼
、提高維護開發的效率
,所以將程式碼依照分類
及分層
抽出獨立控管,讓不同類型的程式專心處理自己相關的商業邏輯,讓開發維護更容易。
隨著程式架構的演進會發展出更多不同的架構,所以這個設計架構準則也是會隨著時間做演進的。
資料來源
https://laravel5-book.kejyun.com/design-pattern/design-pattern-model-coding-structure-principle.html
根據 GitBook 台灣作者 Kejyun 的 Laravel 5 學習筆記為基礎,再次調整結構與處理原則。
資料層級需求分析
Model (模型)
在 Model 中我們僅會寫對資料表對 Eloquent 相關的設定,像是primaryKey (主鍵名稱)
或table (資料表名稱)
以及常數,將一些撈取資料的邏輯都往Repository
集中整理,放在Repository
的目的是集中存取做Cache
。
注意:為了集中做Cache (快取)
所以Model(模型)
只可被Repository
呼叫。
Repository (資源庫)
在不同Service
、Crontab
或Controller
可能會需要對資料庫撈取同樣的資料,為了避免撈取資料的邏輯重複出現在不同的地方,我們會分出一層Repository (資源庫)
,將同樣撈取Model (模型)
資料的邏輯都寫在一起,供不同的Service
或table
存取。
在Repository
中,會在 function 名稱中指出這個方法是要撈什麼樣的資料,這樣方法可以重用,也可以讓程式有可讀性,不需要再去看Model
sql 條件的邏輯,判斷是在做什麼樣的處理。
e.g. PostRepository->getWeekTopPosts(); // 取得本週熱門文章
注意:呼叫單一表格資料邏輯應使用Repository (資源庫)
,跨表格該用Service (服務)
,只呼叫Model(模型)
,可被Service (服務)
、Crontab(排程)
、Controller(控制器)
、Queue(隊列)
呼叫。
ExceptionCode (例外代碼)
我們在做 API 的資料存取時,會針對不同的例外狀況回傳不同的錯誤代碼 (error_code)
,而同一個錯誤代碼可能在不同的Controller
或Service
被回傳,像是文章找不到我們會回傳錯誤代碼10000001
,為了管理及閱讀性方便,我會分出一層ExceptionCode (例外代碼)
去做共用的例外代碼管理。
像是找不到文章的代碼我會設為PostExceptionCode::POST_NOT_FOUND = 10000001;
,所以在程式中只要看到PostExceptionCode::POST_NOT_FOUND
就可以馬上知道這個是找不到文章的錯誤代碼。
假設是Service (服務)
的錯誤可以設為PostServiceExceptionCode::UNKNOW_ERROR
。
在
ExceptionCode (例外代碼)
的方法皆為靜態方法
,所以可以供任何類別去做存取。
Cache (快取)
在使用Repositoy
去對Model
做存取得時候,如果我們資料沒有做異動,我們會將資料存放在快取中,直接讀取快取的資料,而減少對資料庫的存取,提高資料的存取效率。
而我們需要對資料快取的設定、清除
及快取鍵值
去做管理,快取像是協助Repository
做資料的存取,所以我會分出一層Cache (快取)
去輔助Repository
做快取的處理。
注意:Cache (快取)
只能在Repository
使用。
Service (服務)
原本在 Controller 處理請求時,會針對使用者的請求,做跨表邏輯的處理,而同樣的處理邏輯可能會被不同的 Controller 存取,為了讓同個處理邏輯程式能夠重複使用,所以分出一層Service (服務)
,將這些處理邏輯放在裡面,供不同的 Controller 存取。
在Service
方法中,只會針對有跨表業務邏輯的資料進行蒐集與彙整,處理過後再回傳給Controller
。
注意:需要跨表格處理資料才使用Service (服務)
,Service (服務)
只呼叫Repository (資源庫)
,可被Crontab(排程)
、Controller(控制器)
、Queue(隊列)
呼叫。
Concrete (服務組合)
可以看做是Service(服務)
整合層,會在Controller
去呼叫各個不同的Service(服務)
去做資料的判斷處理。
有時候不同的Controller
會有相同呼叫Service
的邏輯順序及組合的資料,為了減少重複程式,所以分出一層Concrete (服務組合)
去協助Controller
去做不同Service
資料的整合撈取。
在許多Controller
方法中,我們使用$PostService->find($post_id);
去找文章資料,並用$UserService->find($post_user_id);
去找文章作者相關資訊,這些在Controller
重複出現兩次以上呼叫Service
邏輯的部分,我們就可以寫在Concrete
中去做呼叫。
注意:Concrete (服務組合)
只呼叫至少兩個Service (服務)
,可被Crontab(排程)
、Controller(控制器)
、Queue(隊列)
呼叫。
Validator (驗證器)
為了讓資料驗證方法能夠重複使用,不需要在不同的地方去驗證相同的東西,這樣會造成驗證邏輯重複出現,若有需要異動驗證規則時,會難以維護。所以沒有選擇將這些驗證的邏輯寫在Controller
、Services
、Repository
或Model
中。而是特別分出一層Validator (驗證器)
輔助Checker(檢查器)
做資料驗證。
注意:Validator (驗證)
只檢查單一表格資料內容,可被Service(服務)
、Crontab(排程)
、Queue(隊列)
、Controller(控制器)
或Concrete(組合服務)
呼叫。
Checker (檢查器)
每個商業邏輯需要驗證的資料不同,有些欄位在不同商業邏輯會有必填與非必填不同的差異,像是在做使用者身份驗證時,若有 Email 驗證及手機驗證,在 Email 驗證時,手機欄位為非必填欄位,在做手機驗證時, Email 為非必填欄位,但兩者皆為使用者的資料,無法強制使用者兩個欄位資料皆為必填,但在某些商業頁邏輯是必要的,但基本的資料驗證規則還是一樣,像是資料最長長度、email 格式...等等。
所以為了能夠重複使用共用的驗證器規則,所以建構了一個Checker(檢查器)
的結構,去呼叫不同的Validator
去檢查資料,像是同一個 Controller 處理的資料可能含有「會員」、「商品」資料,所以同時需要呼叫會員驗證器(UserValidator)
及商品驗證器(GoodsValidator)
的驗證規則去驗證資料,所以就透過Checker(檢查器)
去呼叫不同的驗證器,來達到驗證不同資料的功能,輔助 Controller 做資料驗證。
注意:Checker (檢查器)
只檢查多表格資料內容,可被Service(服務)
、Crontab(排程)
、Queue(隊列)
或Controller(控制器)
呼叫。
Support (輔助)
做共用的且不包含任何資料邏輯處理的靜態輔助方法。
像是我們可以用GoogleAnalyticSupport::api()
去對 GA 的 API 做存取。
或是使用 StringSupport::generate('32')
產生UUID。
在Support(輔助)的方法皆為
靜態方法
,所以可供任何類別去做存取。
Controller (控制器)
組合底層的所有方法進行控制,處理後回吐資料,每個 function 都使用 try catch 處理錯誤去捕獲底層傳上來的錯誤訊息。
注意:為了集中做Cache (快取)
,所以跟Controller(控制器)
同層級的禁止直接呼叫Model(模型)
。
Queue(隊列)
層級上接近Controller(控制器)
,但是在低一點,大多用來處理非即時性資料,降低伺服器負荷。
Crontab(排程)
層級上同Controller(控制器)
,大多用來固定時間檢查、處理錯誤或產生資料。
Command(指令)
層級上同Controller(控制器)
,大多用來當有需求時,手動檢查處理錯誤或產生資料。