【问题标题】:Encapsulating common logic (domain driven design, best practices)封装通用逻辑(领域驱动设计、最佳实践)
【发布时间】:2009-02-05 11:59:59
【问题描述】:

更新日期: 2009 年 9 月 2 日 - 修改问题,提供更好的示例,增加赏金。


嗨,
我正在使用数据库和实体(域对象)之间的数据映射器模式构建一个 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());

同样,微不足道,但此示例已简化。重复代码仍然适用。

在我看来,拥有某种可以处理的中心对象是有意义的:

  • 检索一个(或多个)站点和所有必要的关联实体
  • 使用新的关联实体创建新的站点实体
  • 获取一个(或多个)站点并保存它和所有关联实体(如果它们已更改)

那么回到我的问题,这个对象应该是什么?

  • 现有的映射器对象?
  • 基于存储库模式的东西?*
  • 基于工作单元模式的东西?*
  • 还有别的吗?

* 我不完全理解其中任何一个,你可能猜到了。

是否有解决此问题的标准方法,有人可以简要说明他们将如何实现它吗?我不是在寻找任何人来提供一个完全有效的实现,只是理论。

谢谢,
杰克

【问题讨论】:

    标签: oop domain-driven-design theory encapsulation


    【解决方案1】:

    使用存储库/服务模式,您的存储库类将为您的每个实体提供一个简单的 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,所以如果语法上有问题,请原谅。

    【讨论】:

    • 好的,有道理,需要阅读服务模式。但是,(我已经遇到过这个问题),存储库和映射器之间的区别是什么。我的映射器已经提供了一个 CRUD 接口,那么为什么还要在顶部添加存储库层呢?
    • 另外,您应该为每种类型的实体提供服务,还是只为您想要定制的实体提供服务?
    • 我认为存储库与您的映射器非常相似,您可以保留它。新组件是服务层。
    • 理想情况下,您的表示层将只访问服务层而不是直接访问映射器(因此需要为每种实体类型提供服务,或者为每组相关实体类型提供服务)。这样,如果需要一些特殊的逻辑,您只需要修改服务方法。
    • 太好了,感谢您的帮助!我必须尽快开始实际实施一些东西,所以要睡在上面并决定如何做这一切。可能还会有更多问题。
    【解决方案2】:

    [编辑:此条目试图解决这样一个事实,即编写自定义代码来直接处理情况通常比尝试将问题融入模式更容易。]

    模式在概念上很好,但它们并不总是“映射”。经过多年的高端 PHP 开发,我们已经确定了一种非常直接的方式来处理此类问题。考虑一下:

    文件:Site.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 不会在请求之间保持“状态”,因此创建一个对象实例只是为了立即销毁它通常是没有意义的。

    【讨论】:

    • 这并没有真正解决使用(创建新的、创建现有的和保存)多个相关实体的问题。
    【解决方案3】:

    我可能会先将通用任务提取到某个辅助方法中,然后等待查看设计的要求。感觉现在说还为时过早。

    你会给这个方法起什么名字?该名称通常暗示方法所属的位置。

    【讨论】:

    • 嗯,很好的问题 :-) ,我想我需要多考虑一下。
    【解决方案4】:
    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();
    

    【讨论】:

    • 虽然这是一个优雅的解决方案,但不适用于我的设置,根据数据映射器模式,我的实体不了解映射器,因此它们没有保存方法。实体被解析回映射器的保存方法以保存到数据库。
    【解决方案5】:

    将您的 Site 对象设为Aggregate Root 以封装复杂的关联并确保一致性。

    然后创建一个SiteRepository,负责检索站点聚合并填充其子级(包括所有页面)。

    您将不需要单独的 PageRepository(假设您没有将 Page 设为单独的聚合根),并且您的 SiteRepository 也应该负责检索 Page 对象(在您的情况下,使用现有的 Mappers)。

    所以:

    $siteRepository = new SiteRepository($myDbConfig);
    $site = $siteRepository->findById(1); // will have Page children attached
    

    然后findById 方法将负责查找站点的所有页面子项。这将具有与 CodeMonkey1 给出的答案类似的结构,但是我相信您将通过使用聚合和存储库模式受益更多,而不是为此任务创建特定的服务。站点聚合的任何其他检索/查询/更新,包括其任何子对象,都将通过相同的 SiteRepository 完成。

    编辑:Here's a short DDD Guide 以帮助您了解术语,但如果您想要了解完整情况,我真的建议您阅读 Evans

    【讨论】:

    • 好的,那么您会设置一个“存储库”来执行与 CodeMonkey1 建议的服务层类似的任务吗?术语的混淆确实让我感到困惑(这是我不得不问这个问题的原因之一)似乎(而不仅仅是在 SO)人们可以互换使用映射器、存储库和服务层。
    • 你说得对,这些术语可能会让人很困惑——尽管这被标记为领域驱动设计,这就是我使用 DDD 术语的原因。在 DDD 中,存储库的任务是将持久层隐藏到域的其余部分。在这种情况下,我认为不需要 Service。
    猜你喜欢
    • 2010-11-14
    • 1970-01-01
    • 2011-11-18
    • 2023-04-05
    • 1970-01-01
    • 2011-01-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多