Статьи

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

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

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

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

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

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

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

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

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

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…»

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

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

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

@Servicepublic 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;}
@Overridepublic String toString() {return "LineNumber: " + lineNumber + "\nLines:\n" + lines;}
  }
}

Взяв сначала код Results, вы увидите, что есть четыре открытых метода; три, отвечающие за сортировку данных результата, и одна, которая создает отчет:

  • добавить файл(…)
  • addResults (…)
  • Чисто(…)
  • генерировать (…)

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

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

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

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

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

public interface Formatter {
public <T> T format(Results report);}

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

@Servicepublic class TextFormatter implements Formatter {
private static final String RULE ="\n==================================================================================================================\n";
@SuppressWarnings("unchecked")@Overridepublic <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, аккуратно добавляя текст, пока отчет не будет завершен. Там есть только интересующий объект, и он находится в третьей строке кода в формате (…):

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

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

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

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

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

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


Код для этого блога доступен на Github по адресу: 
https://github.com/roghughe/captaindebug/tree/master/error-track .

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

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