构建器是否应接受基元或值对象

问题描述 投票:0回答:2

鉴于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()
        );
    }
}

以上似乎对我有意义,一个公共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, ...);
}

使用Value Objects

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

public function build()
{
    return Address::fromBuilder($this);
}
php design-patterns domain-driven-design builder builder-pattern
2个回答
1
投票

关于AddressBuilder,它应该接受在build()方法中验证的原语,还是它应该期望相关的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是一个原始的,那么你可能会非常关心。


1
投票

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

要回答有关验证的具体问题,请执行以下操作:否,您不会“稍后”验证您的实体。您的实体及其属性永远不应处于无效状态,因为知道调用“验证”代码的责任将取决于消费者。此外,您将无法在需要时序列化该实体

© www.soinside.com 2019 - 2024. All rights reserved.