构建器应该接受原语还是值对象

Should a builder accept primitives or value objects

给定一个 Address 至少 必须 有一个 $firstLine$postcode 但可以包含可选属性, 我希望实现 builder 以简化 Address.

的构造

删节版 Address 可能如下所示:

class Address
{
    /**
     * @var AddressLine
     */
    private $firstLine;

    /**
     * @var null|AddressLine
     */
    private $secondLine;

    /**
     * Other properties excluded for brevity
     */
    ...

    /**
     * @var Postcode
     */
    private $postcode;

    /**
     * @param AddressLine $firstLine
     * @param null|AddressLine $secondLine
     * ...
     * @param Postcode $postcode
     */
    public function __construct(AddressLine $firstLine, AddressLine $secondLine, ... , Postcode $postcode)
    {
        $this->firstLine = $firstLine;
        $this->secondLine = $secondLine;
        ...
        $this->postcode = $postcode;
    }

    public static function fromBuilder(AddressBuilder $builder)
    {
        return new self(
            $builder->firstLine(),
            $builder->secondLine(),
            ... ,
            $builder->postcode()
        );
    }
}

以上对我来说似乎很有意义,一个 public constructor 通过 typehints 保护其不变量 并允许传统构造,另外还有一个接受 AddressBuilder 的工厂方法,它可能看起来像下面这样:

class AddressBuilder
{
    public function __construct(...)
    {
    }

    public function withSecondLine(...)
    {
    }

    public function build()
    {
        return Address::fromBuilder($this);
    }
}

关于 AddressBuilder,它应该接受在 build() 方法中验证的原语,还是应该期望相关的 Value Object?

有基元

public function withSecondLine(string $line)
{
    $this->secondLine = $line;
}

public function build()
{
    ...
    $secondLine = new AddressLine($this->secondLine);

    return new Address(... , $secondLine, ...);
}

有值对象

public function withSecondLine(AddressLine $secondLine)
{
    $this->secondLine = $secondLine;
}

public function build()
{
    return Address::fromBuilder($this);
}

构建器不是域驱动设计范例的一部分,因为它不能表示为您所在域的通用语言的一部分。 如果你想 DDD,你应该使用工厂(例如,静态方法工厂、服务工厂或其他形式的工厂),或者如果你从某个来源反序列化,则应该使用 repo。

虽然要回答您关于验证的具体问题:不,您没有验证您的实体 "later"。您的实体及其属性永远不应处于无效状态,因为知道调用 "validating" 代码的责任将取决于消费者。此外,您将无法在需要时序列化该实体

In regards to the AddressBuilder, should it accept primitives which are validated during the build() method, or should it expect the relevant Value Object?

两种方法都可以。

当您处于应用程序的边界时,使用原语往往是最好的。例如,当您从 http 请求的有效负载中读取数据时,以域不可知原语表示的 API 可能比以域类型表示的 API 更容易处理。

随着您越来越接近应用程序的核心,使用域语言更有意义,因此您的 API 可能反映了这一点。

一种思考方式是构建器模式主要是一个实现细节。在简单的情况下,消费者只是一个函数

BowlingGame buildMeABowlingGameForGreatGood(int.... rolls) {
    BowlingGame.Builder builder = ...
    rolls.forEach(r -> {
        builder.addRoll(r)
    } )
    return builder.build();
}

而且函数的使用者根本不关心细节。

你甚至可以有不同的构建器API,以便不同的客户端上下文可以调用最合适的构建器

BowlingGame buildMeABowlingGameForGreatGood(int.... rolls) {
    BowlingGame.PrimitiveBuilder primitiveBuilder = new PrimitiveBuilder(
        new BowlingGame.ModelBuilder(...)
    );

    // ...
}

如果您不确定参数是否会通过验证检查,事情可能会变得有趣。

AddressBuilder builder = ...

// Do you want to reject an invalid X here?
builder.withSecondLine(X)

// Or do you prefer to reject an invalid X here?
builder.build()

构建器模式为您提供了对正在进行的构建的可变状态的句柄,您可以传递它。所以 build 语句可以任意远离 withSecondLine 语句。如果您已经知道 X 是有效的(因为它已经是一个模型值对象),那么它可能并不重要。如果 X 是原语,那么您可能会很在意。