贫血领域模型是如何导致糟糕的软件产生

使用贫血领域模型通常被认为是一种反模式,因为它鼓励程序员无意义地重复编写代码。下面我将简短(而琐碎)地用一个例子来阐述这个是如何产生的。我们可以通过细致的规划以及严格的编码规范来避免其发生,但是同样可以获得较好的封装。防止陷入贫血领域模型深坑的难度随项目人数呈指数级增长。

我相信所有人对面向对象都有所认识,但我却有趣地发现一些看似毫无意义的小举措却导致了最终一场大灾难。

第一步:编写贫血实体

在软件开发的某些情况下,我们会在一个领域实体之外实现一些逻辑。这可能是由于一个明确的设计决定或者,更有可能,持久类不能引用外部服务造成了不能将这段逻辑实现在领域对象的内部。把外部服务(依赖)添加到实体对象中将会造成与数据库的交互变的复杂而晦涩难懂。

public class User {
   private final String name;
   private final String emailAddress;

   public User(final String name, final String emailAddress) {
      this.name=name;
      this.emailAddress=emailAddress;
   }

   public String getName() {
      return this.name;
   }

   public String getEmailAddress() {
      return this.emailAddress;
   }
}

第二部:逻辑被实现在外部类中

一个开发组的成员决定他们需要一个用来操作这个实体的方法。这个方法(在我们的例子中)要调用到User对象,但它还需要用到一个User类所不知道的外部服务。这段逻辑被实现在一个帮助类(helper)或者说一个服务类(service)的方法中,并且以某种方式协助了这个实体。这个帮助类不包含自带的数据,并且仅仅从这个实体中获取数据、修改其状态。

public class UserReminderService { // 用户提醒服务
   private IMailService mailService; // 邮件服务
   private IMessageGeneratorService messageGeneratorService; // 消息生成服务

   public void sendReminderMessage(final IUser user) { // 发送一个提醒
      String reminderMessage = this.messageGeneratorService.generateReminderMessage(user.getName);
      this.mailService.sendMessage(user.getEmailAddress(), reminderMessage);
   }

   ...
}

这个并不能实现在User实体中,因为我们根本无法在实体中取得邮件服务或者是消息生成器。到目前为止,这个看起来还不算很糟糕(我们很好地封装了消息的创建以及邮件发送过程),但是这仅仅是“败坏”的开始,然后马上开始让这些不警惕的开发者陷入灾难。哪里错了呢?

UserReminderService是一个游手好闲的类(它掌管了太多其他类的活动)。消息的创建、把它发送出去这些都应该是User类自己的业务逻辑。

public class SignupVerificationService { // 注册确认服务
    private IMailService mailService; // 邮件服务
    private IMessageGeneratorService messageGeneratorService; // 消息生成服务

    public void sendVerificationEmail(final IUser user) { // 发送确认邮件
      String verificationMessage = this.messageGeneratorService.generateVerificationMessage(user.getName);
      this.mailService.sendMessage(user.getEmailAddress(), reminderMessage);
   }
}

第三步:重复代码产生

在此期间,另一个开发者开发了一个全新的组件,同样也使用了User实体。这个新的服务被用来决定注册用户是真的用户而不是一个机器人。

public class SignupVerificationService { // 注册确认服务
    private IMailService mailService; // 邮件服务
    private IMessageGeneratorService messageGeneratorService; // 消息生成服务

    public void sendVerificationEmail(final IUser user) { // 发送确认邮件
      String verificationMessage = this.messageGeneratorService.generateVerificationMessage(user.getName);
      this.mailService.sendMessage(user.getEmailAddress(), reminderMessage);
   }
}

这个开发者可能会发现这个方法与之前的sendReminderMessage方法十分的相似。在这个情况下,他觉得他把验证功能与其他组件分开来的做法十分精明,看上去没有必要为这短短两行代码重用之前的实现。哪里错了呢?

这两个方法看上去十分相似,但是又是不同的,使得开发者认为是两个不同的活动。这里有一种冗余的感觉,但还没有造成问题。

第四步:逻辑变更

