quarta-feira, 9 de dezembro de 2009

Repositorio, e especificações como implementar - Parte 2

quarta-feira, 9 de dezembro de 2009
Em meu post anterior citei como fazer a implementação flexível do padrão Repositório e Especificação, citado por Eric Evans juntamente com o Hibernate, realmente as pessoas tem muita confusão de como implementar, uma coisa sempre e certa a sempre 1001 maneiras de implementar algo, e como Eric Evans, em seus exemplos, foi dado do SQL direto a implementação da Especificação e do Repositório, mas nós podemos usar o Hibernate que facilita muito a vida, e ele disse também sobre isto, que muitas vezes o sistema de mapeamento objeto relacional pode nos ajudar.

Mas vamos ao que interessa, no post anterior falei a respeito de como implementar a Especificação e vimos que a mesma serve para manter as regras de negócio da aplicação toda sobre um único lugar, assim o sistema se torna de mais fácil manutenção, e podemos ganhar muito com isto, nem sempre e fácil fazer a implementação destes padrões, visto a grande confusão e insistência em fazer as coisas usando as receitas de bolo da programação.

Algo que com certeza podemos mais cedo ou mais tarde fazer nos nossos sistemas é a junção de filtros para obter um resultado, vimos que a Especificação pode nos ajudar a abstrair do cliente toda a complexidade do filtro da consulta e manter as coisas simples, mas vamos a um exemplo real no sistema.

Digamos que o cliente diga o seguinte a você que está desenvolvendo o sistema.

"O relatório de clientes está bom mas, gostaríamos que na tela que faz a configuração do relatório seja incluído outros filtros onde gostaríamos de filtrar os clientes do relatório que possuem não contatamos a mais de 20 dias, visto que estes clientes devem ter uma atenção especial, assim como já funciona na tela do cadastro, e gostaria além disso também que fosse exibido os que tiveram uma reclamação do nosso serviço no histórico."

Repare que neste dialogo o cliente solicitou que você casa-se ou junta-se filtros diferentes, um filtro já existe, sendo que já está funcionando na tela de cadastro, a Especificação que já está implementada e a de clientes sem contato a mais de 20 dias, agora ele te Especificou algo novo, o cliente que possui uma ou mais reclamações. Vejamos então o que temos e o que deveríamos desenvolver:

public class EspecificacaoClientesComReclamacao implements Especificacao {

    private Integer numReclamacoes;
 
    public EspecificacaoClienteComReclamacao(Integer numReclamacoes){
        this.numReclamacoes = numReclamacoes;
    }
 
    public Criterion configurarCriteria(Criteria criteria,Class alvo){
        if (alvo != Cliente.class) return null;
        return Restrictions.gt("numReclamacoes", numReclamacoes );
    }
}
E a outra especificacao:

public class EspecificacaoClientesSemContato implements Especificacao {
    private Integer dias;
 
    public EspecificacaoClienteSemContato(Integer dias){
        this.dias = dias;
    }
 
    public Criterion configurarCriteria(Criteria criteria,Class alvo){
        if (alvo != Cliente.class) return null;
        Date dataAtual = new Date();  
        Calendar calendarData = Calendar.getInstance();  
        calendarData.setTime(dataAtual);  
        calendarData.add(Calendar.DATE,dias);  
        Date dataInicial = calendarData.getTime();  
  
        return Restrictions.gt("ultimoContato", dataInicial );
    }
}

Repare que no dialogo o cliente queria executar estas duas especificações para obter a lista?

Como fazer isto então? Bem conforme Evans comentou podemos pegar emprestado o conceito de operadores lógicos, o que são estes operadores lógicos? Seria o seguinte:

AND, OR, NOT

Veja como ficaria para usarmos os operadores lógicos, e assim agrupar as duas Especificações:
Especificacao clientesSemContato = new EspecificacaoClientesSemContato();

Especificacao clientesComReclamacao = new EspecificacaoClientesComReclamacao();

Especificacao especificacoes = clientesSemContato.and(clientesComRelamacao);
Repare que na ultima linha juntamos as duas especificações e agora poderíamos passar para o repositório as especificações agrupadas assim.
RepositorioCliente clientes = new RepositorioCliente();
clientes.recuperarPorEspecificacao(especificacoes);
Desta forma estaríamos passando não uma mas sim duas especificações que poderiam ser executadas.

E digamos que queríamos os clientes sem contato OU os clientes com reclamação? Poderíamos fazer assim:
Especificacao especificacoes = clientesSemContato.or(clientesComRelamacao);
Repare agora o uso do operador or? O mesmo se aplica ao NOT onde queremos os clientes sem contato que não tiveram reclamação:
Especificacao especificações = clientesSemContato.not(clientesComRelamacao);

