一个简单的问题是,我在域中有一个email
值对象,而在我的域中,电子邮件可以是(null,有效的电子邮件),那么在这种情况下,如果我想将电子邮件对象传递给我的contact_info
值对象,哪个选项更有效//从ddd的角度来看
选项1:
class email{
public function construct($email)
{
if($email !== null)
{
// Assert that the email is valid
}
$this->email = $email
}
}
class contact_info{
public function __construct(Email $email){
}
}
选项2:
class email{
public function construct($email)
{
// Assert that the email is valid
$this->email = $email
}
}
class contact_info{
public function __construct(Email $email = null){
if($email !== null){
$this->email = $email;
}
}
}
显然,选项1要好得多,它的抽象具有较小的误差幅度。但我是ddd的新手,所以我不确定:)
您的第一个选项是错误的,因为类有两种可能的状态,而其中一种是无效的,因为null
无法完全填充Email
接口。想象一下,使用该类的一个实例,即使电子邮件不是可选的,您也必须检查电子邮件是否有效这使得类型安全有点毫无意义:
class Consumer{
// email is not optional at all
function __construct(Email $email){
if(!$email->isActuallyAnEmail()) // WTH?
throw new Exception;
}
}
您尝试使用的是NullObject模式。但是,如果使用NullObjects来代替仅行为接口,则效果会更好;相反,在您的情况下,您是围绕标量数据建模的,标量数据是电子邮件字符串:NullObject模式可以使用,只要它看起来工作正常,并且对调用方代码做得非常好。如果接口应该在某个地方返回标量数据(在您的情况下是电子邮件字符串),那么实现所述接口的NullObject只能弥补该数据。所以这实际上不是一个NullObject,而是我所说的Fake/Example/Placeholder对象。大致:
// behavioral-only, a perfectly fine NullObject
class NullLogger implements Logger{
function log($message){ return true; }
}
// this sucks. you are just converting null to errors
// or moving errors to another place
// this NullObject implementation is invalid
// as it doesn't actually fullfill the Email interface
class NullEmail implements Email{
function __toString(){ throw new Exception; }
}
// not behavioral-only, an ExampleObject in this case
class ExampleEmail implements Email{
function __toString(){ return "example@example.org"; }
}
显然,这有着完全不同的含义。它不再是可选值。数据是存在的,但它是虚构的;它是一个示例值、一个默认值或只是一个占位符。
因此,选项2是正确的(除非您可以将example@example.org
附加到"联系人信息"对象)。
如果你想了解更多关于null
处理的信息,也可以查看Option模式/Maybe monad,它理解起来有点复杂;你可以看一下,但不是严格要求的。
这里有两个ValueObject:电子邮件和联系人信息,两者都可以有不同的不变量。
如果您的电子邮件VO具有null值,则该VO应无效(电子邮件(null)应引发异常)。然而,即使没有设置电子邮件,您的ContactInfo也是有效的。
在我看来,对于构造函数,您应该只传递对象有效所需的值,如果不需要电子邮件,则不要将其传递到那里。
当然,默认参数会让事情变得更容易,但它往往隐藏了设置值的真正含义。
在这个例子中很难看到,但看看这个:
class Person:
def __init__(self, id, type=USER):
self._id = id
self._type = type
VS
class Person:
def __init__(self, id):
self._id = id
self._type = USER
def give_admin_permissions(self):
self._type = ADMIN
我们可以将ADMIN类型直接传递给构造函数,但它真的说明了我们做了什么吗?它不太冗长。
然而,在你的例子中,这很好。