面對現實吧!維護大型 PHP 應用程式不簡單!

JokerLinly發表於2017-05-05

file

(如果你很著急,可以直接跳到 解決辦法 ~)

我們都知道 Laravel 是迄今為止最受歡迎的 PHP 框架。 它的目錄結構好、組織有序、定義簡單。當我們在一箇中小型專案工作時,使用 Laravel 提供目錄結構是非常友好的。 但是,當它開始成為一個超過 50 個模型的大型應用程式時,我們就已經是一腳踩進自己埋的坑裡面了。且再難回頭!

維護一個大的應用程式真的不是開玩笑的,它是需要好好地被思考和設計。而 Laravel 的預設的目錄結構對於這種情況明顯是心有餘而力不足的。
首先,我們可以來看看 Laravel 預設的目錄成為大型應用程式產生的變化。
Laravel 預設的目錄結構就像這樣:

|- app/
   |- Console/
      |- Commands/
   |- Events/
   |- Exceptions/
   |- Http/
      |- Controllers/
      |- Middleware/
   |- Jobs/
   |- Listeners/
   |- Providers/
   |- User.php
|- database/
   |- factories/
   |- migrations/
   |- seeders
|- config/
|- routes/
|- resources/
   |- assets/
   |- lang/
   |- views/

這樣的目錄結構設計沒有任何問題。 而當我們的業務邏輯稍微複雜一點時,我們通常會用 Repositories、Transformers 等這些個資料夾來劃分。 跟下面這個差不多:

|- app/
   |- Console/
      |- Commands/
   |- Events/
   |- Exceptions/
   |- Http/
      |- Controllers/
      |- Middleware/
   |- Jobs/
   |- Listeners/
   |- Models/
   |- Presenters/
   |- Providers/
   |- Repositories/
   |- Services/
   |- Transformers/
   |- Validators/
|- database/
   |- factories/
   |- migrations/
   |- seeders
|- config/
|- routes/
|- resources/
   |- assets/
   |- lang/
   |- views/

這顯然是一個目錄結構設計友好的 Laravel 專案。 看看 Models 資料夾裡面:

|- app/
  |- Models/
     |- User.php
     |- Role.php
     |- Permission.php
     |- Merchant.php
     |- Store.php
     |- Product.php
     |- Category.php
     |- Tag.php
     |- Client.php
     |- Delivery.php
     |- Invoice.php
     |- Wallet.php
     |- Payment.php
     |- Report.php

看上去也不是那麼糟糕對吧!這裡面也建立了一個資料夾 Services 專門處理所有的業務邏輯。還有 Repositories、Transformers、Validators 這些有著差不多相同的類的資料夾。很多人也覺得這樣的設計很不錯,並且樂於這樣去設計。不過完成僅僅只是單個實體/模型的工作需要瀏覽不同的資料夾和檔案,即操作很多類,寫各種介面,光是這一點讓一些開發者覺得很麻煩。

但問題的關鍵不在於用不同的資料夾去劃分,而是開發者維護程式碼和服務之間的通訊。

分析下前面的程式碼結構可以看到:

  • 這是一個 龐大 的應用程式
  • 對於一些開發人員來說,很難 維護
  • 生產力 低(在考慮系統內部連線上需要耗費時間)
  • 程式碼的 規模調整 也是一個問題

解決方案是顯而易見的 —— 微服務。即使我們使用 SOA(面向服務架構),我們還是需要將我們的龐大的應用分解成較小的獨立的部分,以便日後將其分開擴充套件。照理來說這個解決方法挺好的。但現實中我們並沒有這麼做。因為對於將程式碼分解成更小的部分這件事情,說的通常比做的要容易得多得多~

通常分離服務需要兩個簡單的步驟:

  • 將子服務(Models、Repositories、Transformers等)移動到新的 PHP 微服務應用程式中
  • 重新確認服務函式呼叫的目標確確實實指向到新的微服務中(例如,建立 HTTP 請求)

