使用Spring跟踪应用异常(3)—策略模式与包级私有

这是轻松追踪应用程序错误系列的第三篇博客。在这个系列中,我写了一个轻量级但是具有工业生产力的应用来定期扫描日志文件,查找错误。如果发现任何错误,则会生成并发出一个报告.

如果你已经阅读了这个系列的第一篇文章。你可能还记得我一开始讲过我需要一个Report类。如果你去查看代码,会发现找不到Report类。该类被重命名为Results并且重构产生了Formatter接口、TextFormatter和HtmlFormatter两个类,以及Publisher接口和EmailPublisher类。这篇博客涵盖了整个设计过程、强调了重构背后的理由,及我最终是怎么实现的。

如果你继续读下去,你可能会认为以下给出的设计逻辑还是有一定思想的。原因是这样的,虽然实际的处理过程,从Report类到Results类,Formatter 和Publisher接口及其他们的实现可能瞬间就能创建出来。但是把这些全部写下来却也花了我不少时间,整个设计的过程是这样的……

如果你有一个名为Report的类,那你怎样定义该类的责任?你可能会这样说:“Report 类负责产生错误的报告”,这个看起来貌似符合单一职责原则,所以这样做是OK的……确实是吗?这样说一个Report类负责产生报告其实是语义重复的。就像说一张桌子的作用就是作为一张桌子,其实这等于没说。我们需要进一步的去阐述,“生成一个报告”的意思是什么?涉及到哪些步骤?思考一下,生成一个报告我们需要做:

1、抽取异常数据;
2、格式化异常数据使其成为可读文件;
3、发送该报告到一个已知的目标位置。

如果在把这些包含在Report类中,该类的作用你将定义成:“Report类负责抽取异常数据,并将数据格式化为可读文件,然后将报告发送到目标地点。”

很明显,这违背了单一职责原则,因为Report 类有三个责任而不是一个。虽然你可以通过’and’将他们连接起来。但从实际意义来讲,我们需要三个类:一个用于处理结果,一个用于格式化报告,还有一个用于发送报告。这个三个松耦合的类必须合作完成报告的交付。

如果你回过去查看最原始的需求,第六点和第七点是这样说的:

6、当所有的文件验证完成后,格式化该报告并准备发送。
7、通过邮件或者其他技术发送报告。

需求的第六点非常直接和具体,我们知道我们需要格式化该报告。在一个实际的项目中,你可以自己定义格式,也可以询问你的客户,看他们想要看到什么格式样的报告。

需求的第七点存在一些疑问。第一部分是没问题的,它说“用email发送邮件”,这个使用Spring就可以了。第二部分写的非常不好:哪个才是其他技术?这是第一个版本的要求吗?如果这是一个真实的项目,是一个你为了生计而要做的项目,有几个地方你需要大声去询问——如果有必要的话。这是因为一个无法量化的需求将严重影响项目的进度,这也使你看起来很差劲。

质问一个不好的需求或者事件是成为一个优秀的开发者的一项核心技能。如果一个需求是错误的或者模糊不清的,而如果你仅仅按照你自己的理解去做事情是没有人会感谢你的。怎样分析一个需求是另外一个问题。通常来说一个好的办法是“专业化”。你可以这样说:“打扰一下,你能花五分钟的时间给我解释一下这个事情吗?我有点不清楚。”你将可能得到几种答案,他们经常会说:

1、“现在不要来打扰我,一会再说……”
2、“哦,对的。这是需求中的一个错误,非常感谢你指出,我马上整理”。
3、“这个地方终端用户确实很模糊。我马上联系他们,弄清楚到底是什么意思”。
4、“我也不知道,你猜一下……”
5、“这个需求的意思是你需要做X,Y,Y……”

记得记下你未完成的需求问题,并督促他们完成:其他人的不作为将威胁到你项目的最后期限。

这种特殊的情况,将在之后的博客中弄清楚,我需要增加额外的发送方法。我想让设计出可扩展的代码,意思就是说要用接口……

上面的这个图显示了Report类的初始意图需要被分成三个部分:Results、Formatter和Publisher。任何熟悉设计模式的人都将看到我使用了策略模式将Formatter和Publisher的实现注入到Results类中。这允许Results类产生一个报告,但Results类不知道任何关于报告的结构以及被发送的地方。

    @Service
    public class Results {

      private static final Logger logger = LoggerFactory.getLogger(Results.class);

      private final Map<String, List<ErrorResult>> results = new HashMap<String, List<ErrorResult>>();

      /**
       * Add the next file found in the folder.
       *
       * @param filePath
       *            the path + name of the file
       */
      public void addFile(String filePath) {

        Validate.notNull(filePath);
        Validate.notBlank(filePath, "Invalid file/path");

        logger.debug("Adding file {}", filePath);
        List<ErrorResult> list = new ArrayList<ErrorResult>();
        results.put(filePath, list);
      }

      /**
       * Add some error details to the report.
       *
       * @param path
       *            the file that contains the error
       * @param lineNumber
       *            The line number of the error in the file
       * @param lines
       *            The group of lines that contain the error
       */
      public void addResult(String path, int lineNumber, List<String> lines) {

        Validate.notBlank(path, "Invalid file/path");
        Validate.notEmpty(lines);
        Validate.isTrue(lineNumber > 0, "line numbers must be positive");

        List<ErrorResult> list = results.get(path);
        if (isNull(list)) {
          addFile(path);
          list = results.get(path);
        }

        ErrorResult errorResult = new ErrorResult(lineNumber, lines);
        list.add(errorResult);
        logger.debug("Adding Result: {}", errorResult);
      }

      private boolean isNull(Object obj) {
        return obj == null;
      }

      public void clear() {
        results.clear();
      }

      Map<String, List<ErrorResult>> getRawResults() {
        return Collections.unmodifiableMap(results);
      }

      /**
       * Generate a report
       *
       * @return The report as a String
       */
      public <T> void generate(Formatter formatter, Publisher publisher) {

        T report = formatter.format(this);
        if (!publisher.publish(report)) {
          logger.error("Failed to publish report");
        }
      }

      public class ErrorResult {

        private final int lineNumber;
        private final List<String> lines;

        ErrorResult(int lineNumber, List<String> lines) {
          this.lineNumber = lineNumber;
          this.lines = lines;
        }

        public int getLineNumber() {
          return lineNumber;
        }

        public List<String> getLines() {
          return lines;
        }

        @Override
        public String toString() {
          return "LineNumber: " + lineNumber + "\nLines:\n" + lines;
        }
      }
    }

