Статьи

Отчеты об отслеживании ошибок — часть 3 — стратегия и пакет

IMG_0742 Это третий блог в серии, в котором подробно рассказывается об отслеживании ошибок приложений. В этой серии статей я пишу легковесное, но промышленное приложение, которое периодически сканирует файлы журналов приложений, ищет ошибки и, если таковые обнаружены, генерирует и публикует отчет.

Если вы читали первый блог в серии, вы, возможно, помните, что я первоначально сказал, что мне нужен класс Report и что «если вы посмотрите на код, вы не найдете класс с именем Report, он был переименован в Results и подвергся рефакторингу». создать интерфейс Formatter, классы TextFormatter и HtmlFormatter вместе с интерфейсом Publisher и классом EmailPublisher ». В этом блоге рассказывается о процессе разработки, освещаются причины рефакторинга, и как я пришел к окончательной реализации.

Если вы читаете дальше, вы можете подумать, что приведенная ниже логика проектирования несколько надумана. Это потому что это так. Фактический процесс перехода от класса Report классу Results , интерфейсам Formatter и Publisher вместе с их реализациями, вероятно, занял всего несколько секунд. однако, написание всего этого заняло некоторое время. История дизайна выглядит так …

Если у вас есть класс с именем Report как вы определяете его ответственность? Вы можете сказать что-то вроде этого: «Класс Report отвечает за генерацию отчета об ошибке. Кажется, это соответствует принципу единой ответственности , поэтому мы должны быть в порядке … или мы? Утверждение, что «Отчет» отвечает за создание отчета, довольно тавтологично. Это все равно что сказать, что стол отвечает за то, чтобы быть столом, это ничего не говорит нам. Нам нужно разбить это дальше. Что означает «создание отчета»? Какие шаги предпринимаются? Размышляя об этом, для составления отчета нам необходимо:

  1. Маршалл данные ошибки.
  2. отформатировать данные об ошибках в читаемый документ.
  3. опубликовать отчет в известном месте назначения.

Если вы включите все это в определение ответственности класса Report , вы получите что-то вроде этого:

«Класс Report отвечает за сбор данных об ошибках, их форматирование в удобочитаемый документ и публикацию отчета в известном месте назначения».

Очевидно, что это нарушает принцип единой ответственности, потому что класс Report имеет три обязанности вместо одной; Вы можете сказать, используя слово «и». Это действительно означает, что у нас есть три класса: один для обработки результатов, один для форматирования отчета и один для публикации отчета, и эти три слабосвязанных класса должны сотрудничать, чтобы получить этот отчет.

Если вы посмотрите на первоначальные требования, пункты 6 и 7 сказали:

1
2
6. When all the files have been checked, format the report ready for publishing.
7 . Publish the report using email or some other technique.

Требование 6 довольно простое и конкретное, мы знаем, что нам нужно отформатировать отчет. В реальном проекте вам придется либо придумать формат самостоятельно, либо спросить клиента, что они хотели видеть в своем отчете.

Требование 7 несколько более проблематично. Первая часть в порядке, там написано «публиковать отчет по электронной почте», и с Spring это не проблема. Вторая очень плохо написана: какая другая техника? Это требуется для этого первого выпуска? Если это был проект в реальном мире, которым вы зарабатываете на жизнь, тогда вам нужно задать несколько вопросов — очень громко, если это необходимо. Это потому, что не поддающееся количественному определению требование будет влиять на временные рамки, что также может сделать вас плохо выглядящим.

Опрос плохо сформулированных требований или историй является ключевым навыком, когда дело доходит до хорошего разработчика. Если требование неверное или расплывчатое, никто не поблагодарит вас, если вы просто придумали и интерпретировали его по-своему. То, как вы формулируете свой вопрос, — это другое дело. Обычно хорошей идеей является быть профессионалом в этом вопросе и говорить что-то вроде: «извините, у вас есть пять минут, чтобы объяснить мне эту историю, я ее не понимаю». Есть только несколько ответов, которые вы получите:

  1. «Не мешай мне сейчас, возвращайся позже…»
  2. «О, да, это ошибка в требованиях — спасибо, что заметил, я разберусь».
  3. «Конечный пользователь был действительно расплывчатым, я свяжусь с ними и уточню, что они имели в виду».
  4. «Я понятия не имею — догадайся …»
  5. «Это требование означает, что вам нужно сделать X, Y, Y…»