然後你需要查詢與該服務相關的所有檔案。你可能會很驚奇地發現,我們可能有部分程式碼不經過服務而直接使用了它的模型或儲存庫(我真的幹過這種事)。而這還不是唯一的問題,我們甚至可以總結一下:

  • 有太多的檔案要考慮
  • 犯錯誤的機會很高
  • 容易讓開發者沮喪
  • 有時需要重新考慮域名之間的邏輯
  • 新的開發者?還是洗洗睡吧!

最後一個原因非常重要,因為新的開發人員很難在短時間內掌握整個應用程式。而通常專案經理不會給他太多的時間去研究。這容易產生給內建物件擴充套件方法、程式碼放在錯誤的位置、甚至會讓下一個新的開發人員更加困惑等問題。

幸運的是,我們已經有一個解決方案 —— HMVC。即將整個應用程式分成較小的部分,每個部分都有自己的檔案和資料夾,如 app/ 資料夾,並通過 composer.json 自動載入,如下所示:

|- auth/
   |- Exceptions/
   |- Http/
   |- Listeners/
   |- Models/
   |- Presenters/
   |- Providers/
   |- Repositories/
   |- Services/
   |- Transformers/
   |- Validators/
|- merchant/
   |- Console/
   |- Events/
   |- Exceptions/
   |- Http/
   |- Jobs/
   |- Listeners/
   |- Models/
   |- Presenters/
   |- Providers/
   |- Repositories/
   |- Services/
   |- Transformers/
   |- Validators/
|- database/
   |- factories/
   |- migrations/
   |- seeders
|- config/
|- routes/
|- resources/
   |- assets/
   |- lang/
   |- views/

但當我們想要將特定模組移動到微服務中時,HMVC 讓事情變得複雜起來,因為我們還是需要在主要程式碼庫中保留控制器、中介軟體等。大多數時候,將程式碼移動到微服務會需要重新定義路由和控制器。這裡面有太多不必要的工作,除開我很懶這個理由,正常的開發者都只想分開那些不得不分開的東西。因此我不是很推崇這種目錄結構。

領域驅動設計也可以是一個解決方案

沒有完美的解決方案,凡事都有兩面性,還是要看每個人偏好。我們不會在這裡討論領域驅動設計(DDD),但總的來說,DDD (可能)將你的 Laravel 應用程式構建為4部分:

  • 應用程式(Application) —— 掌管 控制器、中介軟體、路由
  • 領域(Domain)—— 掌管業務邏輯 Model、Repository、Transform、Policy 等
  • 基礎設施(Infrastructure) —— 掌管 Logging、Email 等常見服務
  • 介面(Interface) —— 掌管 View 、lang、assets

這看起來很容易,那麼為什麼我們不這樣構建我們的應用程式,並使用名稱空間?

|- app/
   |- Http/ (Application)
   |- Controllers/
   |- Middleware/
|- Domain/
   |- Models/
   |- Repositories/
   |- Presenters/
   |- Transformers/
   |- Validators/
   |- Services/
|- Infrastructure/
   |- Console/
   |- Exceptions/
   |- Providers/
   |- Events/
   |- Jobs/
   |- Listeners/
|- resources/ (Interface)
   |- assets/
   |- lang/
   |- views/
|- routes/
   |- api.php
   |- web.php

因為將專案分割成資料夾是不行的。 這僅僅只是意味著我們只新增了一個父名稱空間。

真理的時刻

你能看到這裡也是挺了不起的了,畢竟好像哪個方法都行不通。我們還是來好好聊一聊真的解決方案吧!看看下面的目錄結構:

|- app/
   |- Http/
      |- Controllers/
      |- Middleware/
   |- Providers/
   |- Account/
      |- Console/
      |- Exceptions/
      |- Events/
      |- Jobs/
      |- Listeners/
      |- Models/
         |- User.php
         |- Role.php
         |- Permission.php
      |- Repositories/
      |- Presenters/
      |- Transformers/
      |- Validators/
      |- Auth.php
      |- Acl.php
   |- Merchant/
   |- Payment/
   |- Invoice/
|- resources/
|- routes/

