domingo, 21 de junho de 2009

Business, Service e Generic Exceptions

Depois de um período de mudanças (sai de Uberlândia e vim morar em São Paulo), volto a postar. Vou falar sobre minhas impressões sobre os famosos BusinessException, ServiceException, GerericException, enfim, as exceções genéricas presentes em muitos sistemas por ai.

Imagine a arquitetura de uma aplicação web simples, onde temos os controladores acessando interfaces de serviço, até ai tudo bem. Agora imagina que uma dessas interfaces possui um método para incluir usuário, esse método poderia ter tal assinatura:

Usuario insereUsuario(String nome, String email, Date dataNascimento) throws ServiceException;

Parece normal, como muitos sistemas por ai, mas tem um grande problema, aquela exceção. Imagine que um esse método lance exceção quando:

  1. Encontre um usuário com mesmo nome já cadastrado.
  2. Encontre um usuário com mesmo email já cadastrado.
  3. O usuário tem menos de 18 anos.

Em cada uma das exceções a implementação lançaria um ServiceException:

throw new ServiceException("Já existe um usuário com esse nome cadastrado")

throw new ServiceException("Já existe um usuário com esse email cadastrado")

throw new ServiceException("O usuário deve ter pelo menos 18 anos")

Parece perfeito, no controlador eu faria:

Servico servico = ...
try {
    servico.insereUsuario(nome, email, dataNascimento);
} catch(ServiceException e) {
    erros.add(e.getMessage());
}

Economia de linha no try catch, sem varias exceções, a mensagem vem do domínio e eu apenas jogo na tela para o usuário ver.

Mas ai vem dois grandes problemas, o primeiro é encapsulamento e o segundo a internacionalização. Eu, como controlador web ou como qualquer outro cliente da interface de serviço, quero direcionar o usuário da minha aplicação web para meu site de adolescentes caso o usuário não possa ser incluído devido a problemas de idade. Como faria isso? duas formas:

A primeira é antes de chamar o método de serviço testar se ele tem mais de 18 anos, o que resultaria em quebra de encapsulamento da minha lógica de serviço, se no futuro meu serviço mudar e eu aceitar usuários a partir de 16 anos, esse controlador (cliente do meu serviço) precisará ser alterado.

A segunda solução seria testar a mensagem de erro que a exceção de serviço (ServiceException) retornou (arrhhgg), caso for a mensagem de idade invalida, eu faço o direcionamento, o que também seria uma quebra de encapsulamento. Notem que não tem como eu cliente realizar essa lógica minha sem conhecer detalhes da implementação, isso porque nossa interface tem aquele ServiceException genérico, ela não nos diz muita coisa sobre as exceções que podem lançar, caso eu não queira simplesmente mostrar a mensagem para o usuário da minha aplicação web e sim fazer tratamentos diferentes para as varias exceções que modelo pode lançar.

Para quem realiza testes unitários de suas classes, nunca notou algo errado quando precisa fazer um teste para testar uma exceção especifica? Como faz, assim?

@Test
public void testaInclusaoDeUsuarioComNomeJaExistente() {
  try {
    service.incluiUsuario("nomeExistente", "email@teste.com", new Date());
  } catch(ServiceException e) {
    assertEquals(e.getMessage(), "Já existe um usuário com esse nome cadastrado")
  }
}

Perceba a baita quebra de encapsulamento, não é assim que se testa exceções, ou vocês acham que aquelas anotações que alguns frameworks de teste tem (como o springtest) @ExpectedException não servem para nada?! Usando exceções genéricas, elas realmente não servem para nada, pois não tem como usar de forma confiável, pois a exceção pode vir indicando que ocorreu X quando na verdade você queria testar a ocorrência da exceção indicando Y.

O outro problema que comentei foi internacionalização. O chefe vem e fala: vamos vender esse sistema para fora do Brasil, teremos que ter um sistema internacionalizavel. Você pensa: "beleza, coloca um buddle com as mensagens em cada idioma, e mudo todas exceções para ao invés de passar a string da mensagem, passar um código, as exceções retornam o código e meu framework web joga na tela a mensagem associada a esse código".

Mais uma vez quebra de encapsulamento, o que era antes isso:

@Test
public void testaInclusaoDeUsuarioComNomeJaExistente() {
  try {
    service.incluiUsuario("nomeExistente", "email@teste.com", new Date());
  } catch(ServiceException e) {
    assertEquals(e.getMessage(), "Já existe um usuário com esse nome cadastrado")
  }
}

