PHP Readonly Properties与Domain Model:构建不可变核心业务对象的实践

PHP Readonly Properties与Domain Model:构建不可变核心业务对象的实践

大家好,今天我们来深入探讨PHP的readonly properties与Domain Model结合,构建不可变核心业务对象的实践。在软件开发中,尤其是涉及复杂的业务逻辑时,保持数据的完整性和一致性至关重要。不可变对象为我们提供了一种强大的工具,可以有效地防止数据在生命周期中被意外修改,从而提高代码的可靠性和可维护性。

1. 什么是Domain Model?

首先,我们需要理解什么是Domain Model。简单来说,Domain Model是软件系统对现实世界业务领域的抽象表示。它包含了一系列的对象(实体、值对象等),以及它们之间的关系,这些对象共同描述了业务规则和流程。

例如,在一个电商系统中,Product(商品)、Order(订单)、Customer(顾客)等都可以被认为是Domain Model中的对象。每个对象都具有自己的属性和行为,这些属性和行为共同构成了该对象在业务领域中的含义。

Domain Model 的重要性:

  • 业务逻辑集中化: 将业务逻辑封装在模型内部,避免逻辑分散在Controller或其他地方,提高代码的可读性和可维护性。
  • 领域驱动设计 (DDD): Domain Model是DDD的核心,通过清晰地定义领域模型,可以更好地理解业务需求,并将其转化为高质量的代码。
  • 可测试性: 结构良好的Domain Model更容易进行单元测试,因为每个对象都相对独立,易于隔离和验证。

2. 为什么需要不可变对象?

不可变对象是指一旦创建,其状态(属性值)就不能被修改的对象。 这种不变性带来了诸多好处:

  • 线程安全: 不可变对象天生就是线程安全的,因为它们的状态不会被多个线程同时修改,避免了并发问题。
  • 简化推理: 当我们使用不可变对象时,可以更容易地推理代码的行为。由于对象的状态不会改变,因此我们可以确定对象在任何时刻都具有特定的值。
  • 缓存友好: 不可变对象更容易进行缓存,因为它们的值不会改变,可以放心地将它们存储在缓存中,而无需担心缓存失效。
  • 防御性编程: 通过使用不可变对象,可以防止代码中其他部分意外地修改对象的状态,从而提高代码的健壮性。
  • 更清晰的副作用: 不可变性使得函数和方法更容易理解,因为它们不会修改输入对象的状态。

3. PHP Readonly Properties 的作用

PHP 8.1 引入了 readonly 关键字,用于声明类的只读属性。 一旦 readonly 属性在构造函数中或属性声明时被赋值,之后就不能再被修改。

ReadOnly Properties 的优点:

  • 强制不变性: readonly 关键字强制属性的不可变性,编译器会在运行时检查是否尝试修改只读属性,从而提供更强的保证。
  • 代码清晰性: 明确地声明属性为只读,可以提高代码的可读性,让开发者更容易理解代码的意图。
  • 减少错误: 避免意外修改属性值,减少程序出错的可能性。

示例:

<?php

class Product
{
    public readonly int $id;
    public readonly string $name;
    public readonly float $price;

    public function __construct(int $id, string $name, float $price)
    {
        $this->id = $id;
        $this->name = $name;
        $this->price = $price;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getPrice(): float
    {
        return $this->price;
    }
}

$product = new Product(123, 'Example Product', 99.99);

echo "Product ID: " . $product->id . "n";
echo "Product Name: " . $product->name . "n";
echo "Product Price: " . $product->price . "n";

// 尝试修改 readonly 属性会导致错误:
// Fatal error: Uncaught Error: Cannot modify readonly property Product::$id
// $product->id = 456;

在这个例子中,Product 类的 idnameprice 属性都被声明为 readonly。这意味着一旦 Product 对象被创建,这些属性的值就不能再被修改。 任何尝试修改这些属性的操作都会导致一个致命错误。

4. 将 Readonly Properties 应用于 Domain Model

readonly properties 应用于 Domain Model 可以帮助我们构建不可变的核心业务对象,从而提高代码的可靠性和可维护性。

示例:Order

<?php

class Order
{
    public readonly int $id;
    public readonly Customer $customer;
    public readonly array $items; // array of OrderItem
    public readonly DateTimeImmutable $orderDate;
    public readonly string $status; // "pending", "processing", "shipped", "delivered", "cancelled"

