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

时间:2018-03-05 19:59:19

标签: php design-patterns domain-driven-design builder builder-pattern

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

使用值对象

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

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

2 个答案:

答案 0 :(得分:1)

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

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

答案 1 :(得分: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是一个原语,那么你可能会非常关心。