Tag: design

Trabalhando com Genéricos em Java

cup_generics Trabalhar com genéricos é uma abordagem muito produtiva para reutilização de código.

Exemplo: DAOs genéricos

Implementações genéricas de DAOs (Data Access Object) é algo que vejo repetidas vezes em diferentes projetos e equipes. Implementando alguns métodos genéricos, é possível evitar a criação de dezenas de classes semelhantes num projeto. Se bem planejado e usado, técnicas como essa economizam tempo e facilitam a manutenção.

Em Java, podemos criar uma classe base com um tipo genérico para representar nosso DAO. Considere o seguinte trecho de código:

public class BaseDao<T, ID> {
    public T find(ID entityId) { /*...*/ return null; }
    public void insert(T entity) { /*...*/ }
    public int update(T entity) { /*...*/ return 0; }
    public void delete(ID entityId) { /*...*/ }
}

Há duas principais abordagens para usar uma classe genérica como essa.

Herança

Podemos estender a classe BaseDao, gerando subclasses específicas que estendem as funcionalidades padrão.

Exemplo:

public class ClienteDao<Cliente, Integer> { ... }

public class LoginDao<Login, String> { ... }

Uso direto

Quando não há necessidade de especialização, é possível usar diretamente a classe base.

Exemplo:

BaseDao<Cliente, Integer> baseDao = new BaseDao<>();

BaseDao<Login, Integer> loginDao = new BaseDao<>();

Identificando o tipo genérico

Em determinadas situações é desejável descobrir em tempo de execução qual é o tipo genérico de uma classe.

Continuando em nosso exemplo do DAO, poderíamos ler alguma anotação ou configuração associada à classe Cliente quando o DAO fosse deste tipo.

Como fazer isso nos dois tipos de DAOs mencionadosacima?

Herança

Considere o código:

public class BaseDao<T, ID> {

    final private Class<T> entityType;

    public BaseDao() {
        entityType = (Class<T>) ((ParameterizedType)
                getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }

    //...

}

Supondo que existem subclasses, tal como ClienteDao extends BaseDao<Cliente, Integer>, então o atributo entityType receberá a referência para a classe Cliente.

Trechos de código semelhantes a esse estão disponíveis em várias postagens e respostas do Stack Overflow, mas é algo que pode ser considerado “inseguro”, pois há cenários em que isso simplesmente não funciona.

Uso direto

O código do construtor, como exibido acima, não funciona numa instanciação direta de BaseDao.

O seguinte exemplo vai gerar um erro:

 BaseDao<Login, Integer> loginDao = new BaseDao<>(); //não vai instanciar o objeto

Qual o motivo?

Type erasure

Primeiro, é preciso entender que a informação do tipo genérico de uma variável é removida em tempo de execução.

Por exemplo:

List<String> lista = new ArrayList<String>();

O compilador “garante” que você só consiga adicionar objetos do tipo String dessa lista, mas quando sua classe é executada na JVM, não há qualquer verificação quanto ao tipo.

Sim, você pode dar um cast inseguro de List<String> para List e então adicionar outro tipo de objeto. O compilador vai aceitar, afinal ele “confia” em você.

É impossível para a instância lista saber o seu próprio tipo genérico, já que no fim das contas só existe uma única classe ArrayList, o tipo genérico é apenas um mecanismo de segurança para o desenvolvedor.

Li uma vez que manter o type satefy implicaria em um overhead muito grande para a máquina virtual, pois o Java precisaria praticamente criar uma cópia da classe List para cada tipo genérico usado no programa, de modo a ter literalmente uma versão da classe para cada tipo.

Quando é possível recuperar o tipo genérico

Repare no método getGenericSuperclass(). Ele retorna os tipos que a classe atual define para os parâmetros genéricos da superclasse imediata. Algo análogo ocorre com o getGenericInterfaces().

Isso significa que se você tiver uma subclasse assim:

class ClienteDao extends BaseDao<Cliente, Integer> { ... }

O código funcionaria perfeitamente, pois você está definindo um tipo para o parâmetro genérico da superclasse. Isso funciona porque o tipo genérico não está apenas numa variável, mas declarando de forma estática (fixa) na própria definição da classe.

Mas como você instancia diretamente o GenericDao, não há uma generic superclass e, portanto, a exceção sendo lançada.

Basta usar herança?

Usar sempre herança como no exemplo com extends é uma saída, mas muitos não recomendam porque há chances do código “quebrar” com novos erros estranhos.

Essa é uma possibilidade, pois como a própria documentação do método getGenericSuperclass() afirma, ele só retorna o tipo genérico implementado na superclasse imediata do seu DAO. Então se houver uma hierarquia diferente de exatamente dois níveis, o código vai falhar.

Recomendação

A recomendação que vai funcionar de forma mais simples e direta é passar a classe desejada por parâmetro para o construtor do GenericDao.

Exemplo:

public class GenericDao<T extends Serializable> {

