β

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

ImportNew 15 阅读
原文出处: zetaplusae

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

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

第一步:编写贫血实体

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

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.
作者:ImportNew
原文地址:贫血领域模型是如何导致糟糕的软件产生, 感谢原作者分享。

发表评论