Auth.php and Acl.phpapp/Account/ 資料夾中的服務檔案。 控制器只能訪問這兩個類並呼叫它們的方法。 其他類永遠不會知道 app/Account/ 資料夾中其他剩下的類。 這些服務中的方法將僅接收基本的 PHP 資料型別,例如 array、string、int、bool 和 POPO(Plain Old PHP Object),但沒有類例項。 示例 :

...
public function register(array $attr) {
    ...
}
public function login(array $credentials) {
    ... 
}
public function logout() {
    ...
}
...

這裡要注意一個地方,register 函式接收一個陣列屬性的引數,而不是 User 物件。 這個點很重要,因為讓其他類去呼叫該函式的時,不應該讓這個函式知道 User 模型的存在。這是這整個結構你必須要遵守的基本規則。

當我們想分開程式碼

我們的應用程式變得越來越大時,我們希望將 Account 相關的內容分開到單獨的微服務中並將其轉換為 OAuth 伺服器。
所以,我們只是需要移動以下部分

|- Account/
   |- Console/
   |- Exceptions/
   |- Events/
   |- Jobs/
   |- Listeners/
   |- Models/
      |- User.php
      |- Role.php
      |- Permission.php
   |- Repositories/
   |- Presenters/
   |- Transformers/
   |- Validators/
   |- Auth.php
   |- Acl.php

如果是將應用程式搬到 Lumen:

|- app/
   |- Http/
      |- Controllers/
      | - Middleware/
   |- Account/
      |- Events/
      |- Jobs/
      |- Listeners/
      |- Models/
         |- User.php
         |- Role.php
         |- Permission.php
      |- Repositories/
      |- Presenters/
      |- Transformers/
      |- Validators/
      |- Auth.php
      |- Acl.php
|- routes/
|- resources/

不可避免地我們必須在控制器和路由中編寫程式碼,因為我們需要使其成為一個 OAuth 伺服器。

那麼接下來我們需要在主要程式碼庫中做什麼改變?

我們只需要保留服務檔案 Auth.phpAcl.php,並將其方法中的程式碼更改為針對新建立的微服務的 HTTP 請求(或其他資訊傳遞方式)。

...
public function login(array $credentials) {
 // change the code here
}
...

整個應用程式將保持不變。 而應用程式的目錄結構將如下所示:

|- app/
   |- Console/
   |- Exceptions/
   |- Http/
      |- Controllers/
      |- Middleware/
   |- Providers/
   |- Account/
      |- Auth.php
      |- Acl.php
   |- Merchant/
   |- Payment/
   |- Invoice/
|- resources/
|- routes/

這個方法只做了很少的事情,就將部分程式碼移動到完全獨立的微服務中。(反正我暫時是想不到更好的方法了)

權衡

正如上面所說的,任何事情都有一個權衡點,對於這個解決方案來說也一樣。而且,這裡我們有個關於 遷移 的問題!因為在上述資料夾結構(分離之前)所有的遷移檔案都是在 database/migrations/ 目錄中。但是,當我們要分離一個域時,我們需要確定並移動該域的遷移。這件事情有點難辦,因為我們沒有明確指出哪個遷移屬於哪個域。我們可能需要研究如何將識別符號放在遷移檔案中。

該識別符號可以是域字首。例如,我們可以命名遷移檔案 xxxxxxxxx_create_account_users_table.php 而不是 xxxxxxxxx_create_users_table.php。如果我們想要,我們也可以使用 account_users 表名替換 user。我更喜歡在分離過程中識別哪些表要移動。分離遷移檔案可能有點令人沮喪,但是如果我們使用字首或任何型別的標記,那麼這整個過程肯定會變得不那麼痛苦。

我還在計劃嘗試構建一個 Laravel 軟體包的結構,提供 artisan 命令來自動執行檔案生成和分離過程。完成後,我會新增包連結分享出來。

在此之前,如果有更好的意見請分享,我需要你的幫助才能找到最佳解決方案。

週五了,週末愉快:beers:

本文翻譯改編自 Tawsif AqibLarge Scale Laravel Application

本作品採用《CC 協議》,轉載必須註明作者和本文連結

Stay Hungry, Stay Foolish.

相關文章