Back-End

27 mai, 2014

Rastreamento de exceções em aplicativos com o Spring: estratégia e pacote privado – Parte 03

100 visualizações
Publicidade

Esse é o terceiro artigo de uma série que está vagamente olhando para rastreamento de erros de aplicativos. Nesta série, estou escrevendo um aplicativo leve mas para uso pesado que verifica periodicamente os arquivos de log da aplicação, procurando por erros e, se forem encontrados, ele gera e publica um relatório.

Se você já leu o primeiro artigo da série, vai se lembrar que eu disse inicialmente que precisava de uma classe Report e, “se você olhar o código, não vai encontrar uma classe chamada Report, pois ela foi rebatizada de Results e refatorada para criar uma interface Formatter, as classes TextFormatter e HtmlFormatter, juntamente com a interface Publisher e a classe EmailPublisher”. Este artigo aborda o processo de design, com destaque para o raciocínio por trás da refatoração e como eu cheguei na implementação final.

Se continuar lendo, pode pensar que a lógica do design feito abaixo é um pouco artificial. Isso é porque de fato é. O atual processo de obtenção da classe Report para a classe Results, as interfaces Formatter e Publisher juntas com suas implementações provavelmente levaram apenas alguns segundos para serem pensadas; no entanto, escrever tudo até o fim levou algum tempo. A história de design foi assim…

Se você tem uma classe chamada Report, então como você define a responsabilidade dela? Você poderia dizer algo como isto: “A classe Report é responsável pela geração de um relatório de erro. Isso parece se enquadrar no princípio de responsabilidade única, estão devemos estar bem… ou estamos mesmo? Dizer que um Report é responsável por gerar um relatório é bastante tautológico. É como dizer que uma classe Table é responsável pelas tabelas, e não dizer nada. Precisamos quebrar isso ainda mais. O que “gerar um relatório” significa? Quais são as etapas envolvidas? Pensando nisso, para gerar um relatório, precisamos:

  1. organizar os dados de erro.
  2. formatar os dados de erro em um documento legível.
  3. publicar o relatório em um destino conhecido.

Se você incluir tudo isso na definição de responsabilidade da classe Report, obterá algo parecido com isso: “a classe Report é responsável por organizar os dados de erro e formatar os dados em um documento legível e publicar o relatório para um destino conhecido”.

Obviamente que isso quebra o princípio da responsabilidade única porque a classe Report tem três responsabilidades em vez de uma. Você pode perceber isso pelo uso da palavra “e”. Isso realmente significa que temos três classes: uma para lidar com os resultados, uma para formatar o relatório e uma para publicar o relatório, e essas três classes levemente relacionadas devem colaborar para conseguir que o relatório seja entregue.

Se você olhar lá trás, nos requisitos originais, nos pontos 6 e 7 eu disse:

6. Quando todos os arquivos forem verificados, formatar o relatório deixando pronto para publicação.

7. Publicar o relatório usando e-mail ou alguma outra técnica.

A exigência 6 é bastante simples e concreta. Sabemos que temos que formatar o relatório. Em um projeto real, você teria que criar a formatação ou perguntar ao cliente o que ele gostaria de ver em seu relatório.

O requisito 7 é um tanto mais problemático. A primeira parte é ok, ele diz “publicar o relatório usando e-mail” e que não há problema com o Spring. A segunda está escrita muito mal: que outra técnica? Isso é necessário para essa primeira versão? Se esse era um projeto do mundo real, que você está fazendo para ganhar a vida, então é o lugar onde você precisa fazer algumas perguntas – bem alto, se necessário. Isso porque um requisito não quantificável terá um impacto nas escalas de tempo, o que também pode fazer você ficar mal.

Questionar requisitos ou histórias mal definidas é uma habilidade fundamental quando se trata de ser um bom programador. Se o requisito está errado ou vago, ninguém vai agradecer se você acabar fazendo as coisas e interpretando do seu próprio jeito. Como você expressa sua pergunta é outra questão… é geralmente uma boa ideia ser “profissional” e dizer algo como: “desculpem-me, vocês têm cinco minutos para explicar essa história para mim, eu não entendo isso”. Normalmente há apenas algumas respostas que você vai receber, e elas são geralmente:

  1. “Não me incomode agora, volte mais tarde…”
  2. “Ah, sim, isso é um erro nos requisitos – obrigado por detectar isso, eu vou resolvê-lo.”
  3. “O usuário final é muito vago aqui, eu vou entrar em contato com eles e esclarecer o que quiseram dizer.”
  4. “Eu não tenho ideia alguma – dê um chute…”
  5. “Essa exigência significa que você precisa fazer X , Y, Z…”

