首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 設計模式:值物件模式解決物件引用共享問題

     編輯:PHP 綜合知識

值物件模式:以值而非身份區分物件,避免共享引用副作用

值物件 沒有身份識別,只由屬性值定義。兩個值物件若屬性相同則視為相等。PHP 5+ 物件預設引用語義導致值物件共享引用 Bug,值物件模式透過不可變性與值比較解決此問題。

問題重現:BadDollar 引用共享 Bug

<?php
class BadDollar {
    public function __construct(public float $amount = 0) {}
    
    public function add(BadDollar $dollar): void {
        $this->amount += $dollar->amount; // 修改自身狀態!
    }
}

class Work {
    public function __construct(public BadDollar $salary = new BadDollar(200)) {}
    public function payDay(): BadDollar { return $this->salary; }
}

class Person {
    public ?BadDollar $wallet = null;
}

// 測試
$job = new Work();
$p1 = new Person();
$p2 = new Person();

$p1->wallet = $job->payDay(); // 兩人共享同一 BadDollar 實例!
$p2->wallet = $job->payDay();

$p1->wallet->add($job->payDay()); // p1 加薪
// Bug: p2 錢包也變了!Work 的 salary 也被修改!
// 因為 PHP 物件賦值是引用複製
?>

值物件正確實作:不可變 + 值比較

<?php
class Money {
    private float $amount;
    private string $currency;
    
    public function __construct(float $amount, string $currency = 'USD') {
        if ($amount < 0) throw new InvalidArgumentException('Amount cannot be negative');
        $this->amount = $amount;
        $this->currency = strtoupper($currency);
    }
    
    // 不可變:操作回傳新實例
    public function add(Money $other): Money {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException('Currency mismatch');
        }
        return new Money($this->amount + $other->amount, $this->currency);
    }
    
    public function subtract(Money $other): Money {
        return new Money($this->amount - $other->amount, $this->currency);
    }
    
    public function multiply(float $factor): Money {
        return new Money($this->amount * $factor, $this->currency);
    }
    
    // 值比較
    public function equals(Money $other): bool {
        return $this->amount === $other->amount && $this->currency === $other->currency;
    }
    
    public function __toString(): string {
        return sprintf('%.2f %s', $this->amount, $this->currency);
    }
    
    // Getter
    public function getAmount(): float { return $this->amount; }
    public function getCurrency(): string { return $this->currency; }
}

// 修正後的 Work
class Work {
    public function __construct(private Money $salary) {}
    public function payDay(): Money { return $this->salary; } // 回傳值物件 (不可變安全)
}

// 測試
$job = new Work(new Money(200));
$p1 = new Person();
$p2 = new Person();

$p1->wallet = $job->payDay();
$p2->wallet = $job->payDay();

// p1 加薪:產生新 Money 實例,不影響 p2 或 job
$p1->wallet = $p1->wallet->add($job->payDay()); 

// p1: 400, p2: 200, job: 200 ✅ 正確!
?>

值物件核心特徵

特徵 說明 實作要點
不可變性 建立後狀態不變 無 setter、操作回傳新實例、屬性 private/final
值相等性 屬性相同即相等 實作 equals()、PHP 8.1+ 可用 readonly class
無身份識別 不區分「哪一個」實例 不需 ID、不追蹤生命週期
自我驗證 建構子保證有效狀態 建構子驗證、拋出異常
可替換性 相等值物件可自由互換 無副作用、線程安全

常見值物件範例

<?php
// 1. Email 地址
readonly class Email {
    public function __construct(public string $value) {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("Invalid email: $value");
        }
    }
    public function equals(Email $other): bool { return $this->value === $other->value; }
}

// 2. 日期範圍
readonly class DateRange {
    public function __construct(
        public DateTimeImmutable $start,
        public DateTimeImmutable $end
    ) {
        if ($start > $end) throw new InvalidArgumentException('Start must be before end');
    }
    public function contains(DateTimeImmutable $date): bool {
        return $date >= $this->start && $date <= $this->end;
    }
    public function overlaps(DateRange $other): bool {
        return $this->start <= $other->end && $other->start <= $this->end;
    }
}

// 3. 地址
readonly class Address {
    public function __construct(
        public string $street,
        public string $city,
        public string $state,
        public string $postalCode,
        public string $country
    ) {}
    public function equals(Address $other): bool {
        return $this->street === $other->street
            && $this->city === $other->city
            // ...
    }
}

// 4. 數量 (防止負數)
readonly class Quantity {
    public function __construct(public int $value) {
        if ($value < 0) throw new InvalidArgumentException('Quantity cannot be negative');
    }
    public function add(Quantity $other): Quantity {
        return new Quantity($this->value + $other->value);
    }
}
?>

PHP 8.1+ readonly class 完美支援值物件

<?php
// PHP 8.1+ 唯讀類別:自動不可變、自動 getter
readonly class Money {
    public function __construct(
        public float $amount,
        public string $currency = 'USD'
    ) {
        if ($amount < 0) throw new InvalidArgumentException('Negative amount');
        $this->currency = strtoupper($currency);
    }
    
    public function add(Money $other): Money {
        if ($this->currency !== $other->currency) throw new InvalidArgumentException('Currency mismatch');
        return new Money($this->amount + $other->amount, $this->currency);
    }
    
    // PHP 8.1 無自動 equals,需手動實作
    public function equals(Money $other): bool {
        return $this->amount === $other->amount && $this->currency === $other->currency;
    }
}
?>

值物件 vs 實體 比較

維度 值物件 實體
識別方式 屬性值 唯一 ID
可變性 不可變 可變
生命週期 無 (值即存在) 有 (建立/修改/刪除)
比較方式 值比較 ID 比較
範例 Money、Email、Address、DateRange User、Order、Product、Account
資料庫映射 嵌入式 (Embedded) / JSON 欄位 獨立表、主鍵

Doctrine ORM 中嵌入值物件

<?php
use Doctrine\ORM\Mapping as ORM;

#[ORM\Embeddable]
class Address {
    #[ORM\Column(length: 100)] public string $street;
    #[ORM\Column(length: 50)] public string $city;
    // ...
}

#[ORM\Entity]
class User {
    #[ORM\Id, ORM\GeneratedValue, ORM\Column] private int $id;
    
    #[ORM\Embedded(class: Address::class, columnPrefix: 'address_')]
    private Address $address;
    
    public function getAddress(): Address { return $this->address; }
    public function setAddress(Address $address): void { $this->address = $address; }
}
// 產生欄位: address_street, address_city, ...
?>
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved