【问题标题】:DDD: how to keep a complex value object immutable?DDD:如何保持复杂值对象不可变?
【发布时间】:2011-11-16 08:15:26
【问题描述】:

我想将Address 建模为值对象。由于使其不可变是一种很好的做法,因此我选择不提供任何设置器,以便以后对其进行修改。

一种常见的做法是将数据传递给构造函数;但是,当值对象非常大时,它可能会变得非常臃肿:

class Address {
    public function __construct(
        Point $location,
        $houseNumber,
        $streetName,
        $postcode,
        $poBox,
        $city,
        $region,
        $country) {
        // ...
    }
}

另一种方法是将参数作为数组提供,从而产生一个干净的构造函数,但这可能会弄乱构造函数的实现:

class Address {
    public function __construct(array $parts) {
        if (! isset($parts['location']) || ! $location instanceof Point) {
            throw new Exception('The location is required');
        }
        $this->location = $location;
        // ...
        if (isset($parts['poBox'])) {
            $this->poBox = $parts['poBox'];
        }
        // ...
    }
}

这对我来说也有点不自然。

关于如何正确实现一个相当大的值对象的任何建议?

【问题讨论】:

  • 我个人认为,如果你的值对象足够大以至于导致问题,它需要被分解成多个值对象。地址示例对我个人的感受来说似乎很好,但如果你觉得它太大,它可能会变成位置 + 街道地址 + 城市(其中城市包括地区和国家)。
  • @Domenic:这也是一种有趣的方法!

标签: domain-driven-design immutability php-5.3 value-objects


【解决方案1】:

large list of parameters 的主要问题是可读性和混淆参数的危险。您可以使用Builder pattern 解决这些问题,如Effective Java 中所述。它使代码更具可读性(尤其是不支持命名和可选参数的语言):

public class AddressBuilder {
    private Point _point;
    private String _houseNumber;

    // other parameters

    public AddressBuilder() {
    }

    public AddressBuilder WithPoint(Point point) {
        _point = point;
        return this;
    }

    public AddressBuilder WithHouseNumber(String houseNumber) {
        _houseNumber = houseNumber;
        return this;
    }

    public Address Build() {
        return new Address(_point, _houseNumber, ...);
    }
}

Address address = new AddressBuilder()
    .WithHouseNumber("123")
    .WithPoint(point)
    .Build();

优点:

  • 参数已命名,因此更具可读性
  • 更难将门牌号与地区混淆
  • 可以使用您自己的参数顺序
  • 可选参数可以省略

我能想到的一个缺点是忘记指定参数之一(例如不调用WithHouseNumber)将导致运行时错误,而不是使用构造函数时的编译时错误。您还应该考虑使用更多的值对象,例如 PostalCode(与传递字符串相反)。

在相关说明中,有时业务需求需要更改部分值对象。例如,最初输入地址时,街道号码可能拼写错误,现在需要更正。由于您将 Address 建模为不可变对象,因此没有设置器。此问题的一种可能解决方案是在地址值对象上引入“无副作用功能”。该函数将返回对象本身的副本,但新街道名称除外:

public class Address {
    private readonly String _streetName;
    private readonly String _houseNumber;

    ... 

    public Address WithNewStreetName(String newStreetName) {
        // enforce street name rules (not null, format etc)

        return new Address(
            newStreetName
            // copy other members from this instance
            _houseNumber);
    }

    ... 
}

【讨论】:

  • 非常有趣的答案,谢谢。这也是我们所说的工厂,还是工厂模式保留给实体?
  • DDD 工厂可用于构造复杂的值对象。这个 Builder 可以被认为是一个 DDD 工厂。
  • 作为对这种模式的改进,您还可以避免在Address 中使用冗长的构造函数,而是将构造函数传递给构造函数:public Address Build() { return new Address(this); },构造函数将从构造函数中提取数据而是。
【解决方案2】:

这是领域驱动设计示例的常见问题。领域专家不见了,他会告诉您地址是什么及其要求。我怀疑领域专家会告诉您地址没有点。您可能能够从地址产生一个点,但它不需要一个点。还有一个 P.O.框不会是地址中的单独值。您可能需要一个邮政信箱地址类 (POBoxAddress) 我这么说是因为这个类看起来是由开发人员定义的,而不是运输或计费领域专家。通过与领域专家交谈,您可以减少构造函数参数的数量。

第二
您可以开始将参数分组为值对象。您可以创建一个 City 值对象。这可能需要城市、地区/州和国家。除非我知道地区和国家,否则我认为城市名称没有多大意义。 Paris 仅表示美国伊利诺伊州的巴黎或法国的法兰西岛巴黎,可以为您提供完整的画面。所以这也会减少对 Address 对象的计数参数计数。

如果您在 DDD 的道路上为您正在编码的领域寻找领域专家,那么您不应该是专家。有时问题不应该通过代码或漂亮的设计模式来解决。

【讨论】:

    【解决方案3】:

    不可变适用于并发计算,无阻塞和无锁,不可变适用于高性能和良好的可扩展性。

    所以Value Object可以在并发系统中更好地运行,包含在分布式系统中,用新的VO替换旧的VO,不需要更新,所以没有阻塞。

    【讨论】:

      猜你喜欢
      • 2020-08-25
      • 1970-01-01
      • 2011-02-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-02
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多