0%

MVC與MTV框架的重構

前言

當我們在寫Laravel, Django 等MVC以及MTV的架構時,部分的人可能都會習慣把所有的邏輯,包含Use Case, 資料驗證 等等全部寫在同一包裡面

一開始或許開發的時候比較方便,你自己在自幹的時候想怎麼寫都沒差

但換個角度,當今天這個專案是10個人一起做的呢?甚至是過了10個月你再回來看,我們要花多少時間來看這些架構

再來,當我們今天要新增一個功能或是更改 Use Case 時,我們又要花多少時間來做閱讀?

其實在看過Clean Code, Clean Coder 和 Clean Architecture之後其實就會知道,傳統的MVC框架真的滿髒亂而且不容易維護的

一般MVC架構

看個範例,以註冊會員為例子好了,假設我們帶入member_name, member_email, member_password

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MemberController extends Controller
{
public function store(Request $request)
{
// 驗證請求參數
$validatedData = $request->validate([
'member_name' => 'required|string|min:1|max:60',
'member_email' => 'required|string|email|max:255|unique:members', // 確保email在資料庫中唯一
'member_password' => 'required|string|max:255',
]);

// 在資料庫中建立會員資料
try {
$member = Member::create([
'member_name' => $validatedData['member_name'],
'member_email' => $validatedData['member_email'],
'member_password' => $validatedData['member_password'],
]);

// 成功建立資料,回傳201 Created狀態碼
return response()->json(['message' => '會員資料建立成功'], 201);
} catch (\Exception $e) {
// 發生錯誤,回傳錯誤訊息
return response()->json(['message' => '會員資料建立失敗'], 500);
}
}
}

光是簡單的驗證和建立資料庫就已經有點長度了,如果我今天還要再加入更多功能進來,例如要付費才能建立會員,所以要使用金流,或是什麼註冊會員送好禮,所以又要使用物流等等的功能。

一開始可能還好,但不要忘了,隨著軟體生命週期的增長,維護的成本是會越來越高的。更不用說多數的人希望有新的功能加進自己的軟體裡面。

到時候一個新進來的人要花上多少時間來熟悉這些Code,且維護又要花上多少時間?

這個架構甚至連單元測試都測不了,出問題要從哪邊找起搞不好都是一個問題。

更不用說還有一堆公司會為了追求快速開發而把前後端的Code寫在同一包檔案裡面,到時候要加一個新功能都有很有可能要花很大的時間去修改和Study

甚至這不是你寫的Code,還要花多少時間來讀懂作者的邏輯?

軟體的維護

隨著時間時間增長,可能會有新增加的功能,或是配合市場要調整的部分,此時架構或是Code如果不夠乾淨,那維護的時間將是隨著指數增長

我們引用Clean Code這本書中的資料
80年代某不願具名軟體公司的內部統計資料:

工程人員的增長 同一時期的生產力 每行程式碼的平均成本

我們可以發現幾件事情

  • 工程人員持續且倍數的增長
  • 每行程式碼的平均成本也是持續且倍數的增長
  • 但生產力自從第三年之後就沒有增長多少,後續甚至是幾乎持平的

「當對程式碼的整潔程度或設計的結構沒有多少想法時,那你就會跟這條曲線一樣走到最終悲慘的結局」
取自: Clean Architecture (p.6)

再舉一個極端的例子

「我知道有一間公司在 80 年代後期開發了一個殺手級應用,但後來發行的週期開始拖長,程式裡的錯誤也無法在下次發行之前修復,程式載入的時間與崩潰機率也愈來愈長和高。不久,這家公司就倒閉了。我問他當時發生了什麼…」
「急於將產品上市,導致他們的程式碼變得一團糟,當他們加入愈來愈多的產品特點時,程式碼就變得愈來愈糟糕,一直到他們再也無法管理這團混亂。劣質的程式碼導致了這家公司的倒閉」
取自: Clean Code (p.3)

如果你問我說,「我的主管要求我一定要在三天內產出這個專案,那我該怎麼辦?」
我建議你先去看一下Clean Coder,上面有很多身為一個專業人士該做的事情和應對方式。
如果主管還是不合理的要求你,那我只好搬出那句話,程式跟人只要有一個能跑就好

隨時準備好可以跳槽的準備。

實作

傳統 MVC 和 MTV

