貧血模型跟充血模型

軟體的「貧血模型」(Anemic Domain Model)和「充血模型」(Rich Domain Model)是兩種在軟體設計中常見的模型設計風格,特別是在使用面向對象編程(Object-Oriented Programming,簡稱OOP)時,這兩種模型風格對於如何處理領域邏輯(Domain Logic)有著不同的理念。 DDD 本身就是著名的充血模型實作,而分散式拆分則是為貧血模型,以下將會細節說明。 貧血模型(Anemic Domain Model): 在貧血模型中,對象(Object)通常只是包含資料的純資料結構,並且缺乏有效的行為(方法)。這種模型將領域邏輯(Domain Logic)主要放在服務(Service)或管理類別中,而不是在對象本身。這意味著對象只是資料的容器,而所有處理邏輯都放在外部。 貧血模型的優點是簡單且易於理解,因為領域邏輯都放在一個中央位置,容易進行修改和維護。然而,它也有一些缺點。例如,當領域邏輯變得複雜時,服務類別可能會變得過度龐大,造成程式碼不易維護和測試。此外,貧血模型也未完全利用面向對象編程的優點,如封裝和多型性。 充血模型(Rich Domain Model): 相比之下,充血模型是一種較為嚴格的面向對象設計風格,將領域邏輯嵌入到對象中。這意味著對象不僅包含資料,還包含了處理資料的相關行為和邏輯。這使得對象能夠自主管理自己的狀態和行為,更符合真實世界中物件的行為。 充血模型的優點是更好地利用了面向對象編程的特性,更容易理解,也更符合物件導向的設計原則,如封裝和單一職責原則。此外,由於領域邏輯分散在不同的對象中,這使得程式碼更容易擴展和維護。 然而,充血模型可能在某些情況下增加了複雜性。當領域邏輯變得非常複雜時,對象之間的交互可能變得複雜,需要更深入的設計和理解。 在選擇使用貧血模型還是充血模型時,開發者需要考慮項目的需求、複雜性和可擴展性等因素,以及團隊成員對於這兩種設計風格的熟悉程度。 當談到「貧血模型」和「充血模型」,這兩種模型風格涉及領域邏輯的處理方式。 貧血模型(Anemic Domain Model)範例: 在貧血模型中,對象只是包含資料的純資料結構,而大部分的領域邏輯則放在服務(Service)類別中。 // User.php (Entity) class User { private $id; private $name; private $email; // Getter and setter methods... } // UserService.php (Service) class UserService { public function sendWelcomeEmail(User $user) { // Send a welcome email to the user... } public function generateUsername(User $user) { // Generate a unique username based on user's name and ID....

August 22, 2023 · Yish

Laravel Precognition

