域模型不一定是可以直接转换为数据库行的对象。
您的 Person 示例确实符合此描述,我喜欢将这样的对象称为实体(从 Doctrine 2 ORM 中采用)。
但是,就像 Martin Fowler 所描述的那样,域模型是包含行为和数据的任何对象。
严格的解决方案
对于您描述的问题,这是一个非常严格的解决方案:
说您的Person 域模型(或实体)必须有名字和姓氏,并且可选地有婚前姓氏。这些必须是字符串,但为简单起见可以包含任何字符。
您希望强制只要存在这样的Person,就满足这些先决条件。该类将如下所示:
class Person
{
/**
* @var string
*/
protected $firstname;
/**
* @var string
*/
protected $lastname;
/**
* @var string|null
*/
protected $maidenname;
/**
* @param string $firstname
* @param string $lastname
* @param string|null $maidenname
*/
public function __construct($firstname, $lastname, $maidenname = null)
{
$this->setFirstname($firstname);
$this->setLastname($lastname);
$this->setMaidenname($maidenname);
}
/**
* @param string $firstname
*/
public function setFirstname($firstname)
{
if (!is_string($firstname)) {
throw new InvalidArgumentException('Must be a string');
}
$this->firstname = $firstname;
}
/**
* @return string
*/
public function getFirstname()
{
return $this->firstname;
}
/**
* @param string $lastname
*/
public function setLastname($lastname)
{
if (!is_string($lastname)) {
throw new InvalidArgumentException('Must be a string');
}
$this->lastname = $lastname;
}
/**
* @return string
*/
public function getLastname()
{
return $this->lastname;
}
/**
* @param string|null $maidenname
*/
public function setMaidenname($maidenname)
{
if (!is_string($maidenname) or !is_null($maidenname)) {
throw new InvalidArgumentException('Must be a string or null');
}
$this->maidenname = $maidenname;
}
/**
* @return string|null
*/
public function getMaidenname()
{
return $this->maidenname;
}
}
如您所见,在不满足先决条件的情况下,您无法实例化 Person 对象(忽略反射)。
这是一件好事,因为每当您遇到Person 对象时,您都可以 100% 确定您正在处理的数据类型。
现在您需要第二个域模型来处理用户输入,我们称它为PersonForm(因为它通常代表正在网站上填写的表格)。
它具有与Person 相同的属性,但盲目地接受任何类型的数据。
它还将有一个验证规则列表,一个像isValid() 这样使用这些规则来验证数据的方法,以及一个获取任何违规的方法。
我会把类的定义留给你想象:)
最后,您需要一个控制器(或服务)将它们联系在一起。这是一些伪代码:
class PersonController
{
/**
* @param Request $request
* @param PersonMapper $mapper
* @param ViewRenderer $view
*/
public function createAction($request, $mapper, $view)
{
if ($request->isPost()) {
$data = $request->getPostData();
$personForm = new PersonForm();
$personForm->setData($data);
if ($personForm->isValid()) {
$person = new Person(
$personForm->getFirstname(),
$personForm->getLastname(),
$personForm->getMaidenname()
);
$mapper->insert($person);
// redirect
} else {
$view->setErrors($personForm->getViolations());
$view->setData($data);
}
}
$view->render('create/add');
}
}
如您所见,PersonForm 用于拦截和验证用户输入。只有当该输入有效时,才会创建 Person 并将其保存在数据库中。
业务规则
这确实意味着某些业务逻辑将被复制:
在Person 中,您需要强制执行业务规则,但它可以简单地在出现问题时抛出异常。
在PersonForm 中,您将拥有应用相同规则的验证器,以防止无效的用户输入到达Person。但是在这里,那些验证器可以更高级。考虑诸如人为错误消息、违反第一条规则等问题。您还可以应用稍微改变输入的过滤器(例如将用户名小写)。
换句话说:Person 将在低级别执行业务规则,而PersonForm 更多的是处理用户输入。
更方便
一种不太严格的方法,但可能更方便:
限制在Person 中完成的验证以强制执行所需的属性,并强制执行属性的类型(字符串、int 等)。仅此而已。
您还可以在Person 中获得约束列表。这些是业务规则,但没有实际的验证代码。所以只是一点配置而已。
拥有一个Validator 服务,该服务能够接收数据以及约束列表。它应该能够根据约束验证该数据。您可能需要为每种类型的约束创建一个小的验证器类。 (看看Symfony 2 validator component)。
PersonForm 可以注入Validator 服务,因此它可以使用该服务来验证用户输入。
最后,有一个PersonManager 服务负责您想要在Person 上执行的任何操作(例如创建/更新/删除,可能还有注册/激活/等)。 PersonManager 需要 PersonMapper 作为依赖项。
当您需要创建Person 时,您调用类似$personManager->create($userInput); 的调用将创建PersonForm,验证数据,创建Person(当数据有效时),并持久化@ 987654352@ 使用PersonMapper。
这里的关键是:
您可以在所有这些类周围画一个圆圈,并将其称为您的“个人域”(DDD)。该域的接口(入口点)是PersonManager 服务。 您要对Person 执行的每个操作必须通过PersonManager。
如果您在应用程序中坚持这一点,那么在确保业务规则方面您应该是安全的 :)