我們先來看看傳統 MVC 或是 MTV 架構的樣子

其實滿簡單的,就是Model負責與資料庫有關的操作,Controller和其他兩層溝通,View負責畫面顯示
以傳統的MVC來說,甚至會有前後端不分離也就是伺服器端渲染(Server-Side Rendering,簡稱 SSR)的狀況
但這個模式到現在,隨著前端技術的進步,以及開始重視團隊協作的現在,過去的 SSR 其實顯得有非常多的問題,像是開發效率、維護性等等。

現代的MVC可能連View那一層都不會有了(後端的部分),因為View的部分已經給了前端

調整過後的架構

這邊我從bxcodecgo-clean-arch得到很大的靈感
以上方的 註冊會員 為例子,我們需要驗證、儲存資料、金流和物流以及回傳資料
我們可以得到這張圖

搭配一下時序圖來了解狀況

我們可以得知這個順序是

  1. Request 進入 Controller
  2. Controller 向 Validator 驗證使用者的資料
  3. Controller 向 UseCase 傳遞資料(處理業務邏輯,像是金流物流)
  4. Controller 向 Repository 請求建立資料
  5. Repository 向 Model 請求建立資料
  6. Model 向 DB 建立資料
  7. Controller 向 Formatter 請求回傳資料
  8. Controller 給出 Response

也就是說,我們這邊盡量讓Controller只做資料傳遞的動作,不處理其他的業務邏輯
Code大概會長這樣(這邊先不講DI的部分,有時間這個移到鐵人賽再說XD)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class MemberController extends Controller
{
protected $member_validator;
protected $member_use_case;
protected $member_repository;
protected $member_formatter;

public __construct(MemberValidator $member_validator, MemberUseCase $member_use_case, MemberRepository $member_repository, MemberFormatter $member_formatter)
{
$this->member_validator = $member_validator;
$this->member_use_case = $member_use_case;
$this->member_repository = $member_repository;
$this->member_formatter = $member_formatter;
}

public function registerMember(Request $request)
{
$request_data = $request->data;

// 驗證請求參數
$error_key_group = $this->member_validator->getRegisterMemberErrorKeyGroup($request->data);
if (count(error_key_group) != 0) {
$validate_fail_response_data = $this->$member_formatter->getValidateFailResponseData($error_key_group)
return response()->json($validate_fail_response_data, 400)
}

// 金流和物流
if (!$this->member_use_case->isPaySuccessful($request_data)) {
$pay_fail_response_data = $this->$member_formatter->getPayFailResponseData()
return response()->json($pay_fail_response_data, 400)
}

if (!$this->member_use_case->isGiftSuccessful($request_data)) {
$gift_fail_response_data = $this->$member_formatter->getGiftFailResponseData()
return response()->json($gift_fail_response_data, 400)
}

// 建立資料
try {
$this->member_repository->createMember($request_data);
} catch (\Exception $e) {
$create_data_fail_response_data = $this->$member_formatter->getCreateDataFailResponseData();
return response()->json($create_data_fail_response_data, 400);
}

$success_response_data = $this->$member_formatter->getSuccessResponseData();
return response()->json($success_response_data, 201);
}
}

這麼做最大的好處有幾個

  1. 我可以撰寫單元測試,來驗證每一個function的邏輯是否正確
  2. 以後要加什麼功能,我可以很清楚知道我要在哪裡做
  3. 他不再是互相依賴的關係,而是有需要才會使用到

結語

如果我真的要把裡面用到的概念全部講完,大概要好幾篇文章
這個留到鐵人賽再來說吧,如果我有參賽,沒意外應該是會講Laravel的開發流程,且不走傳統的方式,但可惜我對前端不熟,我不太會寫 HTML,可能我也不太會花時間去講太多 Blade 的東西
畢竟我也滿討厭把前端跟後端的邏輯寫在一起的。

參考資料

Day 01: 【序】– 架構與設計、代碼、工程師
打造 Laravel 優美架構
DAY6 - 你的 Backend 可以更有彈性一點 - Clean Architecture 概念篇
go-clean-arch
Clean Code
Clean Coder
Clean Architecture


如果有錯誤的部份,歡迎指正,謝謝。
如果你喜歡這篇文章,請幫我拍手
只需要註冊會員就可以囉,完全不用花費任何一毛錢就可以用來鼓裡創作者囉