Repararam no poder deste simples sistema? O cliente tem total liberdade para especificar que resultado deseja sem se preocupar em como ele será obtido, sendo que como ele é obtido fica a cargo do repositório.


Pois bem como implementar isto em Java e usando o hibernate?

A Especificação que era uma interface agora irá usar o padrão Template Method, ela passará a ser conforme mostrado abaixo:

public abstract class Especificacao {
    public Especificacao and(Especificacao... especificacoes) {
        List<especificacao> especs = new ArrayList<especificacao>();
        for (Especificacao e : especificacoes)
            especs.add(e);
        especs.add(this);

        return new EspecificacaoAnd(especs.toArray(new Especificacao[especs.size()]));
    }

    public Especificacao not() {
        return new EspecificacaoNot(this);
    }

    public Especificacao or(Especificacao... especificacoes) {
        List<especificacao> especs = new ArrayList<especificacao>();
        for (Especificacao e : especificacoes)
            especs.add(e);
        especs.add(this);

        return new EspecificacaoOr(especs.toArray(new Especificacao[especs.size()]));
    }

    @SuppressWarnings("unchecked")
    public abstract Criterion configurarRestricao(Criteria criteria, Class classe) throws DomainException;

    public abstract boolean satisfazEspecificacao(Object o) throws DomainException;
    }
}
E os objetos que fazem uso do operador AND,OR,NOT são implementados no exemplo abaixo:

Especificação Or:

class EspecificacaoOr extends Especificacao {

    private Especificacao[] especificacoes;

    public EspecificacaoOr(Especificacao... especificacoes) {
        this.especificacoes = especificacoes;
    }

    @SuppressWarnings("unchecked")
    public Criterion configurarRestricao(Criteria criteria, Class classe) throws DomainException {
        Disjunction or = Restrictions.disjunction();

        for (Especificacao espec : especificacoes) {
            if (espec == null) continue;
            Criterion ret = espec.configurarRestricao(criteria, classe);
            if (ret != null)
                or.add(ret);
        }

        return or;
    }

    public boolean satisfazEspecificacao(Object o) throws DomainException {
        for (Especificacao espec : especificacoes) {
            boolean retorno = espec.satisfazEspecificacao(o);
            if (retorno)
                return true;
        }
        return false;
    }
}
Especificação AND:

class EspecificacaoAnd extends Especificacao  {
    private Especificacao[] especificacoes;
    public EspecificacaoAnd(Especificacao... especificacoes) {
    this.especificacoes = especificacoes;
    }
    @SuppressWarnings("unchecked")
    @Override
    public Criterion configurarRestricao(Criteria criteria,Class classe) throws DomainException {
        Conjunction and = Restrictions.conjunction();
        List<criterion> retorno = new ArrayList<criterion>();
        for (Especificacao espec : especificacoes) {
            if (espec == null) continue;
            Criterion ret = espec.configurarRestricao(criteria,classe);
            if (ret != null){
                retorno.add(ret);
                and.add(ret);
            }
        }

        if (retorno.size() == 0) return null;
  
        return and;
    }

    @Override
    public boolean satisfazEspecificacao(Object o) throws DomainException{
        for (Especificacao espec : especificacoes) {
            Boolean retorno = espec.satisfazEspecificacao(o);
            if (!retorno)
                return false;
        }
        return true;
    }
}
Especificação NOT:

class EspecificacaoNot extends Especificacao {

    private Especificacao espec;

    public EspecificacaoNot(Especificacao especificacao) {
        this.espec = especificacao;
    }

    @SuppressWarnings("unchecked")
    public Criterion configurarRestricao(Criteria criteria,Class classe) throws DomainException {
        if (espec == null) return null;
        Criterion ret = espec.configurarRestricao(criteria,classe);
        if (ret != null)
            return Restrictions.not(ret);

        return null;
    }

    public boolean satisfazEspecificacao(Object o) throws DomainException {
        return !espec.satisfazEspecificacao(o);
    }
}
Com estas especificações agora podemos usar o sistema conforme exemplificado e passar para o repositório agora não mais uma especificação e sim varias.

Conforme podemos ver alisto abaixo o real benefício desta implementação:

1. Classes mais leves: O repositório passa a ter uma interface simples e não recheada de métodos.
2. As regras da aplicação ficam em apenas um lugar.
3. As especificações podem ser combinadas e assim filtros mais complexos, com uso dos operadores lógicos.
4. Maior clareza, e encapsulamento da regra.

Em outro Post mostrarei como utilizar as especificações para executar outras coisas como recuperar um conjunto de registros paginado, Ordenado, usando especificações e como fazer isto na prática, postarei um código de exemplo.


Nenhum comentário :

Postar um comentário