    private final Class<T> entityType;

    public GenericDao(Class<T> entityType) {
        this.entityType = entityType;
    }

    ...

}    

Muito mais simples, certo?

O maior inconveniente é a chamada verbosa de criação da classe:

BaseDao<Cliente> clienteDao = new BaseDao<Cliente>(Cliente.class);

Na verdade, a partir do Java 7 poderíamos omitir o tipo Cliente da classe instanciada:

BaseDao<Cliente> clienteDao = new BaseDao<>(Cliente.class);

Melhorou um pouco, mas podemos fazer melhor. Que tal criar um método factory?

Exemplo:

public class BaseDao<T> {

    final private Class<T> entityType;

    public static <X> BaseDao<X> getInstance(Class<X> entityType) {
        return new BaseDao<X>(entityType);
    }

    private BaseDao(Class<T> entityType) {
        this.entityType= entityType;
    }

    ...

}

E a criação da instância da classe fica assim:

GenericDao<Cliente> clienteDao = BaseDao.getInstance(Cliente.class);

Alguns podem achar demais, porém considero este um design mais claro.

Considerações

Certamente há formas ainda mais flexíveis de trabalhar com genéricos usando Injeção de Dependência, Spring Beans e outros, mas por agora, estes fogem ao escopo do artigo.

O importante é compreender que Generics é um recurso importante e flexível da plataforma Java, com aplicações diretamente em nosso dia-a-dia.

Análise e Design Orientados a Objetos – uma visão prática

UML_logo Estudantes e praticantes de Engenharia de Software geralmente tem dúvidas sobre como modelar um sistema de forma efetiva.

Atualmente, a abordagem mais recomendada é a Orientada a Objetos, preferencialmente usando UML, já que essas técnicas são amplamente reconhecidas e propagadas.

O problema é que muita confusão ocorre por falta de conhecimento sobre os objetivos da modelagem de sistemas, as fases de um projeto de desenvolvimento de software e os diferentes níveis de modelagem possíveis. Por exemplo: quais classes devem ser incluídas num Diagrama de Classes? Devemos colocar as classes do framework? Como identificar as classes necessárias em um sistema?

O ponto de partida é não misturar a Análise do problema com o Design (projeto) da solução ou com a Implementação tecnológica.

Em resumo, vamos analisar nos tópicos abaixo como aplicar a modelagem orientada a objetos em diferentes fases de um projeto, tendo em mente alcançar objetivos concretos com a modelagem, além das diferentes aplicações dos modelos utilizados.

Análise

O analista é o profissional responsável por identificar um problema a ser resolvido ou necessidade a ser atendida e elicitar os requisitos para a criação de uma solução.

O conjunto de requisitos define o que o sistema deve fazer para atender às necessidades identificadas.

Baseando-se nos requisitos, o analista continua o processo de Análise identificando em alto nível de quais funcionalidades o sistema deverá possuir para atender aos requisitos.

Uma solução comum para mapear cada funcionalidade é através de Casos de Uso (não confundir com o Diagrama da UML). Um Caso de Uso é uma espécie de passo-a-passo da interação entre usuário e sistema, embora esse conceito possa variar um pouco. Além disso, geralmente ele descreve as pré-condições necessárias para a correta execução e as pós-condições, que são os resultados da ação realizada.

Note que ainda estamos em alto nível e nada aqui tem a ver com a solução tecnológica.

Continuando, o analista treinado em Orientação a Objetos e UML irá modelar o conhecimento sobre o domínio e o problema utilizando os diagramas adequados, que geralmente são: Diagrama de Caso de Uso, Diagrama de Atividades, Diagrama de Classes e Diagrama de Estados.

O Diagrama de Caso de Uso é uma representação visual simples das interações do sistema com o mundo externo. Os atores que interagem com o sistema são representações de usuários, outros sistemas ou qualquer entidade externa ao sistema que se comunique com o mesmo. Este diagrama não exclui a necessidade de mapear em detalhes os casos de uso conforme mencionado anteriormente.

O Diagrama de Atividades representa os passos do caso de uso numa espécie de fluxograma, incluindo bifurcações de cenários alternativos, cenários de erro, etc. Nem todos os cenários precisam ser representados no mesmo diagrama.

O Diagrama de Classes, neste estágio de um projeto, deve incluir apenas as classes de domínio, sem qualquer referência à tecnologia que será usada na implementação. Poderíamos chamar este diagrama de Diagrama de Classes de Domínio. A função do diagrama é representar as entidades necessárias e o relacionamento entre elas. Em suma, é a forma moderna e orientada a objetos do Diagrama de Entidade-Relacionamento (DER), embora ambos possam ser usados concomitantemente. No entanto, o DER geralmente é associado à modelagem estruturada.

O Diagrama de Estados é usado para as entidades do sistema que seguem um fluxo de estados. Por exemplo, uma parcela pode estar “em aberto”, “liquidada”, “em atraso”, “em prejuízo”. Este diagrama representa os estados e como ocorrem as transições entre eles.

Com tudo isso, o analista pode validar a solução verificando se as classes e os casos de uso atendem aos requisitos iniciais. Por exemplo, se houver um requisito de que “o gerente poderá extrair um relatório com o total de produtos vendidos no mês”, o analista deve olhar se a classe Produto possui um relacionamento “navegável” com Venda e ItemVenda e se é possível extrair a informação de totalização das vendas de forma lógica. Ele também pode adicionar os métodos e atributos importantes às classes de modo a atender aos requisitos.

Design (Projeto)

Com base em todas essas informações, entram em ação os arquitetos e desenvolvedores para propor uma solução tecnológica para o problema. Isso não necessariamente vem em sequência, muito pode ocorrer em paralelo.

Os projetistas técnicos poderão criar vários outros diagramas para representar o que será implementado. O Design pode ser feito de forma agnóstica, isto é, sem considerar quais tecnologias, frameworks e bibliotecas serão usadas. No entanto, creio que é mais produtivo modelar a divisão de componentes e classes já pensando um pouco na implementação, de forma a não gerar mais um gap de informação.

Os diagramas mais relevantes e geralmente usados são:

