逸言

职责与封装

| Comments

面向对象设计的关键,我认为是识别职责,封装合理的对象。缺乏合理的封装,就会缺少正确的领域对象,使得属于共同职责的领域信息散乱分布到系统的各个方法中,导致概念不够清晰,职责混乱,以及代码的重复。然而,如果没有正确地识别职责,又可能导致封装无从谈起,因为我们获得的需求可能是散乱的,既难以抽象概念,又缺乏层次。故而,职责与封装是相辅相成的概念。如下图所示:

我们不能简单地将封装当做是“信息隐藏”。所谓隐藏信息以及具体行为的细节,确实是封装的本质所在,但面向对象思想中的封装,其目的还在于对领域概念以及设计概念的识别。前者是对业务模型的抽象,如电子商务系统中的Product、Order、OrderItem等概念;后者是对设计模型的改进,如在设计模式中引入策略对象、命令对象,DDD中提倡的Repository、Factory对象。这也是为何许多设计者很容易理解封装的概念,但却始终无法做到合理封装的根本原因。

此时,我们需要运用职责驱动设计,通过对职责的识别来提炼这些概念。概念可以起到分类的作用,根据职责对行为与数据进行分类,找到其应该归属的对象,散乱的逻辑就会变得清晰起来。就好似我们对彩球按照颜色进行分类,并放置到不同的位置:

不同的颜色有分明不同的特征,只要不是色盲,分类自然水到渠成。现实中的系统需求自然不如颜色这般泾渭分明,要从纷繁复杂的混沌需求中超脱出来,最好的办法就是按照不同的角度或层次去寻找职责,并用最简单的语言一句话描述这些职责。根据职责的特征,我大致将职责分为三个层次,由外自内分别为:业务价值、业务功能与业务实现,如下图所示:

业务价值基本上体现了这个需求用例(或用户故事)存在的目的,即解释了需求的Why。简言之,只有提供了该职责,则此需求对于客户才是有价值的。这也符合用户故事INVEST原则中的V(Valuable)。没有价值的需求,自然是应该放弃的。故而在识别业务价值时,常常从用户的视角来分析,辨别。

业务价值是职责驱动的入口,因为在寻找到了业务价值之后,我们就可以剖析该价值需要哪些支撑功能(它可以解释需求的What),再由功能继续深入分析,找到实现功能的职责(它可以解释需求的How)。因此,这个模型是一个层层推进的过程。

职责驱动不仅仅可以从文字需求入手,同样可以针对已经实现的代码。甚至我们可以将这种职责驱动看做是一种阅读代码的技巧,通过寻找业务价值,进而分析业务功能和业务实现,对代码形成一个整体的感观,进而通过合理地分配职责改善原有代码。如下代码是《修改代码的艺术》一书中给出的例子:

import javax.mail.*;
import javax.mail.internet.*;

public class MailingListServer {
  public static final String SUBJECT_MARKER = "[list]";
  public static final String LOOP_HEADER = "X-LOOP";

  public static void main(String[] args) {
      if (args.length != 8) {
          System.err.println("Usage: java MailingList <popHost> " + 
          "<smtpHost> <pop3user> <pop3password> <smtpuser> <smtppassword> <listname> " +
          "<relayinterval");
      }
      return;

      HostInformation host = new HostInformation(arg[0]);
      String listAddress = args[6];
      int interval = new Integer(args[7]).intValue();
      Roster roster = null;
      try {
          roster = new FileRoster("roster.txt");
      }catch(Exception e) {
          
      }

      try {
          do {
              try {
                  Properties properties = System.getProperties();
                  Session session = Session.getDefaultInstance(properties, null);
                  Store store = session.getStore("pop3");
                  store.connect(host.pop3Host, -1, host.pop3User, host.pop3Password);
                  Folder defaultFolder = store.getDefaultFolder();
                  if (defaultFolder == null) {
                      return;
                  }
                  Folder folder = defaultFolder.getFolder("INBOX");
                  if (folder == null) {
                      return;
                  }
                  folder.open(FOLDER.READ_WRITE);
                  process(host, listAddress, roster, session, store, folder);
              }catch () {
                  
              }
              try {
                  Thread.sleep(interval * 1000);
              } catch() {
                      
              }

          } while (true)
      }catch () {
          
      }
  }

