鉴于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);
}
答案 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
是一个原语,那么你可能会非常关心。