  • Diagrama de Componentes: representa a divisão em alto nível dos componentes principais do sistema. A divisão não representa a estrutura de pastas dos arquivos do projetos, mas é uma divisão lógica de responsabilidades.
  • Diagrama de Deployment: uma representação do ambiente onde o sistema será executado, incluindo servidores, bancos de dados, replicação, proxies, etc.
  • Diagrama de Sequência: para uma determinada ação no sistema, este diagrama representa a interação entre diversos objetos através das chamadas executadas e do retorno, permitindo visualizar a sequência de chamadas no decorrer tempo.

Note que cada um dos diagramas citados podem ser produzidos para vários casos diferentes. Quando falamos em Diagrama de Classes ou Diagrama de Componentes, não há necessariamente um único diagrama que representa o sistema como um todo. A representação pode ser feita em níveis diferentes, por exemplo, um mostrando os componentes gerais do sistema e outros diagramas mostrando a estrutura interna de cada componente individualmente. A representação também pode ser feita em contextos diferentes, por exemplo, vários diagramas para representar apenas o necessário para uma funcionalidade importante, ignorando classes e pacotes não relevantes naquele contexto.

Implementação

A implementação deve seguir o que foi definido no Design, porém não significa que cada método, classe, pacote e componente deve ser mapeado um-para-um no projeto “físico”, em seus arquivos e estrutura de diretórios.

O programador deve ter a liberdade de encontrar a melhor solução para atender ao que foi solicitado com a estrutura que ele desejar. Seria péssimo, do ponto de vista de boas práticas, impor cada detalhe do que deve ser implementado. Se isso fosse possível, não precisaríamos de programadores, mas de um gerador de código.

Construindo a ponte

Ao estudar com atenção as “fases” (entre aspas porque não são uma sequência linear) de um projeto de desenvolvimento de software, é possível notar que existe um grande salto (gap) entre cada uma delas. Uma analogia comumente usada nos livros de Engenharia de Software consiste em construir uma ponte entre o que o cliente precisa e a solução tecnológica.

Ainda hoje, a Engenharia de Software é uma disciplina um tanto imatura. Não temos uma forma padronizada de construção como a Civil ou Elétrica para nos apoiar. A validade de um Modelo de Análise, um Modelo de Design ou da solução implementada depende quase exclusivamente de fatores humanos, como a capacidade de comunicação e entendimento dos analistas, além da capacidade técnica dos desenvolvedores.

Não existem regras definitivas sobre como e em qual nível modelar, assim como não há regras sobre como traduzir uma necessidade em um requisito, um requisito em um modelo e um modelo numa implementação.

A UML foi um grande avanço, mas os diversos diagramas sempre variam em nível de detalhe, abrangência e muitos outros fatores de projeto para projeto, de equipe para equipe e de indivíduo para indivíduo.

Princípios de modelagem

Mesmo com as últimas afirmações acima, não quero ser pessimista. Embora não haja uma resposta definitiva para a modelagem de sistemas, existem alguns princípios que podem nos guiar:

