如何避免贫血数据模型?存储库可以注入实体吗?

How to avoid anemic data model? Can repositories be injected into entities?

我有一个不可变的 User 实体:

public class User {
  final LocalDate lastPasswordChangeDate;
  // final id, name, email, etc.
}

如果必须更改用户密码 i.d,我需要添加一个方法来 return 信息。它没有更改超过 passwordValidIntervalInDays 系统设置。

目前的做法:

public class UserPasswordService {
  private SettingsRepository settingsRepository;

  @Inject
  public UserPasswordService(SettingsRepository settingsRepository) {
    this.settingsRepository = settingsRepository;
  }

  public boolean passwordMustBeChanged(User user) {
    return user.lastPasswordChangeDate.plusDays(
        settingsRepository.get().passwordValidIntervalInDays
      ).isBefore(LocalDate.now());
  }
}

问题是如何使上述代码更加面向对象并避免贫血领域模型反模式? passwordMustBeChanged 方法是否应该移动到 User 如果是如何访问 SettingsRepository,应该将它注入 User 的构造函数,还是应该 Settings 实例提供给 ctor,还是 passwordMustBeChanged 方法需要提供 Settings 实例?

SettingsSettingsRepository的代码并不重要,但为了完整起见,这里是:

public class Settings {
  int passwordValidIntervalInDays;
  public Settings(int passwordValidIntervalInDays) {
    this.passwordValidIntervalInDays = passwordValidIntervalInDays;
  }
}

public class SettingsRepository {
  public Settings get() {
    // load the settings from the persistent storage
    return new Settings(10);
  }
}

对于系统范围的密码过期策略,只要您的 UserPasswordService 是域服务而不是应用程序服务,您的方法还不错。在 User 中嵌入密码过期策略会违反 SRP 恕我直言,这也好不了多少。

您还可以考虑类似的东西(使用正确的设置初始化工厂):

PasswordExpirationPolicy policy = passwordExpirationPolicyFactory().createDefault();
boolean mustChangePassword = user.passwordMustBeChanged(policy);


//class User
public boolean passwordMustBeChanged(PasswordExpirationPolicy policy) {
    return policy.hasExpired(currentDate, this.lastPasswordChangeDate);
}

如果最终可以为单个用户指定策略,那么您可以简单地将策略对象存储在 User

您也可以在您当前的设计中使用 ISP,并在您的 UserPasswordService 服务上实现一个 PasswordExpirationPolicy 接口。这将使您以后可以灵活地重构为真实的策略对象,而无需更改 User 与策略的交互方式。

如果你有一个 Password 值对象,你也可以通过类似的东西(密码创建日期将嵌入密码 VO):

//class User
public boolean passwordMustBeChanged(PasswordExpirationPolicy policy) {
    return this.password.hasExpired(policy);
}

只是抛出另一种可能的解决方案是实施一个长 运行 过程,该过程可以进行过期检查并向 PasswordExpiredHandler 发送一个命令,该命令可以将用户标记为密码过期。

我偶然发现了一份文档,它提供了我的问题的答案:

A common problem in applying DDD is when an entity requires access to data in a repository or other gateway in order to carry out a business operation. One solution is to inject repository dependencies directly into the entity, however this is often frowned upon. One reason for this is because it requires the plain-old-(C#, Java, etc…) objects implementing entities to be part of an application dependency graph. Another reason is that is makes reasoning about the behavior of entities more difficult since the Single-Responsibility Principle is violated. A better solution is to have an application service retrieve the information required by an entity, effectively setting up the execution environment, and provide it to the entity.

http://gorodinski.com/blog/2012/04/14/services-in-domain-driven-design-ddd/