    public function __construct(int $id, Customer $customer, array $items, DateTimeImmutable $orderDate, string $status = "pending")
    {
        $this->id = $id;
        $this->customer = $customer;
        $this->items = $items;
        $this->orderDate = $orderDate;
        $this->status = $status;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getCustomer(): Customer
    {
        return $this->customer;
    }

    public function getItems(): array
    {
        return $this->items;
    }

    public function getOrderDate(): DateTimeImmutable
    {
        return $this->orderDate;
    }

    public function getStatus(): string
    {
        return $this->status;
    }

    // 订单状态改变的方法,返回一个新的Order对象
    public function withStatus(string $newStatus): Order
    {
        return new Order(
            $this->id,
            $this->customer,
            $this->items,
            $this->orderDate,
            $newStatus
        );
    }

    // 添加订单项,返回新的Order对象
    public function withAddedItem(OrderItem $newItem): Order
    {
        $newItems = $this->items;
        $newItems[] = $newItem;
        return new Order(
            $this->id,
            $this->customer,
            $newItems,
            $this->orderDate,
            $this->status
        );
    }
}

class OrderItem
{
    public readonly int $productId;
    public readonly int $quantity;
    public readonly float $price;

    public function __construct(int $productId, int $quantity, float $price)
    {
        $this->productId = $productId;
        $this->quantity = $quantity;
        $this->price = $price;
    }

    public function getProductId(): int
    {
        return $this->productId;
    }

    public function getQuantity(): int
    {
        return $this->quantity;
    }

    public function getPrice(): float
    {
        return $this->price;
    }
}

class Customer
{
    public readonly int $id;
    public readonly string $name;
    public readonly string $email;

    public function __construct(int $id, string $name, string $email)
    {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getEmail(): string
    {
        return $this->email;
    }
}

// 使用示例
$customer = new Customer(1, 'John Doe', '[email protected]');
$item1 = new OrderItem(101, 2, 25.00);
$item2 = new OrderItem(102, 1, 50.00);

$order = new Order(1, $customer, [$item1, $item2], new DateTimeImmutable());

echo "Order ID: " . $order->getId() . "n";
echo "Customer Name: " . $order->getCustomer()->getName() . "n";
echo "Order Date: " . $order->getOrderDate()->format('Y-m-d H:i:s') . "n";
echo "Order Status: " . $order->getStatus() . "n";

// 修改订单状态 (创建新的 Order 对象)
$updatedOrder = $order->withStatus("shipped");

echo "Original Order Status: " . $order->getStatus() . "n";      // 输出: pending
echo "Updated Order Status: " . $updatedOrder->getStatus() . "n"; // 输出: shipped

// 添加新的订单项 (创建新的 Order 对象)
$item3 = new OrderItem(103, 3, 10.00);
$updatedOrderWithItem = $order->withAddedItem($item3);

echo "Original Order Item Count: " . count($order->getItems()) . "n";           // 输出: 2
echo "Updated Order Item Count: " . count($updatedOrderWithItem->getItems()) . "n"; // 输出: 3
?>

在这个例子中,Order 类的所有属性都被声明为 readonly。 这意味着一旦 Order 对象被创建,它的 idcustomeritemsorderDatestatus 属性就不能再被修改。

需要注意的是,虽然 Order 对象本身是不可变的,但如果 items 属性包含可变对象(例如,没有使用 readonly properties 的 OrderItem 类),那么 Order 对象的状态仍然可能被间接修改。 为了确保完全的不可变性,我们需要确保所有相关的对象都是不可变的。

另外,修改订单状态或者添加订单项,我们并没有直接修改原有的 Order 对象,而是创建了一个新的 Order 对象,并将新的状态或订单项赋值给新的对象。 这种方式遵循了不可变对象的原则,保证了原有对象的状态不会被改变。 withStatus()withAddedItem() 方法返回新的 Order 对象,保持了原始 Order 对象的不可变性。

关键点:

  • 构造函数赋值: readonly 属性只能在构造函数或属性声明时赋值。
  • 深拷贝: 当属性包含对象时,需要考虑深拷贝,确保内部对象也是不可变的。 在上面的例子中,CustomerDateTimeImmutable 对象也是不可变的。 如果 Customer 是可变的,那么需要创建 Customer 的一个副本,以保证 Order 的不可变性。
  • 集合处理: 当属性是集合(例如,数组)时,需要返回新的集合,而不是修改原始集合。 上面的 withAddedItem 方法创建了一个新的 $newItems 数组,并将新的 OrderItem 添加到新的数组中,然后用新的数组创建一个新的 Order 对象。

5. 值对象 (Value Object) 与不可变性

值对象是 Domain Model 中另一个重要的概念。 值对象是根据其属性值来定义的,而不是根据其身份来定义的。 例如,Address(地址)、Money(金额)等都可以被认为是值对象。

值对象通常是不可变的,因为它们的值一旦确定,就不应该再被修改。

示例:Money

<?php

class Money
{
    public readonly float $amount;
    public readonly string $currency;

