封装通用逻辑(领域驱动设计、最佳实践)
-
21-08-2019 - |
题
更新: 09/02/2009 - 修改了问题,提供了更好的例子,增加了赏金。
你好,
我正在使用数据库和实体(域对象)之间的数据映射器模式构建 PHP 应用程序。我的问题是:
封装经常执行的任务的最佳方法是什么?
例如,一项常见任务是从站点映射器检索一个或多个站点实体,并从页面映射器检索其关联的(主页)页面实体。目前,我会这样做:
$siteMapper = new Site_Mapper();
$site = $siteMapper->findByid(1);
$pageMapper = new Page_Mapper();
$site->addPage($pageMapper->findHome($site->getId()));
现在,这是一个相当简单的示例,但实际上它变得更加复杂,因为每个站点也有一个关联的区域设置,并且该页面实际上有多个修订(尽管出于此任务的目的,我只对最近的一个感兴趣) )。
我需要在应用程序中的多个位置执行此操作(获取站点和关联的主页、区域设置等),并且我想不出封装此任务的最佳方法/位置,因此我不会必须到处重复。理想情况下,我希望得到这样的结果:
$someObject = new SomeClass();
$site = $someObject->someMethod(1); // or
$sites = $someObject->someOtherMethod();
生成的站点实体已创建其关联实体并可供使用。
将这些对象保存回来时也会出现同样的问题。假设我有一个网站实体和关联的主页实体,并且它们都已被修改,我必须执行以下操作:
$siteMapper->save($site);
$pageMapper->save($site->getHomePage());
同样,这很简单,但是这个例子被简化了。重复代码仍然适用。
在我看来,有某种可以照顾的中心对象是有意义的:
- 检索一个或多个站点以及所有必要的关联实体
- 使用新的关联实体创建新的站点实体
- 获取一个或多个站点并保存它和所有关联的实体(如果它们已更改)
回到我的问题,这个对象应该是什么?
- 现有的映射器对象?
- 基于存储库模式的东西?*
- 基于工作单元模式的东西?*
- 还有别的事吗?
* 我不 完全 正如您可能猜到的那样,了解其中任何一个。
是否有解决这个问题的标准方法,有人可以提供他们如何实现它的简短描述吗?我并不是在寻找任何人提供完整的工作实现,只是理论。
谢谢,
杰克
解决方案
使用存储库/服务模式,您的存储库类将为每个实体提供一个简单的 CRUD 接口,然后服务类将是一个附加层,用于执行附加逻辑(例如附加实体依赖项)。然后,您的应用程序的其余部分仅使用服务。您的示例可能如下所示:
$site = $siteService->getSiteById(1); // or
$sites = $siteService->getAllSites();
然后在 SiteService 类中你会看到这样的内容:
function getSiteById($id) {
$site = $siteRepository->getSiteById($id);
foreach ($pageRepository->getPagesBySiteId($site->id) as $page)
{
$site->pages[] = $page;
}
return $site;
}
我不太了解 PHP,所以如果语法上有问题请原谅。
其他提示
[编辑:本文试图解决这样一个事实,即编写自定义代码来直接处理情况通常比尝试将问题适应模式更容易。]
模式在概念上很好,但它们并不总是“映射”。经过多年的高端 PHP 开发,我们已经确定了一种非常直接的方式来处理此类问题。考虑一下:
文件:站点.php
class Site
{
public static function Select($ID)
{
//Ensure current user has access to ID
//Lookup and return data
}
public static function Insert($aData)
{
//Validate $aData
//In the event of errors, raise a ValidationError($ErrorList)
//Do whatever it is you are doing
//Return new ID
}
public static function Update($ID, $aData)
{
//Validate $aData
//In the event of errors, raise a ValidationError($ErrorList)
//Update necessary fields
}
然后,为了(从任何地方)调用它,只需运行:
$aData = Site::Select(123);
Site::Update(123, array('FirstName' => 'New First Name'));
$ID = Site::Insert(array(...))
关于 OO 编程和 PHP 要记住的一件事......PHP 不会在请求之间保留“状态”,因此创建对象实例只是为了立即销毁它通常没有意义。
我可能会首先将常见任务提取到某个地方的辅助方法,然后等待看看设计需要什么。感觉现在说还为时过早。
您将这个方法命名为什么?该名称通常暗示该方法所属的位置。
class Page {
public $id, $title, $url;
public function __construct($id=false) {
$this->id = $id;
}
public function save() {
// ...
}
}
class Site {
public $id = '';
public $pages = array();
function __construct($id) {
$this->id = $id;
foreach ($this->getPages() as $page_id) {
$this->pages[] = new Page($page_id);
}
}
private function getPages() {
// ...
}
public function addPage($url) {
$page = ($this->pages[] = new Page());
$page->url = $url;
return $page;
}
public function save() {
foreach ($this->pages as $page) {
$page->save();
}
// ..
}
}
$site = new Site($id);
$page = $site->addPage('/');
$page->title = 'Home';
$site->save();
使您的 Site 对象成为 聚合根 封装复杂的关联并保证一致性。
然后创建一个 站点存储库 负责检索网站聚合并填充其子页面(包括所有页面)。
您不需要单独的 PageRepository(假设您没有将 Page 设为单独的聚合根),并且您的 SiteRepository 也应该负责检索 Page 对象(在您的情况下通过使用现有的映射器)。
所以:
$siteRepository = new SiteRepository($myDbConfig);
$site = $siteRepository->findById(1); // will have Page children attached
然后是 findById
方法还负责查找站点的所有页面子级。这将具有与 CodeMonkey1 给出的答案类似的结构,但是我相信通过使用聚合和存储库模式,您将受益更多,而不是为此任务创建特定的服务。站点聚合的任何其他检索/查询/更新(包括其任何子对象)都将通过同一个 SiteRepository 完成。
编辑: 这是简短的 DDD 指南 帮助您了解术语,尽管我真的建议您阅读 埃文斯 如果你想要全貌。