Model 模型
最后更新于:2022-04-01 23:12:50
# Model 模型設計模式
我們在使用任何的 Framework 中,都會聽到 MVC 模型,V(View)是負責畫面顯示,C(Controller)是負責控制程式呼叫模型的邏輯,而最重要的 M(Model)是負責整個資料庫的操作,以及撈取資料的邏輯
我們常常把模型用來作為處理資料的商業邏輯,不管是任何的「資料樣式的轉換」、「資料撈取的邏輯」、「資料格式的驗證」、「資料處理的順序及商業邏輯」...等等都是放在模型(Model)去處理
**_資料樣式的轉換_**
~~~
// 2016-01-01 00:00:00.123789
$now = Carbon::now();
// 2016/01/01
$now_date = $now->format('Y/m/d');
~~~
**_資料撈取的邏輯_**
_撈取所有的女會員資料,年紀小於 30 歲_
~~~
User::where('gender'=>'female')
->where('age', '<', 30)
->get();
~~~
_撈取所有的男會員資料,年紀大於 30 歲_
~~~
User::where('gender'=>'male')
->where('age', '>', 30)
->get();
~~~
_資料格式的驗證_
~~~
$validator = Validator::make(Request::all(), [
'title' => 'required|unique:posts|max:255',
'content' => 'required',
]);
~~~
_資料處理的順序及商業邏輯_
~~~
/**
* 發送 Email 及簡訊給所有女會員
*/
// 取得所有女會員資料
$users = User::where('gender'=>'female')
->get();
// 發送 Email
foreach ($users as $u) {
Mail::send('emails.hello', ['user' => $u], function ($mail) use ($u) {
$mail->to($u->email, $u->name)
->subject('安安!');
});
}
// 發送簡訊
foreach ($users as $u) {
SMS::send('sms.hello', ['user' => $u], function ($sms) use ($u) {
$sms->to($u->mobile_phone, $u->name)
->content('安安!');
});
}
~~~
如果把這些不同類別的資料全部丟到 Model 模型去處理會變得很亂,程式碼難以維護,所以我們會用設計模式來降低程式碼的耦合性,讓程式變得容易維護,我們會將 Model 分成:
- 實體(Entity)
- 資源庫(Repository)
- 服務(Service)
- 表單驗證(Form)
- 資料呈現(Presenter)
- ARCA 架構檔案結構
### 實體(Entity)
實體就是我們用來設定 Eloquent Model 的相關設定,像是資料表名稱($table)、主鍵名稱($primaryKey) 等等,裡面除了 Eloquent 相關設定以外,**_不要擺任何的商業邏輯或資料撈取方法_**
_實體與資料表的關係是「1 對 1」的關係,有幾個資料表就有幾個實體_
> 詳情請見[Eloquent Model (模型) - 設定](#)
### 資源庫(Repository)
資源庫是我們要用來撈取資料表資料的各個邏輯,我們資料表會有不同的欄位,不同的欄位條件代表不同的意義,像是:
_撈取所有的女會員資料,年紀小於 30 歲_
~~~
User::where('gender'=>'female')
->where('age', '<', 30)
->get();
~~~
_撈取所有的男會員資料,年紀大於 30 歲_
~~~
User::where('gender'=>'male')
->where('age', '>', 30)
->get();
~~~
這些不同的撈取資料邏輯,我會將它包在資源庫中,該資源庫長得會像這樣:
~~~
class UserRepository {
/**
* 撈取所有的女會員資料,年紀小於 30 歲
*/
public function getYoungFemale()
{
return User::where('gender'=>'female')
->where('age', '<', 30)
->get();
}
/**
* 撈取所有的男會員資料,年紀大於 30 歲
*/
public function getOldMale()
{
return User::where('gender'=>'male')
->where('age', '>', 30)
->get();
}
}
~~~
這樣我們撈取這些不同資料邏輯時就可以這樣去撈取:
~~~
$userRepository = new UserRepository();
// 撈取所有的女會員資料,年紀小於 30 歲
$young_female_user = $userRepository->getYoungFemale();
// 撈取所有的男會員資料,年紀大於 30 歲
$old_male_user = $userRepository->getOldMale();
~~~
這樣除了可以讓程式碼易讀性提高之外,撈取資料的邏輯也可以抽離出來,下次如果有需要撈取同樣的資料時,就可以重複的去使用它,而且不會有重複的程式碼出現在專案的各個地方,讓管理程式碼變得簡單
_資源庫與實體的關係是「1 對 1」的關係,有幾個實體就有幾個資源庫,每個資源庫是代表那個實體的各個不同的資料撈取邏輯_
### 服務(Service)
服務代表我們程式要處理資料的商業邏輯,我會將各個功能邏輯獨立成一個服務,像是使用者「註冊身份驗證」是一個服務,而使用者「個人隱私設定」也是一個服務
_服務與資源庫的關係是「多 對 1」的關係,像是同樣使用者資料,有「註冊身份驗證」及「個人隱私設定」2 種不同類型的服務_
**_使用者「註冊身份驗證」服務_**
~~~
/**
* 使用者「註冊身份驗證」服務
*/
class UserAuthService
{
/**
* 註冊
*/
public function signup()
{
}
/**
* 登入驗證
*/
public function signin()
{
}
}
~~~
**_使用者「個人隱私設定」服務_**
~~~
/**
* 使用者「個人隱私設定」服務
*/
class UserPrivacyService extends AnotherClass
{
/**
* 取得使用者隱私設定
*/
public function getUserPrivacy()
{
}
/**
* 設定使用者隱私
*/
public function setUserPrivacy()
{
}
}
~~~
不同類型的服務,只要彼此耦合性很低,我傾向把他分成不同的服務去處理,這樣可以很清楚的知道哪個個服務是專門處理哪一種商業邏輯,程式也比較好管理,在異動程式時也比較不會影響到彼此,避免牽一髮動全身的狀況發生
### 表單驗證(Form)
我們設計後端程式的原則,是不要相信任何第三方傳來的資料,在資料做進一步處理時都需要對資料格式做檢查,若於我們設定的資料格式相符,我們才會去做進一步的資料商業邏輯處理
但是我們可能會在控制器(Controller)做表單資料的驗證,但是服務(Service)、資源庫(Repository)或實體(Entity)為了保護自己的程式邏輯,也有可能去做表單資料的驗證,若每一個階段都做表單資料的驗證,這樣不僅造成了資料發生重複驗證的狀況,也會降低程式的執行速度,更慘的是會造成驗證程式重複出現,如果有驗證規則要修改,我們就必須要確保所有有驗證表單資料的地方,都有正確的被修改,不然程式的商業邏輯可能會沒辦法順利的去執行。
因為我們對模型做了分層地處理,所以模型的層級架構會像:
> 控制器 (Controller) > 服務(Service) > 資源庫(Repository) > 實體(Entity)
控制器會根據他需要的商業邏輯,呼叫不同的服務來處理他的程式邏輯,而且每個控制器,而且每個控制器可能會有不同類型的服務,可能會有使用者(User)的資料、文章(Posts)的資料...等等需要做資料的驗證,所以驗證資料的規則複雜度會很多。
我自己會傾向將所有的表單資料驗證都放在服務(Service)中去驗證,不同的商業邏輯可能需要驗證的資料規則不同,但是我們可以確定的是,同一個服務會是同一個類型的資料,像是_使用者「註冊身份驗證」服務_及_使用者「個人隱私設定」服務_<裡面的資料一定是使用者相關的資料,若我們也有_文章的服務(PostService)_,我們也一定可以確保裡面的驗證資料一定是文章相關的資料。
所以除了服務(Service)層去做資料的驗證外,其他的層級都不需要做任何的資料驗證!
### 資料呈現(Presenter)
我們會將需要處理不同資料樣式的邏輯,使用 [laracasts/presenter](https://github.com/laracasts/Presenter) 去做實體(Entity)的分層處理,不要將有程式邏輯的功能出現在實體(Entity)中
### ARCA 架構檔案結構
我會將 Model 的檔案結構依照 Domain 去區分,檔案結構大概會像這樣
~~~
/app
/KeJyunApp
/User
/Entities
User.php
UserPrivacy.php
/Repositories
UserRepository.php
UserPrivacyRepository.php
/Service
UserAuthService.php
UserPrivacyService.php
/Form
UserForm.php
UserPrivacyForm.php
/Presenter
UserPresenter.php
UserPrivacyPresenter.php
/Post
/Entities
Post.php
/Repositories
/Service
/Form
/Presenter
~~~
這樣區分的好處是,類似功能的程式可以方便集中管理,當我們在撰寫某一功能的程式,我們可以很快地在同一個資料夾中找到這些檔案,若要找其他功能的程式時,也可以在同一個資料夾很快地去找到
如果我們將程式檔案依照功能去放置,可能會像這樣
~~~
/app
/SomeApp
/Entities
User.php
Post.php
Tags.php
News.php
Event.php
...
/Repositories
/Service
UserAuthService.php
UserPrivacyService.php
UserStatisticService.php
PostManageService.php
PostRankService.php
PostStatisticService.php
TagsService.php
NewsService.php
EventService.php
...
/Form
/Presenter
~~~
當專案還小,只有少數幾個模型資料需要管理時,還沒有什麼大的問題,但是當我們撰寫很多複雜功能時,這樣檔案管理的方式會是個很大的夢靨,像是服務(Service)與資源庫(Repository)的關係是「多 對 1」的關係,所以服務(Services)資料夾的檔案可能有 40~50 個以上,在我們要找相關的檔案時,就很考驗我們的眼力了(工程師的眼睛是很珍貴的,我們要好好的珍惜~)
### 參考資料
- [在 Laravel 4 使用資源庫 (Repositories) 及服務 (Services) 去降低程式的耦合性](http://laravel4-book.kejyun.com/laravel-design-pattern/model/decoupling-your-code-in-laravel-using-repositiories-and-services.html)
- [胖胖Model減重的五個方法 by howtomakeaturn](http://slides.com/howtomakeaturn/model#/)
- [PHP 也有 Day #16 - 胖胖 Model 減重的五個方法 by 尤川豪](https://www.youtube.com/watch?v=e0qVLniXbHw)
';