… и не забудьте записать свои нерешенные вопросы о требованиях и преследовать их: чья-то бездеятельность может угрожать вашим срокам.

В данном конкретном случае пояснение будет состоять в том, что я собираюсь добавить дополнительные методы публикации в более поздних блогах и что я хочу, чтобы код, разработанный для расширения, был прост, что на простом английском языке означает использование интерфейсов

Снимок экрана 2014-03-09 в 15.35.14

Диаграмма выше показывает, что первоначальная идея класса Report была разделена на три части: Results , Formatter и Publisher . Любой, кто знаком с шаблонами проектирования, заметит, что я использовал шаблон стратегии для внедрения реализаций Formatter и Publisher в класс Results . Это позволяет мне сказать классу Results generate() отчет без того, чтобы класс Results ничего не знал о отчете, его структуре или о том, где он собирается.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
@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 , вы увидите, что есть четыре открытых метода; три, отвечающие за сортировку данных результата, и одна, которая создает отчет:

  • addFile(…)
  • addResults(…)
  • clear(…)
  • generate(…)

Первые три метода, описанные выше, управляют картой хэша Map<String, List<ErrorResult>> results Result s Map<String, List<ErrorResult>> results . Ключи на этой карте — это имена любых файлов журналов, которые FileLocator класс FileLocator , в то время как значения являются ErrorResult компонентов ErrorResult . ErrorResult компонент ErrorResult — это простой внутренний класс EJB-компонента, который используется для группировки деталей всех найденных ошибок.

addFile() — это простой метод, который используется для регистрации файла в классе Results . Он генерирует запись в карте results и создает пустой список. Если это остается пустым, то мы можем сказать, что этот файл без ошибок. Вызов этого метода не является обязательным.

addResult() — это метод, который добавляет новый результат ошибки на карту. После проверки входных аргументов с помощью org.apache.commons.lang3.Validate он проверяет, присутствует ли этот файл в карте результатов. Если это не так, он создает новую запись, прежде чем, наконец, создать новый компонент ErrorResult и добавить его в соответствующий List на Map .

Метод clear() очень прост: он удалит текущее содержимое карты results .

Оставшийся открытый метод generate(…) отвечает за генерацию окончательного отчета об ошибке. Это наша реализация шаблона стратегии, принимающая два аргумента: реализацию Formatter реализацию Publisher. Код очень прост, так как нужно рассмотреть только три строки. Первая строка вызывает реализацию Formatter для форматирования отчета, вторая публикует отчет, а третья строка регистрирует любую ошибку в случае сбоя при создании отчета. Обратите внимание, что это универсальный метод (как показано <T>, прикрепленным к сигнатуре метода). В этом случае, единственное «Gotcha», который нужно остерегаться, это то, что этот «T» должен быть одинакового типа для обоих
Реализация Formatter реализация Publisher . Если это не так, то все рухнет.

1
2
3
4
public interface Formatter {
 
  public <T> T format(Results report);
}

Formatter представляет собой интерфейс с одним методом: public <T> T format(Results report) . Этот метод принимает класс Report в качестве аргумента и возвращает отформатированный отчет любого типа, который вам нравится

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@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(…) :

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

Это учебный пример того, что представляет собой редко используемый видимость пакетов Java. Класс Results класс TextFormatter должны совместно сгенерировать отчет. Для этого коду TextFormatter необходим доступ к данным класса Results ; однако эти данные являются частью внутренней работы класса Result и не должны быть общедоступными. Следовательно, имеет смысл сделать эти данные доступными через package private метод package private , а это означает, что только те классы, которым нужны данные, чтобы взять на себя распределенную ответственность, могут получить доступ к ним.

Заключительная часть создания отчета — публикация отформатированных результатов. Это снова делается с использованием шаблона стратегии; Второй аргумент метода generate(…) класса Report — это реализация интерфейса Publisher :

1
2
3
4
public interface Publisher {
 
  public <T> boolean publish(T report);
}

Это также содержит единственный метод: public <T> boolean publish(T report); , Этот универсальный метод принимает аргумент report типа ‘T’, возвращая true, если отчет успешно опубликован.

Как насчет реализации (й)? этого класса? Первая реализация использует классы электронной почты Spring и станет темой моего следующего блога, который вскоре будет опубликован…

Если вы хотите посмотреть другие блоги этой серии, загляните сюда …

  1. Отслеживание исключений приложений с помощью Spring
  2. Отслеживание исключений в Spring — часть 2 — шаблон делегата