E lembre-se de tomar nota de seus requisitos pendentes e persegui-los: a inatividade de outra pessoa pode ameaçar seus prazos.

Nesse caso particular, o esclarecimento seria eu adicionar métodos de publicação adicionais em artigos posteriores e que eu quero que o código projetado possa ser extensível, que em português claro significa o uso de interfaces… spring-1

O diagrama acima mostra que a ideia inicial de uma classe Report foi dividida em três partes: Results, Formatter e Publisher. Qualquer pessoa familiarizada com Design Patterns vai notar que eu usei o design Strategy Pattern para injetar implementações de um Formatter e um Publisher na classe Results. Isso me permite dizer à classe para gerar resultados um relatório sem que a classe Results saiba nada sobre o relatório, sua construção ou para onde vai.

@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;
    }
  }
}

Vendo o código de Results primeiro, você pode ver que há quatro métodos públicos; três que são responsáveis por mobilizar os dados do resultado e um que gera o relatório:

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

Os três primeiros métodos acima gerenciam os Results internos Map<String, List <ErrorResult>> do mapa de hashs. As chaves que estão nesse mapa são os nomes de todos os arquivos de log que a classe FileLocator encontra, enquanto que os valores são listas do ErrorResult. O programa ErrorResult é uma classe interna simples que é usada para agrupar os detalhes de todos os erros encontrados.

addFile( ) é um método simples que é usado para registrar um arquivo com a classe Results. Ele gera uma entrada no mapa de resultados e cria uma lista vazia. Se ela permanecer vazia, então podemos dizer que esse arquivo é livre de erros. Chamar esse método é opcional.

addResult() é o método que adiciona um novo resultado de erro ao mapa. Depois de validar os argumentos de entrada usando org.apache.commons.lang3. Validate, ele testa se esse arquivo já está no mapa de resultados. Se não estiver, ele cria uma nova entrada, antes de finalmente criar um novo ErrorResult e adicioná-lo à lista apropriada no mapa.

O método clear () é muito simples: ele limpará o conteúdo atual do mapa de resultados.

O métodos público restante, generate ( … ), é responsável por gerar o relatório de erro final. É a nossa implementação do pattern Strategy, tendo dois argumentos: uma implementação do Formatter e uma implementação do Publisher. O código é muito simples, pois há apenas três linhas a serem consideradas. A primeira linha chama a implementação Formatter para formatar o relatório, a segundo publica o relatório e a terceira linha registra qualquer erro se a geração do relatório falhar. Note que esse é um método genérico (como mostrado pelo <T> ligado ao método signature). Nesse caso, a única “pegadinha” a observar é que esse “T” tem que ser do mesmo tipo, tanto para a implementação Formatter quanto para a implementação Publisher. Se não for, a coisa toda vai falhar.

public interface Formatter {

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

Formatter é uma interface com um único método: public <T> T format(Results report). Esse método leva a classe Report como um argumento e retorna o relatório formatado como qualquer tipo você gosta.

@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");
    }
  }
}

Isso é um código muito chato. Tudo que ele faz é criar um relatório usando um StringBuilder, acrescentando cuidadosamente o texto até que o relatório esteja completo. Há apenas um ponto de interesse e que está na terceira linha de código no método format (…):

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

Esse é um caso clássico da visibilidade dos pacotes do Java que raramente são usados. A classe Results e a classe TextFormatter têm que colaborar para gerar o relatório. Para isso, o código TextFormatter precisa de acesso aos dados da classe Results; no entanto, esses dados são parte do funcionamento interno da classe Results e não devem estar disponíveis publicamente. Portanto, faz sentido fazer com que os dados acessíveis através de um método particular de pacotes, o que significa que apenas as classes que precisam dos dados sob sua responsabilidade podem ter acesso a eles.

A parte final de gerar um relatório é a publicação dos resultados formatados. Isso é novamente feito usando o padrão Strategy; o segundo argumento do método generate(…)  da classe Report é uma implementação da interface Publisher:

public interface Publisher {

  public <T> boolean publish(T report);
}

Esse também contém um único método: public <T> boolean publish (T report);. Este método genérico usa um argumento de report do tipo ‘T’ , retornando true se o relatório for publicado com sucesso.

E quanto a implementação(ões) dessa classe? A primeira implementação usa classes de email do Spring e será o tema do meu próximo artigo.

O código para esse artigo está disponível no Github em: https://github.com/roghughe/captaindebug/tree/master/error-track.

Se você quiser olhar para outros posts desta série, dê uma olhada aqui:

  1. Parte 1
  2. Parte 2

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em   http://www.captaindebug.com/2014/03/error-tracking-reports-part-3-strategy.html#.U4MwYygjTN2