如果要提供給前端作即時表單驗證時,傳統作法是要在提供一組 API 作為驗證返回使用,Laravel Precognition 這個新的 Laravel 套件提供了較為方便的即時驗證,並且不會讓驗證規則同時存在兩份,也與前端框架作了整合,以下將以註冊功能添即時驗證為範例。 前置 配置 Breeze $ laravel new laravel10 $ cd laravel10 $ composer require laravel/breeze --dev $ php artisan breeze:install $ php artisan migrate $ npm install $ npm run dev 原本規則 預設配置好之後,原本的規則寫法是送表單型態,也就是說在送出表單當下才會去驗證欄位機制,這邊如果要調整成即時作表單驗證會有兩種作法: 前端取得後端驗證規則後以 JS 再寫入一次規則 後端提供驗證 API 操作 這邊可以看到前端寫規則的話當需要作欄位驗證調整時就得再調整,規則分散在兩邊; 而後端自行提供驗證 API 操作需要經過一定設計才會讓代碼不會有重複的狀況。 Laravel Precognition 在先前版本中提供了一種新的工具 Precognition 就是用來解決這個煩惱的。 routes/auth.php 添加中間件作為驗證機制 Route::post('register', [RegisteredUserController::class, 'store'])->middleware([HandlePrecognitiveRequests::class]); Http/Controllers/Auth/RegisteredUserController.php 將 Validation rule 複製 創建 RegisterRequest $ php artisan make:request RegisterRequest 刪除 authorize 或是改為 true 添加規則,用於判斷送入與即時驗證 precognitive 要驗證什麼,我這邊僅先用 unique 作範例 return [ 'name' => ['required', 'string', 'max:255'], 'email' => [ // 判斷成立 -> 展開陣列到上層陣列 ....

May 25, 2023 · Yish

探索 Laravel 核心機制與生命週期

Laravel 功能相當強大且實用,本篇文章將逐步探索 Laravel 核心生命週期機制和註解理解的流程和功能,不難發現其實核心設計是相當巧妙且具有高彈性和客製化,從 public/index.php 進入點出發,理解其核心運作原理和概念會有助於後續客製化和開發,文末會簡單列出核心步驟方便大家在設計代碼時參考。 Entry point // public/index.php <?php use Illuminate\Contracts\Http\Kernel; use Illuminate\Http\Request; // 定義了常數 LARAVEL_START,其值為當前時間的微秒數,用於計算應用程式的啟動時間 define('LARAVEL_START', microtime(true)); // 檢查應用程式是否處於維護模式。 // 如果存在,則引入該檔案。維護模式用於暫時關閉應用程式,以進行維護或升級。 if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) { require $maintenance; } // 用於自動載入所需的依賴項和類別 require __DIR__.'/../vendor/autoload.php'; // 這邊會細說,這邊會返回 app instance $app = require_once __DIR__.'/../bootstrap/app.php'; // 使用 Service Container 解析實作 Http Kernel 接口的實例。 $kernel = $app->make(Kernel::class); // 使用 Request::capture() 捕獲了當前的請求,並將其傳遞給 handle() 方法進行處理。handle() 方法處理請求並返回一個回應對象,並將其賦值給 $response 變數。 $response = $kernel->handle( $request = Request::capture() )->send(); // 使用 terminate() 方法通知 Kernel 實例已經處理完畢,並傳遞了原始請求和回應對象。這個方法在應用程式執行結束後執行,例如在回應被送出到瀏覽器之後。 $kernel->terminate($request, $response); 核心 Application: Service Container 上面代碼是講述 Laravel 整個生命週期運行的步驟,接下來說明 app....

May 23, 2023 · Yish

實作 Container in PHP

Container 機制是一種依賴注入的設計模式,它允許您將對象的創建和解析分離。這意味著您可以在應用程序中創建一個容器,然後使用容器來解析對象,而不是直接創建它們。這樣做的好處是,您可以更輕鬆地管理對象之間的依賴關係,並且可以更輕鬆地進行單元測試。 Container 機制通常由一個容器類別實現,該類別包含一個綁定數組,用於存儲抽象類別和具體類別之間的映射。當您需要解析一個對象時,容器會查找該對象的綁定,然後使用綁定中指定的具體類別或 Closure 創建該對象。 Container 機制還支持單例模式。如果您需要在應用程序中共享一個對象,則可以使用 singleton 方法來綁定抽象類別到具體類別或 Closure,並將 shared 參數設置為 true。這樣做可以確保每次解析該對象時都返回同一個實例。 使用 ReflectionClass 來解析對象,並使用 ReflectionParameter 來獲取對象的構造函數的參數列表。然後,它遞歸地解析每個參數的依賴關係,直到所有依賴關係都被解析為止。如果對象沒有構造函數,則直接返回一個新的對象實例。否則,它會使用 ReflectionClass::newInstanceArgs() 方法創建一個新的對象實例,並傳遞解析後的依賴關係作為參數。這種方法的好處是,它可以更輕鬆地管理對象之間的依賴關係,並且可以更輕鬆地進行單元測試。 基礎 Container 實作 <?php require_once 'Container.php'; class Foo {} $container = new Container; // 綁定 foo => new Foo $container->bind('foo', function ($container) { return new Foo; }); $foo1 = $container->make('foo'); var_dump($foo1); // object(Foo)#3 (0) { } 具體來說是怎麼實現這種綁定 Container,以下會逐步實現: 實現 bind class Container { protected $bindings = []; // 綁定一個實例進入 container public function bind($abstract, $concrete = null, $shared = false) { // $shared 參數用於指示是否應該返回同一個實例。 // 如果 shared 參數設置為 true,則返回同一個實例。 // 這對於需要共享的對象非常有用,例如資料庫連接或日誌記錄器。 // 如果 shared 參數設置為 false,則每次調用 make 方法時都會創建一個新的實例。 $this->bindings[$abstract] = compact('concrete', 'shared'); // array(1) { ["foo"]=> array(2) { ["concrete"]=> object(Closure)#1 (1) { ["parameter"]=> array(1) { ["$container"]=> string(10) "" } } ["shared"]=> bool(false) } } } ....

May 16, 2023 · Yish

Make More Things the Same

前幾天在網上看到一種設計方式,闡述了一種讓代碼看起來跟擴展性高的一種思維方式,作者透過實際例子描述如何從一開始的設計演化到最後的概念,這樣子的設計概念其實充滿在 Laravel 當中,我認為是相當值得學習的一個作法與思維,以下為實際演示和相關我理解的註解: 首先作者提出了一種原有的代碼設計思路,這邊可以看到示例代碼,用來處理相關 throw 和 report 的機制: class Flaky { protected $throw = true; public function reportFailures() { $this->throw = false; } public function throwFailures() { $this->throw = true; } protected function handle(Exception $e) { $this->throw ? throw $e : report($e); } } 這邊是一個很基礎的設計,可以看到透過呼叫 method 來改變 throw property 來改變最後 handle 的動作。然而當有新的方法,例如 logFailures,就必須調整兩個部分: 新增 logFailures 方法 調整 handle 邏輯 這樣會導致會大幅度去調整原有邏輯跟方法,而沒有一個通用的方法,而且新增的邏輯是有機會跟原本方法不一樣,因此作者透過步驟去分析跟設計,而這樣子的作法也是 Laravel 代碼庫中的實踐方法: class Flaky { protected $handleFailure; public function __construct() { $this->throwFailures(); } public function handleFailures($callback) { // 這邊透過 callback 的行為來增加代碼彈性,使其作用域在內部 $this->handleFailure = $callback; } public function reportFailures() { $this->handleFailures(function($e) { report($e); }); } public function throwFailures() { $this->handleFailures(function($e) { throw $e; }); } // new method public function logFailures() { $this->handleFailures(function($e) { logger($e) }); } protected function handle(Exception $e) { call_user_func($this->handleFailure, $e); } } 透過 handle 去 call handleFailure,除了內部提供的 reportFailures, throwFailures, logFailures,也可以直接呼叫 handleFailure 進行自定義方法:...

March 28, 2023 · Yish

Pest

Pest 對我來說提供了更便捷的測試方式和直譯式的寫法,類似 JS 相關的測試框架,同時又保留了 Laravel 和 PHP 龐大的輔助函數和功能。 official laracasts 主結構 $ composer require pestphp/pest --dev --with-all-dependencies $ ./vendor/bin/pest --init $ ./vendor/bin/pest folders: ├── 📂 tests │ ├── 📂 Unit │ │ └── ExampleTest.php │ └── 📂 Feature │ │ └── ExampleTest.php │ └── TestCase.php │ └── Pest.php ├── phpunit.xml 簡單示例 這邊可以自己添加 phpunit.xml 對應路徑,可以看到 Pest.php 實現細節: 這邊表示注入 TestCase 到 Feature 底下,可以使用 TestCase 裡面所提供的方法,當然也可以注入對應的方法到指定的 folder 底下: uses( Tests\TestCase::class, // Illuminate\Foundation\Testing\RefreshDatabase::class, )->in('Feature'); SumTest...

March 21, 2023 · Yish