    public function __construct(float $amount, string $currency)
    {
        $this->amount = $amount;
        $this->currency = $currency;
    }

    public function getAmount(): float
    {
        return $this->amount;
    }

    public function getCurrency(): string
    {
        return $this->currency;
    }

    public function add(Money $other): Money
    {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException("Currencies must match.");
        }

        return new Money($this->amount + $other->amount, $this->currency);
    }

    public function subtract(Money $other): Money
    {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException("Currencies must match.");
        }

        return new Money($this->amount - $other->amount, $this->currency);
    }

    public function equals(Money $other): bool
    {
        return $this->amount === $other->amount && $this->currency === $other->currency;
    }

    public function __toString(): string
    {
        return $this->amount . ' ' . $this->currency;
    }
}

// 使用示例
$amount1 = new Money(100.00, 'USD');
$amount2 = new Money(50.00, 'USD');

$sum = $amount1->add($amount2);
echo "Sum: " . $sum . "n"; // 输出: Sum: 150 USD

$difference = $amount1->subtract($amount2);
echo "Difference: " . $difference . "n"; // 输出: Difference: 50 USD

$amount3 = new Money(100.00, 'USD');
echo "Amount1 equals Amount3: " . ($amount1->equals($amount3) ? 'true' : 'false') . "n"; // 输出: true

// 尝试修改 amount 属性会导致错误:
// Fatal error: Uncaught Error: Cannot modify readonly property Money::$amount
// $amount1->amount = 200.00;
?>

在这个例子中,Money 类的 amountcurrency 属性都被声明为 readonly。 这确保了 Money 对象一旦创建,其值就不能再被修改。 add()subtract() 方法返回新的 Money 对象,而不是修改原始对象。

6. 使用 Data Transfer Objects (DTOs) 初始化 Domain Model

在实际应用中,Domain Model 对象的数据通常来自外部,例如数据库、API 等。 为了将数据从外部源传递到 Domain Model 对象,我们可以使用 Data Transfer Objects (DTOs)。

DTOs 是一些简单的对象,用于携带数据。 它们通常只包含一些公共属性,用于存储数据,而没有复杂的业务逻辑。

示例:

<?php

// DTO
class ProductData
{
    public int $id;
    public string $name;
    public float $price;
}

// Domain Model
class Product
{
    public readonly int $id;
    public readonly string $name;
    public readonly float $price;

    public function __construct(int $id, string $name, float $price)
    {
        $this->id = $id;
        $this->name = $name;
        $this->price = $price;
    }

    public static function fromData(ProductData $data): Product
    {
        return new Product($data->id, $data->name, $data->price);
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getPrice(): float
    {
        return $this->price;
    }
}

// 使用示例
$productData = new ProductData();
$productData->id = 123;
$productData->name = 'Example Product';
$productData->price = 99.99;

$product = Product::fromData($productData);

echo "Product ID: " . $product->getId() . "n";
echo "Product Name: " . $product->getName() . "n";
echo "Product Price: " . $product->getPrice() . "n";
?>

在这个例子中,ProductData 是一个 DTO,用于携带从外部源获取的商品数据。 Product 类有一个静态方法 fromData(),用于从 ProductData 对象创建一个 Product 对象。 这样可以将数据转换逻辑封装在 Product 类中,并保持 Product 对象的不可变性。

使用 DTO 的好处:

  • 解耦: 将数据传输逻辑与 Domain Model 解耦,使得 Domain Model 更加独立和可测试。
  • 数据验证: 可以在 DTO 中进行数据验证,确保传递到 Domain Model 的数据是有效的。
  • 灵活性: 可以根据需要修改 DTO 的结构,而无需修改 Domain Model。

7. 总结

通过结合 PHP 的 readonly properties 和 Domain Model,我们可以构建不可变的核心业务对象,从而提高代码的可靠性、可维护性和可测试性。 不可变对象可以简化推理、提高线程安全性和缓存友好性。 在设计 Domain Model 时,应该尽可能地将对象设计为不可变的,并使用值对象来表示不可变的数据。 使用 DTOs 可以将数据从外部源传递到 Domain Model 对象,并保持 Domain Model 的独立性。

8. 采用不可变对象构建可靠系统

利用 PHP 的 Readonly Properties,结合领域模型的设计原则,我们可以更好地构建健壮、可维护、可测试的应用程序。在核心业务逻辑中使用不可变对象,可以有效降低bug出现的概率,提升代码的整体质量。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注