  private static void process(
      HostInformation host, String listAddress, Roster roster,
      Session session, Store store, Folder folder) throws MessagingException {
      try {
          if (folder.getMessageCount() != 0) {
              Message[] messages = folder.getMessages();
              doMessage(host, listAddress, roster, session, folder, messages);
          }
      }catch () {
              
      }finally {
          folder.close(true);
          store.close();
      }
  }

  private static void doMessage(
      HostInformation host,
      String listAddress,
      Roster roster,
      Session session,
      Folder folder,
      Message[] messages) throws MessageingException {
      FetchProfile fp = new FetchProfile();
      fp.add(FetchProfile.Item.ENVELOPE);
      fp.add(FetchProfile.Item.FLAGS);
      fp.add("X-Mailer");
      folder.fetch(messages, fp);
      for (int i = 0; i < messages.length; i++) {
          Message message = messages[i];
          if (message.getFlags().contains(Flags.Flag.DELETED)) continue;
          System.out.println("message received: " + message.getSubject());
          if (!roster.constainsOneOf(message.getFrom())) continue;
          MimeMessage forward = new MimeMessage(session);
          Address[] fromAddress = message.getFrom();
          InternetAddress from = null;
          if (fromAddress != null && fromAdress.length > 0) {
              from = new InternetAddress(fromAddress[0].toString());
          }
          forward.setFrom(from);
          forward.setReplyTo(new Address[] {
              new InternetAddress(listAddress)
          });

          forward.addRecipients(Message.RecipientType.BCC, roster.getAddresses());
          String subject = message.getSubject();
          if (-1 == message.getSubject().indexOf(SUBJECT_MARKER)) {
              subject = SUBJECT_MARKER + " " + message.getSubject();
          }
          forward.setSubject(subject);
          forward.setSentDate(message.getSentDate());
          forward.addHeader(LOOP_HEADER, listAddress);
          Object content = message.getContent();
          if (content instanceof Multipart) {
              forward.setContent((Multipart)content);
          } else {
              forward.setText((String)content);
          }

          Properties props = new Properties();
          props.put("mail.smtp.host", host.smtpHost);

          Session smtpSession = Session.getDefaultInstance(props, null);
          Transport transport = smtpSession.getTransport("smtp");
          transport.connect(host.smtpHost, host.smtpUser, host.smtpPassword);
          transport.sendMessage(forward, roster.getAddresses());
          message.setFlag(Flags.Flag.DELETED, true);
      }
  }
}

这段代码集中了所有劣质代码的特性,如可读性差,可测试性差,可重用性差,可扩展性差,也很难体现设计者的意图。

是的,意图!意图就是体现业务价值的关键所在,最好能够通过类的名称直接传达这种业务价值。显然,上述代码完全没有做到这一点。通过仔细阅读这段代码,然后从职责的角度入手,就驱使我们思考它究竟做了什么?显然,这段代码的功能就是侦听邮件服务器,并根据实现给定的邮件名单,将收到的邮件转发给邮件名单的相关人士。所以,它的业务价值就是转发邮件。

要做到转发邮件,基本的功能是要能够侦听邮件服务器。我们可以将侦听看做是业务功能。通常,对于实际的需求而言,职责模型并非简单的三层结构,它可能是一种递归的方式,也即可以对业务价值、业务功能甚至业务实现进行不断深入的分解。例如,对于这里的侦听业务功能而言,还可以分解为下一个业务功能,即接收邮件。同时,要实现邮件的转发,还需要发送邮件的支撑。下面就是我们所能够识别出来的职责:

* 转发邮件
    ** 侦听邮件
        *** 接收邮件
    ** 发送邮件

面对如此简短的职责描述,再要识别对象所要封装的概念就变得非常容易了。下图是根据识别出来的职责获得的类图结构:

比较这个设计模型与之前的代码,新的模型无疑在职责分配上占了胜场。并且,每个对象的名称都很好地传达了它所能完成的功能。由于封装将职责的实现细节有效地隐藏,并为它们各自划分了空间,形成各自的职责单元,然后再以弱耦合的形式进行协作。因而,我们可以很容易地对它们进行单元测试,或者对参与协作的行为进行Mock。接口的引入则是为了未来的功能扩展,例如我们不再使用javax.mail库来实现邮件收发,就可以提供不同的实现类。可独立封装的MessageReceiver与MessageSender还可以为系统的其他模块所重用。职责与封装相得益彰,有效地改善了整个设计模型。

Comments