DDD:如何保持复杂值对象不可变?

Ben*_*min 6 domain-driven-design immutability value-objects php-5.3

我想将一个模型Address作为一个值对象.因为将它变为不可变是一个好习惯,所以我选择不提供任何可能允许稍后修改它的setter.

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

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

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

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'];
        }
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

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

关于如何正确实现一个非常大的价值对象的任何建议?

Dmi*_*try 12

大量参数的主要问题是可读性以及混淆参数的危险.您可以使用Effective 模式解决这些问题,如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();
Run Code Online (Sandbox Code Playgroud)

优点:

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

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

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

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);
    }

    ... 
}
Run Code Online (Sandbox Code Playgroud)

  • 作为对这种模式的改进,您还可以避免在 `Address` 中使用冗长的构造函数,而是将构建器传递给构造函数: `public Address Build() { return new Address(this); } }`,并且构造函数将从构建器中提取数据。 (2认同)