鉴于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);
}
关于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
是一个原始的,那么你可能会非常关心。
构建器不是域驱动设计范例的一部分,因为它不能表示为域无处不在的语言的一部分。如果你想要DDD,你应该使用工厂(例如,静态方法工厂,服务工厂或其他形式的工厂)或repo,如果你从某些来源反序列化。
要回答有关验证的具体问题,请执行以下操作:否,您不会“稍后”验证您的实体。您的实体及其属性永远不应处于无效状态,因为知道调用“验证”代码的责任将取决于消费者。此外,您将无法在需要时序列化该实体