【问题标题】:DDD - Access to the state of the Entity in RepositoryDDD - 访问存储库中实体的状态
【发布时间】:2012-02-11 14:38:23
【问题描述】:

我觉得我缺乏好的设计经验,我可能会把事情复杂化,如果我这样做了,请告诉我:)

让我们看一个用户实体和用户存储库的示例。 我将从存储库开始

class UserRepository {
  public function save(User $user) {
    if($user->getStatus() == User::STATUS_NEW)
      $this->getDataAccessObject()->insert($user->getState());
    else
      $this->getDataAccessObject()->update($user->getState());
    $user->setStatus(User::STATUS_MANAGED);
  }
}

以及它自己的用户

class UserEntity {
  const STATUS_NEW = 1;
  const STATUS_MANAGED = 2;

  private $_status = self::STATUS_NEW; 
  private $_state = array();

  public static function create($username, $password) {
    return new UserEntity(array('Username' => $username, 'Password' => $password));
  }

  public function __construct(array $state) {
    $this->_state = $state;
  }

  public function getState() {
    return $this->_state;
  }

  public function getStatus() {
    return $this->_status;
  }

  public function setStatus($status) {
    $this->_status = $status;
  }
}

在我问这个问题之前,关于代码的注释很少:

  1. 它的php(难懂的C++\C#\Java应该很容易理解)

  2. 为了示例的简单性,我省略了基类(如抽象存储库和抽象实体,UserRepository 和 UserEntity 从中继承)。

  3. 我放弃了工厂对象/类模式,而是更喜欢在自身的实体中使用工厂方法模式。

  4. 工厂方法在这个例子中看起来是多余的,可以替换为

    $user = UserEntity(array('Username' => $username, 'Password' => $password));

但实际上它有点复杂,因为工厂接受“原始”数据(例如来自 POST 表单)并创建所有需要的实体或值对象,然后创建一个有效的用户实体(所以实体中的密码可能不是真正的密码,而是一个包含密码哈希而不是密码的值对象。

现在问题来了:

我并不完整,因为我向世界公开了 getState() 和 setStatus() 方法。这些方法应该只在存储库中使用,但由于它们是公共的,没有什么能阻止我在任何地方访问实体的状态和/或修改它的状态。同样,这可能是过度反应和过度复杂化,但我觉得它不对。

我找到的唯一“解决方案”是通过存储库传递所有内容,例如

class Entity {
  public static function create($identity, $param1, $param2, Repository $r) {
    $state = $r->getNewState($identity, $param1, $param2);
    return new Entity($state);
  }

  private $_state = null;

  public function __construct(State $state) {
    $this->_state = $state;
  }

  public function getIdentity() {
    $this->_state->getIdentity();
  }
}

class Repository {
  private $_states = array();

  public function getNewState($identity, ...) {
    $state = new State($identity, ...);
    $this->_states[$identity] = $state;
    return $state;
  }

  public function save(Entity $entity) {
    $id = $entity->getIdentity();
    //maybe check if such entity is managed by this repository..
    if($this->_states[$id]->getStatus() === State::STATUS_NEW)
      $this->getDataAccessObject()->insert($this->_states[$id]->toArray());
    else
      $this->getDataAccessObject()->update($this->_states[$id]->toArray());
    $this->_states[$id]->setStatus(State::STATUS_MANAGED);
  }
}

class State {
  const STATUS_NEW = 1;
  const STATUS_MANAGED = 2;

  private $_state = array()
  private $_identity = 'something';

  public function getIdentity() {
    return $this->_state[$this->_identity];
  }

  public function toArray() {
    return $this->_state;
  }
}

对我来说它看起来“更正确”,因为在这里我不公开实体的内部状态,只有存储库知道它。

那你怎么看?暴露实体的内部状态可以吗?或者也许第二个例子是设计这种系统的更好方法?或者你知道更好的方法?

非常感谢!

【问题讨论】:

    标签: php design-patterns domain-driven-design


    【解决方案1】:

    首先:不要对Entity和Repository使用单一的基类;它们是不相关的,你这样做会破坏每一个坚实的原则:)

    您可以通过多种不同的方式来解决此问题。首先想到的是DAO,数据访问对象模式。我注意到你在你的存储库实现中使用了这个术语,但我认为你有点倒退了......通常一个实体会有一个相应的 DAO,它负责持久化该特定实体。

    UserEntity myEntity = new UserEntity("username","password");
    UserDAO myDAO = new UserDAO(myEntity)
    myDao.Create();
    

    或

    UserDAO myDAO=new UserDAO();
    UserEntity myEntity=myDAO.GetByUsername("username");
    

    在这种情况下,UserDAO 和 UserEntity 都可以扩展 DTO 基类,您可以将业务逻辑保留在实体中,并将​​持久性代码保留在 DAO 中,从而消除所有重复的字段和(颤抖)属性。实体将承担业务逻辑的单一职责,而 DAO 将承担持久性的单一职责。 Repository 模式可用于抽象存储基础架构,但在使用 DAO 时通常不值得。

    public class UserDTO
    {
        protected bool banned;
        protected string username;
        protected string password;
    }
    
    public class UserEntity : UserDTO
    {
        public void BlockUser();
        public void ChangePassword();
    }
    
    public class UserDAO : UserDTO
    {
        private int Id;
        public void Create();
        public void Update();
        public UserEntity GetByUsername(string userName);
        public void Delete();
    }
    

    现在这看起来就像 hakre 所说的一样,但是存储库模式和 DAO 模式之间存在很大差异。

    如果我对 PHP 中的成员可见性选项有所了解(例如,除了公共/私有/受保护的范围之外,您还有其他范围吗?内部?)我可能会给您一些关于如何使用存储库模式实现它的建议,但 DAO 似乎喜欢它应该在这里做......

    编辑:我突然想到我在方法签名中提供了一些误导性信息; DAO 不应该返回一个实体,一个实体应该有一个带有 DAO 的构造函数,并从中创建一个新的......

    编辑 2:另一种方法是将 UserEntity 视为值对象。不要公开状态的 setter,而是公开的 getter。存储库使用它获取数据,并在检索实体时简单地通过构造函数传递所有属性。然而,这对您希望如何使用用户对象产生了一些影响,这就是我之前没有建议这样做的原因。

    【讨论】:

    • 一个非常有趣的方法!我不了解 DTO(好像我的 State 课程与您的 DTO 类似)。关于 DAO,我确信 DAO 是访问特定存储的接口,即存储库是没有特定实现的存储,它使用 DAO 访问真实存储(内存、数据库、文件系统),所以只有 DAO 知道如何存储对象而不是存储库。我对你的实现中没有存储库这一事实并不完整,因为 DDD 规定应该有一个。
    • 它并没有规定,实际上,它只是一种用于抽象存储的可用模式,在蓝皮书中得到了宣传:) DAO 提供了类似的抽象,但它不太适合存储聚合。示例:您可以有一个组,它是一组用户的聚合根。您可以有一个 GroupRepository 用于持久化组,而后者又使用一堆用户 DAO 来持久化单个用户。与聚合一起使用时,DAO 变得非常混乱。
    • 哦,从 DDD 中删除的要点是业务领域应该驱动设计,而不是您正在使用的特定基础架构。存储库和 DAO 模式抽象了基础架构,允许您为您的领域定义一种普遍存在的(拼写?)语言并围绕它进行设计,而无需担心技术。
    • 感谢您的评论! :) 那么我的意思是存储库不应该附加到特定的存储,即如果明天我决定将我的用户存储在 xml 文件而不是 MySQL 中,我将不得不将新的 DAO 注入我的存储库而不是编写新的存储库.然而,DAO 是附加的,除非我们决定在 DAO 和存储之间放置另一层,但是 DAO 变成了存储库并且从 DTO 继承它似乎是不对的。能够很好地处理聚合对我来说也很重要,因为我也使用它们。
    • DTO 是数据传输对象,一个花哨的词,表示没有行为(方法)的纯数据结构。在我上面描述的情况下,DTO 将保留所有字段,而所有行为都封装在具有单独职责的扩展类中,因此符合 SRP、OCP 和 LSP。在 DAO 和基础设施之间放置另一层是不必要的,因为这就是 DAO 的重点。存储库对于聚合仍然很好,所以一定要为它们使用它。如果您觉得增加复杂性值得,请为所有内容创建存储库,然后委托给 DAO。
    【解决方案2】:

    为什么要重新发明轮子?对于您正在尝试制作的内容,已经有一个名为 Doctrine2 的企业级库 - 具有更多开箱即用的功能,易于使用并且可以处理您在域中可能遇到的最疯狂的聚合根。

    我建议不要使用“自制”解决方案,因为 AR 的制作和维护非常繁琐,而且需要大量的手动工作,因此需要大量时间。

    【讨论】:

      【解决方案3】:

      这些方法只能在存储库中使用

      如果您愿意,请使每个实体和存储库本身都具有相同的基类,实体 getter / setter 是受保护的成员。这样做了,这些都受到保护,不再公开。

      这将使存储库和实体更紧密地结合在一起,这在您的情况下可能还不错,因为存储库会产生实体。

      【讨论】:

      • 这确实是一种有趣的方法。但它看起来“hackish”。我倾向于遵循“is-a”和“has-a”关系来确定类 A 何时应该从 B 继承或包含 B,并且我无法概述一个基本类供 Entity 和 Repository 继承。感谢您提供有趣的方法,它可能在其他地方非常有用,但在这里没有,抱歉。
      • 在 PHP 中没有“包”或“朋友”之类的东西,因此只允许特定的类组访问其中的成员。
      • 我知道 PHP 中没有“包”或“朋友”,这就是我寻找其他解决方案的原因。这并不意味着我应该使用黑客/技巧来模仿缺乏行为。 Repository 和 Entity 不能从基类继承,至少我不能概述这样的类,当然我可能是错的,在这种情况下,我欢迎接受任何批评和/或建议。
      • 这个建议意味着打破单一责任原则、开放/封闭原则和liskov替换原则。我建议从任何破坏以上所有的潜在设计中运行。
      • @Tobias:是的,它确实打破了这一点。开发人员需要决定哪些权重更高。按照我的回答建议(仅强烈关注可见性部分)可能会给代码带来负担。所以这是谨慎的意思,也许我应该在里面放一个通知。使用 PHP 可以做的另一件事是使用反射让存储库将数据映射到域对象上。反射允许将私有成员公开。这也会带来负担,但与我的第一个建议一样,由开发人员决定。
      猜你喜欢
      • 1970-01-01
      • 2020-03-06
      • 2013-12-05
      • 2017-05-18
      • 2018-08-03
      • 2010-11-24
      • 2011-09-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多