  • Coloque a comunicação em primeiro lugar. O objetivo de um diagrama é comunicar informação e não simplesmente ser um espelho do código. Se um diagrama não comunica algo útil, não perca tempo com ele. Considere sua equipe e o seu projeto e faça os diagramas que forem relevantes com os detalhes relevantes para que as pessoas saibam o que estão fazendo. O seu time consegue se reunir numa mesa e discutir um diagrama, rabiscando-o e usando-o como base para a conversa?
  • Não faça diagramas de tecnologias específicas. Se alguém quiser saber como Servlets, Rails ou Django funcionam, é melhor comprar um livro. Você só vai confundir as pessoas. Já vi muitos diagramas por aí que nada mais são do que o modelo MVC com nomes diferentes.
  • Verifique se o diagrama atende os requisitos. O seu diagrama deve ser útil não só para entender o que deve ser feito, mas também para validar se a sua solução atende ao que o cliente precisa. Faça testes mentais lógicos, olhando para as classes, métodos e relacionamentos, verificando se elas tem motivo de estarem ali, se para um certo cenários você consegue extrair os dados necessários, etc.

Como mencionei algumas vezes, a Análise, o Design e a implementação provavelmente serão feitas muitas vezes durante o ciclo de desenvolvimento. Não espere ter tudo certo no começo. Investir tempo demais em detalhamento é ruim, vários autores já alertaram.

Identificando as classes necessárias

Existe um método para identificação de classes de domínio em potencial que consiste na análise de um texto à procura de substantivos. Esta técnica pode ser útil quando você não tem ideia do que está fazendo e quer algumas ideias iniciais. No entanto, é péssima porque em muitos lugares é ensinada como uma forma “burra” de extrair informação.

Volta e meia ouço alguém com a velha ideia de criar um interpretador mágico, um tipo de Inteligência Artificial, capaz de gerar um sistema com base num texto descrevendo as necessidades do usuário. Devaneios à parte, é melhor nos concentrarmos no que é real hoje.

Aliás, é comum usarmos a palavra “extrair” ou “levantar” indevidamente. Quando criamos um sistema, não extraímos ou levantamos os requisitos e as classes necessárias como se elas já existissem ali, ocultas de alguma forma.

Quanto aos requisitos, o termo “elicitar” é mais adequado, com o sentido de descobrir e descrever. Quanto às classes, nós simplesmente decidimos de forma espúria quais delas o sistema deverá conter de modo a atender às necessidades. Se verificarmos que elas não atendem aos requisitos, nós as modificamos para que o façam.

Tudo isso trata-se de um processo criativo e não de um processo de extração como se faz com matéria prima. Por “criativo”, não pense em arte pós-moderna, mas num processo criativo metódico e até científico.

Minha recomendação é identificar, através dos requisitos, quais dados são necessários para que o sistema funcione, assim como o relacionamento entre elos. Já dizem os DBAs: os dados são o coração do sistema. Assim conseguimos as classes que representam as entidades necessárias.

Depois, com base nas funcionalidades que o sistema deve ter, pode-se definir classes que serão responsáveis por tratar essas funcionalidades.

Considerações finais

A Engenharia de Software ainda possui muitos desafios pela frente para se tornar realmente uma engenharia no sentido completo da palavra.

Muitos hoje (inclusive eu) consideram-se mais artesãos tecnológicos do que engenheiros propriamente ditos. Isso é bom em certo sentido, mas também abre muitas brechas para as “artes abstratas”. 😉

Enfim, não há uma resposta definitiva para o sucesso na modelagem de um sistema, mas espero ter exemplificado bem um caminho que aprendi ao longo de alguns anos estudando e refletindo sobre como desenvolver software adequadamente, não apenas do ponto de vista técnico.


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Creative Commons O blog State of the Art de Luiz Ricardo é licenciado sob uma Licença Creative Commons. Copie, compartihe e modifique, apenas cite a fonte.