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.

segunda-feira, 2 de março de 2009

Como não criar um framework

Muitas pessoas tem a bendita ideia de pegar seus códigos fontes, a maneira como elas desenvolvem, seus padrões e criar um framework, uma arquitetura de referência para impor na empresa e todos outros desenvolvedores usarem. Geralmente esses frameworks são baseados em outros frameworks que irão evoluir e com certeza o framework criado não vai conseguir evoluir junto, causando um puta atraso para a empresa e também para os desenvolvedores que são obrigados a usar a gambiarra.

Por outro lado existem pessoas que realmente fazem frameworks úteis, que não tem o objetivo de resolver todos os problemas do mundo, e sim um problema especifico e que ainda não tenha solução.

De qualquer forma, se você ainda tem a ideia de fazer uma arquitetura de referência para resolver a fome do mundo e definir todas as camadas de todos futuros sistemas da sua empresa, ou então tem uma ótima ideia para um framework realmente útil, vale a pena ler esse comentário feito pelo Rod Johnson, criador e CEO do Spring em entrevista para a revista Java Magazine deste mês:

Houve uma série de decisões de projeto / design que foram importantes para que o Spring prevalecesse, uma delas foi que muitos frameworks são baseados na idéia de restringir os desenvolvedores, acreditando que os desenvolvedores e projetistas do framework sabem mais que os usuários do framework. E por isso um dos objetivos do framework é checar e controlar o que os desenvolvedores fazem.

Tivemos uma abordagem diferente com Spring, acreditando que desenvolvedores são profissionais que merecem ser tratados com respeito, ao invés de usar rótulos e forçá-los a trabalhar da forma que queremos que eles trabalhem.

Confesso que conheço muitos desenvolvedores que merecem ser presos e controlados. Nesse comentário, a frase que mais me chamou atenção foi: desenvolvedores são profissionais que merecem ser tratados com respeito. Esse pensamento já vem a algum tempo martelando minha cabeça. Hoje sou muito a favor de tratar desenvolvedores como profissionais, não precisamos e nem devemos fazer frameworks para controlar as coisas que esses caras fazem no sistema, afinal, ele é um profissional, recebe por isso e deve agir e ser cobrado como tal. Caso ele não tenha atitude nem conhecimento necessário para o desenvolvimento no projeto, na minha opinião ele deve ir para outro projeto, para não ser radical e dizer que ele não é necessário para empresa. As reuniões de retrospective e diária do scrum são ótimas para identificar esses caras ;).

Enfim, se for criar um framework, crie algo no espírito do Spring, tenha em mente como alvo desenvolvedores profissionais. Tendo essa mentalidade, caso sua ideia é fazer uma arquitetura de referência, você provavelmente já desistiu. O segundo apelo é para os responsáveis pela contratação de desenvolvedores, por favor, contratem bons profissionais e ajude as arquiteturas de referência a morrerem de vez.