Passou a ser isso:

@Test
public void testaInclusaoDeUsuarioComNomeJaExistente() {
  try {
    service.incluiUsuario("nomeExistente", "email@teste.com", new Date());
  } catch(ServiceException e) {
    assertEquals(e.getMessage(), "nome.do.usuario.ja.existe")
  }
}

Pense nisso, seu serviço provavelmente é stateless, você não sabe qual a localidade do usuário, então não tem como mandar uma mensagem bonita, já formatada para o idioma, pois você não sabe o idioma do seu usuário (a menos que o locale seja passado em todos seus métodos de serviço - nunca pense nisso), tem que mandar esse código mesmo, quem faz a tradução é quem está ali próximo do usuário, conhece ele, ou seja, a camada web, de interface com o usuário, mas não fica estranho?! Os códigos de erros definidos em uma camada de domínio e as mensagens definidas na camada web?!

Mas enfim, minha questão é, em meio de tempos de debates sobre VO, DTO e modelos anêmicos, porque as pessoas têm preguiça de criar exceções e gostam tanto de usar uma exceção genérica? Mesmo com esses problemas já mostrados?!

Como eu acho que deve ser feito: a inclusão de usuário recebe 3 parâmetros, nome, email e data de nascimento, o que pode ocorrer? Não quero dois usuários com mesmo nome nem mesmo email e também não quero usuários com idade menor que 18 anos. Isso é da interface, a minha interface de serviço tem que dizer isso, portanto a assinatura do meu método seria essa:

Usuario insereUsuario(String nome, String email, Date dataNascimento) throws UsuarioJaExistente, IdadeInvalidaException;

Podendo haver uma hierarquia de exceções, como por exemplo:

public class RegistroJaExistenteException extends Exception

public class UsuarioJaExistenteException extends RegistroJaExistenteException

public class NomeUsuarioJaExistenteException extends UsuarioJaExistenteException

public class EmailUsuarioJaExistenteException extends UsuarioJaExistenteException

Agora eu como cliente desse serviço posso fazer uns try catch e tratar as exceções de forma normal, minhas "mensagens" de erro agora estão na assinatura do método, se vier um IdadeInvalidaException eu faço alguma coisa especifica, posso redirecionar para outro site ou posso simplesmente chamar a minha mensagem “usuario.com.idade.invalida" na locale do usuário.

Meus testes unitários ficariam bonitos:

@Test
@ExpectedException(NomeUsuarioJaExistenteException.class)
public void testaInclusaoDeUsuarioComNomeJaExistente() {
  service.incluiUsuario("nomeExistente", "email@teste.com", new Date());
}

Como pode ver, dessa forma, você ira criar varias exceções, para gerenciar todas elas, com certeza você também não criara os packages da sua aplicação no estilo:

br.com.empresa.projeto.dao
 -- todos daos
br.com.empresa.projeto.entity
 -- todos entities
br.com.empresa.projeto.service
 -- todos servicos
br.com.empresa.projeto.exception
 -- todas exceções

Vai à sugestão de criar algo no estilo:

br.com.empresa.projeto.usuario
 Usuario
 UsuarioService
 UsuarioRepository
 UsuarioDAO
 UsuarioJaExistenteException
 EmailUsuarioJaExistenteException
 NomeUsuarioJaExistenteException
 IdadeInvalidaException
 etc

Agrupando em packages o que são relativos ao negócio e não agrupando pelo "tipo" de objetos (DAO's, Entities, Service's, etc).

Caso você já esteja no meio do projeto, não é tarde demais, refatoração faz parte de praticas ágeis, mas você não precisa sair por ai alterando tudo de uma só vez. Você pode considerar sua ServiceException como java.lang.Exception, começar a criar umas classes filhas para os novos desenvolvimentos e aos poucos refatorar o que já esta feito, não vai quebrar o código do seu cliente ou seu controlador (quem usa sua classe de serviço), pois vai continuar lançando ServiceException, com a vantagem que suas interfaces vão ficando aos poucos com uma assinatura mais descente.

Bem, eu já criei sistemas com BusinessException, aprendi com o erro, hoje é muito claro para mim que esse modelo não é certo. Utilizar serviços com exceções genéricas é terrível (a interface não te diz nada sobre o que pode ocorrer), fazer internacionalização fica muito chato, será que a criação de uma classe a mais indicando a exceção é tão custosa assim?! Eu acho que não.