从长远来看,越简单的代码会变的越复杂。在这个迭代后期,我们的开发者在sendReminderMessage方法中添加了一些更复杂的逻辑(预处理用户名和校验邮箱地址)。

  public void sendReminderMessage(final IUser user) {
      String formattedUserName = formatUserNameForMessage(user.getName());
      String reminderMessage = this.messageGeneratorService.generateReminderMessage(formattedUserName);
      if (isEmailAddressValid(user.getEmailAddress()) {
         this.mailService.sendMessage(user.getEmailAddress(), reminderMessage);
      }
   }

   public boolean isEmailAddressValid(final String emailAddress) { // 是否邮箱地址有效
      return emailAddress.contains('@');
   }

   public String formatUserNameForMessage(final String userName) { // 为消息格式化用户名
      return userName.toUpperCase();
   }

我们现在有了sendReminderMessage方法的新版本(虽然是一个很简陋的验证系统),使得(曾经相似的)UserReminderService变得相当不同。

哪里错了呢?

用来给向用户发送消息的过程发生了变化 (需要进行校验).  由于该过程没有包含在User类内部,我们就必须追踪它在所有不同形式下的所有实现,然后对它们进行修改. 假设我们意识到SignupVerificationService也需要校验,然后我们为它添加了校验, 我们仍然需要一种能够重复使用这端校验代码的方法.在需要校验的情况下,我们可能会把方法封装到mailService中,但对于其他的逻辑,比如用户姓名格式化,已经被加入到不同的helper/service类中了,该怎么办呢?. 这些代码可能会被多个service类所需要,也可能只有一个service需要.

第五步: 灾难来临

不能被很容易的移入一个外部依赖(比如,mail service)的代码就不得不被service类所共享. 将这些方法放进一个超类中供两个service共享貌似是个好主意.  我们可以看到那样形成的代码对原始的domain没有影响. 不再是User,Message和MailService,我们最终以一群奇怪的生物,就像AbstractUserService, UserValidationService, UserReminderService等等告终.  很快,我们就很难知道新的代码真正属于哪部分,也很难知道需要写那些新代码的方法所处的位置是否会影响它的使用或者重用.

       AbstractUserService
             /\
             |
             |
     ------------------------------------
    |                                  |
UserValidationService       UserReminderService
与此同时另一个开发者也写了另一个service,这个service是用来给某个Department实体(同样也使用email地址)发送消息的.这位开发者想要使用AbstractUserService中的邮箱验证和名字格式化功能,但他的代码是为Departments服务的,而不是Users,因此,代码结构中另一层又出现了:AbstractEntiryService.

哪里错了呢?

我们已经失去了对我们程序结构的控制,我们的开发团队开始发现很难再写出干净的代码. 我们的类需要比实际需求更多的公共方法来维护复杂的类关系

总结

通过贫血的领域模型来保持代码结构整洁并且可维护是当然不可能的.然而,当我们能够使用充血领域模型的时候,维护代码并且保持类接口简洁就变得非常容易了.

public class User {
   //Dependencies
   private IMailService mailService;
   private IMessageService messageService;

   private final String name;
   private final String emailAddress;

  public User(final String name, final String emailAddress) {
      this.name=name;
      this.emailAddress=emailAddress;
   }

    public void sendReminderMessage() {
      deliverMessage( this.messageGeneratorService.generateReminderMessage(this.getName));
   }

   public void sendVerificationEmail() {
      deliverMessage(this.messageGeneratorService.generateVerificationMessage(this.getName));

   }

   private void deliverMessage(final String message) {
      if (isEmailAddressValid(user.getEmailAddress()) {
         this.mailService.sendMessage(user.getEmailAddress(), reminderMessage);
      }
   }

   public String getName() {
      return this.name;
   }
}

注意,我们不再需要email地址的get方法,而且,如果你能原谅我玩数字游戏,我们增加了两个User类的公共方法二不是引入两个(至少)额外的类. 当我们在适当的对象上执行方法的时候比在一个不自然的service对象上执行方法看起来更直观.

MailService和MessageServices仍被允许留在系统中因为它们的角色很明确. 传送邮件是一个清晰的架构问题,应该被从领域对象中通过接口(IMailService)抽象出来.生成消息应该被如何抽象/封装可能是更值得商榷的,但这篇文章就会比我与其的更长了.



相关文章

发表评论

Comment form

(*) 表示必填项

3 条评论

  1. 屈定 说道:

    感觉有点问题,User中含有service,那么User需要被依赖工具所管理才能注入进去,但是这导致domain单例肯定不对。我觉得有外部依赖的应该放到对应User的repository中。

    Thumb up 4 Thumb down 2

  2. 1 说道:

    1

    Thumb up 1 Thumb down 0

跳到底部
返回顶部