先谈谈Results类的代码,你可以看出来该类有四个公共的方法:三个负责处理结果数据,一个负责产生报告。

前三个方法用于管理Results类内部的Map<String, List<ErrorResult>>这个hashmap。该map的keys是日志文件的名称,这个名称通过FileLocator类查找,而map的values是一个ErrorResult实体的列表。ErrorResult是一个简单内部实体类,用于组合错误信息的细节。

addFile()是Results类中一个简单的方法。用于注入文件,它产生一个结果map和一个空的列表。如果一直保持为空,则我们可以说这个文件是没有错误的。该方法的调用时可选的。

addResult()方法是增加新的错误信息到map中,通过使用org.apache.commons.lang3.Validate类来测试该文件是否已经在结果map中。如果不是,则在最终生成新的ErrorResult实体之前创建一个新的入口,并将它加入到Map中的一个合适的列表中。

clear()方法非常直接:清除结果map的当前内容。

剩下的公共方法generate(…)负责生成最终的错误报告。它是通过策略模式实现的,包括两个参数:一个Formatter的实现和一个Publisher的实现。该方法也非常直接,只有三行代码需要考虑。第一行是调用Formatter 的实现类格式化报告,第二行是发送该报告,第三行是如果报告生成失败,则记录相关的错误日志。注意这个泛型方法(将<T>绑定到方法签名上)。这种情况下,要注意到’T’跟 Formatter的实现和Publisher 的实现是同一个类型。如果不是,整个事情都会奔溃了。

    public interface Formatter {

      public <T> T format(Results report);
    }

Formatter接口只有一个方法:public <T> T format(Results报告).这个方法需要一个Report类作为参数,并可以将已经格式化的报告作为任意类型返回。

    @Service
    public class TextFormatter implements Formatter {

      private static final String RULE = "\n==================================================================================================================\n";

      @SuppressWarnings("unchecked")
      @Override
      public <T> T format(Results results) {

        StringBuilder sb = new StringBuilder(dateFormat());
        sb.append(RULE);

        Set<Entry<String, List<ErrorResult>>> entries = results.getRawResults().entrySet();
        for (Entry<String, List<ErrorResult>> entry : entries) {
          appendFileName(sb, entry.getKey());
          appendErrors(sb, entry.getValue());
        }

        return (T) sb.toString();
      }

      private String dateFormat() {

        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
        return df.format(Calendar.getInstance().getTime());
      }

      private void appendFileName(StringBuilder sb, String fileName) {
        sb.append("File:  ");
        sb.append(fileName);
        sb.append("\n");
      }

      private void appendErrors(StringBuilder sb, List<ErrorResult> errorResults) {

        for (ErrorResult errorResult : errorResults) {
          appendErrorResult(sb, errorResult);
        }

      }

      private void appendErrorResult(StringBuilder sb, ErrorResult errorResult) {
        addLineNumber(sb, errorResult.getLineNumber());
        addDetails(sb, errorResult.getLines());
        sb.append(RULE);
      }

      private void addLineNumber(StringBuilder sb, int lineNumber) {
        sb.append("Error found at line: ");
        sb.append(lineNumber);
        sb.append("\n");
      }

      private void addDetails(StringBuilder sb, List<String> lines) {

        for (String line : lines) {
          sb.append(line);
          // sb.append("\n");
        }
      }
    }

这真是一段无聊的代码。这段代码要做的事情就是使用StringBuilder创建一个报告类,并非常小心的拼接文字直到报告的完成。这里唯一感兴趣的点format(…)方法的第三行代码:

    Set<Entry<String, List<ErrorResult>>> entries = results.getRawResults().entrySet();

这是一个关于Java中很少使用到包级可见的例子。这个Results类和TextFormatter类不得不共同合作生成报告。这样做的原因是,TextFormatter类需要访问Results类的数据。但是,这部分数据Result类内部工作的一部分,不应该被公布出来。因此,这看起来好像是通过包的私有方法访问数据,意思就是说,只有那些需要承担分配责任的类才能持有它。

生成报告的最后一部分是发送已经格式化的结果。这再次需要用到策略模式;Report类的generate(…)方法的第二个参数是一个Publisher接口的实现。

    public interface Publisher {
      public <T> boolean publish(T report);
    }

这个类同样只包含一个方法:public <T> boolean publish(T report);。这个泛型方法接收一个T类型的报告参数,如果该报告成功发送,则返回true。

那这个类怎么实现呢?第一种实现是使用Spring的email类,这是我下一篇博客的主题,该博客会尽快发布…

这篇博客中的代码的Github地址:https://github.com/roghughe/captaindebug/tree/master/error-track

原文链接: captaindebug 翻译: ImportNew.com - paddx
译文链接: http://www.importnew.com/11099.html
[ 转载请保留原文出处、译者和译文链接。]



相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部