規格模式 將「某物件是否滿足特定條件」封裝為獨立類別,支援邏輯組合 (AND/OR/NOT),讓業務規則從領域物件中分離,提升可測試性與維護性。
<?php
// 規格介面
interface Specification {
public function isSatisfiedBy(mixed $candidate): bool;
// 組合方法 (預設實作)
public function and(Specification $other): Specification {
return new AndSpecification($this, $other);
}
public function or(Specification $other): Specification {
return new OrSpecification($this, $other);
}
public function not(): Specification {
return new NotSpecification($this);
}
}
// 組合規格基底
abstract class CompositeSpecification implements Specification {
protected Specification $spec1;
protected Specification $spec2;
public function __construct(Specification $spec1, Specification $spec2) {
$this->spec1 = $spec1;
$this->spec2 = $spec2;
}
}
class AndSpecification extends CompositeSpecification {
public function isSatisfiedBy(mixed $candidate): bool {
return $this->spec1->isSatisfiedBy($candidate)
&& $this->spec2->isSatisfiedBy($candidate);
}
}
class OrSpecification extends CompositeSpecification {
public function isSatisfiedBy(mixed $candidate): bool {
return $this->spec1->isSatisfiedBy($candidate)
|| $this->spec2->isSatisfiedBy($candidate);
}
}
class NotSpecification implements Specification {
private Specification $spec;
public function __construct(Specification $spec) { $this->spec = $spec; }
public function isSatisfiedBy(mixed $candidate): bool {
return !$this->spec->isSatisfiedBy($candidate);
}
}
?>
<?php
// 1. 領域物件
class Traveler {
public function __construct(
public string $name,
public float $minTemperature // 攝氏度
) {}
}
class Destination {
/** @var float[] 12個月平均溫度 */
public function __construct(
public string $name,
public array $monthlyAvgTemps
) {}
public function getAvgTemp(int $month): float {
return $this->monthlyAvgTemps[$month - 1] ?? 0;
}
}
class Trip {
public function __construct(
public Traveler $traveler,
public Destination $destination,
public DateTime $date
) {}
}
// 2. 具體規格:溫度達標
class TemperatureSpecification implements Specification {
public function isSatisfiedBy(mixed $candidate): bool {
if (!$candidate instanceof Trip) return false;
$month = (int)$candidate->date->format('m');
$avgTemp = $candidate->destination->getAvgTemp($month);
return $avgTemp >= $candidate->traveler->minTemperature;
}
}
// 3. 具體規格:預算範圍內
class BudgetSpecification implements Specification {
public function __construct(
private float $maxBudget
) {}
public function isSatisfiedBy(mixed $candidate): bool {
if (!$candidate instanceof Trip) return false;
// 假設 Destination 有 getEstimatedCost()
return $candidate->destination->getEstimatedCost() <= $this->maxBudget;
}
}
// 4. 具體規格:飛行時間限制
class FlightDurationSpecification implements Specification {
public function __construct(
private int $maxHours
) {}
public function isSatisfiedBy(mixed $candidate): bool {
if (!$candidate instanceof Trip) return false;
return $candidate->destination->getFlightHours() <= $this->maxHours;
}
}
// 5. 使用:組合規格
$traveler = new Traveler("Alice", 20.0); // 最低 20°C
$destinations = [
new Destination("東京", [5,6,10,15,20,23,26,27,23,18,13,8]),
new Destination("曼谷", [28,29,30,31,30,29,28,28,28,28,28,27]),
new Destination("雪梨", [22,22,21,19,16,14,13,14,16,18,19,21]),
new Destination("新加坡", [27,27,27,27,28,27,27,27,27,27,27,27]),
];
$date = new DateTime('2024-01-15'); // 1月
// 建立組合規格:溫暖 AND 預算5萬內 AND 飛行8小時內
$spec = (new TemperatureSpecification())
->and(new BudgetSpecification(50000))
->and(new FlightDurationSpecification(8));
echo "適合 {$traveler->name} 的 1 月目的地:\n";
foreach ($destinations as $dest) {
$trip = new Trip($traveler, $dest, $date);
if ($spec->isSatisfiedBy($trip)) {
echo "- {$dest->name} (1月均溫: {$dest->getAvgTemp(1)}°C)\n";
}
}
// 輸出: 曼谷、新加坡 (東京1月5°C太冷、雪梨雖溫但可能超預算/飛行時間)
?>
<?php
// 參數化規格:動態建立
class ParameterizedSpecFactory {
public static function minTemperature(float $minTemp): Specification {
return new class($minTemp) implements Specification {
private float $min;
public function __construct(float $min) { $this->min = $min; }
public function isSatisfiedBy(mixed $candidate): bool {
if (!$candidate instanceof Trip) return false;
$month = (int)$candidate->date->format('m');
return $candidate->destination->getAvgTemp($month) >= $this->min;
}
};
}
public static function maxBudget(float $max): Specification {
return new BudgetSpecification($max);
}
public static function maxFlightHours(int $hours): Specification {
return new FlightDurationSpecification($hours);
}
}
// 策略工廠:根據用戶偏好動態組合
class TripSpecFactory {
public static function createForTraveler(Traveler $traveler): Specification {
$spec = ParameterizedSpecFactory::minTemperature($traveler->minTemperature);
if ($traveler->maxBudget) {
$spec = $spec->and(ParameterizedSpecFactory::maxBudget($traveler->maxBudget));
}
if ($traveler->maxFlightHours) {
$spec = $spec->and(ParameterizedSpecFactory::maxFlightHours($traveler->maxFlightHours));
}
// 可擴展:偏好海島、文化、美食等標籤規格
return $spec;
}
}
// 使用
$spec = TripSpecFactory::createForTraveler($traveler);
// 等同於手動組合,但集中管理、易於擴展
?>
<?php
// 橋接規格到 SQL WHERE 子句
interface SqlSpecification extends Specification {
public function toSql(): string;
public function getParameters(): array;
}
class TemperatureSqlSpec implements SqlSpecification {
private float $minTemp;
private string $monthColumn;
public function __construct(float $minTemp, string $monthColumn = 'month') {
$this->minTemp = $minTemp;
$this->monthColumn = $monthColumn;
}
public function isSatisfiedBy(mixed $candidate): bool {
// 記憶體中判斷邏輯
if (!$candidate instanceof Trip) return false;
$month = (int)$candidate->date->format('m');
return $candidate->destination->getAvgTemp($month) >= $this->minTemp;
}
public function toSql(): string {
// 簡化:假設資料表有 monthly_temp_1 ... monthly_temp_12 欄位
return "monthly_temp_{$this->monthColumn} >= ?";
}
public function getParameters(): array {
return [$this->minTemp];
}
}
// 查詢建構器整合
function buildQuery(SqlSpecification $spec): array {
$sql = "SELECT * FROM destinations WHERE " . $spec->toSql();
$params = $spec->getParameters();
return [$sql, $params];
}
// 組合規格需實作複雜的 SQL 組合邏輯 (AndSpecification::toSql() 等)
?>
| 優點 | 缺點 |
|---|---|
| 業務規則獨立封裝,易於單元測試 | 簡單規則引入過多類別 (Over-engineering) |
| 支援邏輯組合 (AND/OR/NOT),靈活複用 | 複雜組合可能導致效能問題 (大量物件建立) |
| 符合開放封閉原則:新增規格不修改既有代碼 | 資料庫查詢轉換複雜 (需 SQL 產生器) |
| 領域語言化:程式碼可讀性高 (領域專家可理解) | 學習曲線較陡峭 |
| 易於動態組合 (執行時決定規則) |