Re-avaliando relacionamentos com cardinalidade 1..n

O processo de criação de relacionamentos 1..n é obviamente mais complicado que os 1..1. Para isto tivemos que:

  1. declarar um atributo empresas dentro da classe Cidade, sendo este um ArrayList de objetos Empresa;

  2. criar uma lista vazia no construtor da classe, instanciando um ArrayList de objetos Empresa;

  3. incluir um getter para podermos saber quais são as empresas da cidade;

  4. incluir o método addEmpresa no lugar do setter, que torna mais simples o preenchimento da lista de cidades (adicionando uma cidade por vez).

Apesar de o código apresentado funcionar perfeitamente, e este é o tipo de código que encontrarão muito por aí, aquela não é a forma mais adequada de de usarmos coleções como o ArrayList. Vamos discutir os detalhes logo para que você se habitue a forma mais adequada e evite vícios que muitos programadores têm.

Como discutido anteriormente, a Java Collections Framework (JCF) possui uma série de diferentes classes para representar coleções. Temos classes que representam, por exemplo:

  • listas (List), permitindo armazenar elementos repetidos;

  • conjuntos (Set), não permitindo armazenar elementos repetidos.

Para cada tipo de coleção temos classes adicionais que fornecem implementações específicas, como:

  • ArrayList e LinkedList para listas;

  • HashSet e TreeSet para conjuntos.

List e Set não são de fato classes, mas não vamos nos ater a isso por enquanto.

Todas as classes que representam listas possuem os mesmos métodos. Assim, a forma de utilizar uma ArrayList é a mesma de uma LinkedList. O mesmo vale para tipos outros tipos como Set. Cada classe fornece uma implementação diferente da outra. Por exemplo, uma ArrayList utiliza um vetor internamente para armazenar os dados. Você lembra que falamos que vetores tem tamanho limitado. Mostramos que, depois de criado, a única forma de aumentar o total de elementos em um vetor é criar um novo vetor maior e copiar os dados do antigo para este novo. Ao utilizar uma ArrayList, todo esse processo já está implementando internamente. Não precisamos nos preocupar com nada. Apenas adicionamos elementos à lista. Se o vetor interno encher, a classe se encarrega de executar o processo descrito acima para criar um vetor maior.

O vídeo abaixo demonstra este processo. Iniciamos criando uma cidade. Ao verificar a ArrayList empresas, observamos que o tamanho (size) da lista é 0. Ao adicionar a primeira empresa e voltar a visualizar o atributo empresas na cidade criada, o tamanho agora é 1. Podemos ver que as empresas são armazenadas dentro da ArrayList em um vetor chamado elementData. Ao clicar duas vezes nele, descobrimos que o vetor tem capacidade (length) para 10 elementos, mas apenas o elemento 0 possui uma Empresa. Ao clicar duas vezes em tal elemento, conseguimos ver qual é a empresa de fato.

Em seguida criamos mais 9 empresas, totalizando 10, e adicionamos à cidade, o que fará com que o vetor fique cheio. Ao criarmos e adicionarmos a 11ª empresa, veremos que são criadas mais 5 posições para armazenar novos elementos. Isso ocorrerá cada vez que o vetor encher e tentarmos adicionar um novo elemento. Toda essa mágica acontece internamente na classe, criando um vetor maior e copiando os dados do antigo, sem que ao menos percebamos que um novo vetor foi criado (aparentemente a classe apenas aumentou o vetor existente).

A questão é que, se temos diferentes tipos de List e Set, podemos uma hora querer trocar de um tipo para outro, pois avaliamos que o tipo anteriormente escolhido não está sendo adequado ou eficiente para as atuais necessidades do nosso sistema. No mundo real por exemplo, podemos utilizar diferentes meios para criar uma lista de compras, como uma folha de papel ou um aplicativo no celular. Dependendo do tipo de lista que usarmos, teremos vantagens e desvantagens. Uma lista de papel pode ser utilizada por qualquer pessoa, mas pode encher e não termos mais espaço para incluir novos itens e alteração ou remoção de itens requer resurar o papel.

Já um app no celular tem inúmeras vantagens, como permitir: incluir quantos itens desejarmos; facilmente alterar ou remover itens; mudar a ordem para facilitar a coleta dos produtos no supermercado (por exemplo, colocando as frutas e verduras todas em sequência na lista). Por outro lado, nem todas as pessoas conseguem utilizar tais apps ou a bateria do celular pode esgotar e ficarmos sem lista.

Cada tipo de lista tem suas vantagens e desvantagens, mas a forma de utilizar listas de papel é muito diferente de utilizar um app no celular. Se você tentar fazer seus avós criarem e usarem um app de celular para isto, eles podem simplesmente odiar e terem dificuldade para se adaptar. Não seria ótimo se pudéssemos trocar o tipo de lista sem que precisássemos reaprender algo? É exatamente esta a questão de re-avaliarmos o código usado anteriormente para criar a lista de empresas.

Como usamos um ArrayList, declaramos o atributo com este tipo e incluímos um getter que explicitamente indica que o tipo de lista é ArrayList. Se descobrirmos que ArrayList não é o tipo mais adequado para nosso problema, teremos que alterar isto na declaração do atributo, no construtor da classe onde a lista está sendo instanciada e no getter.

Adicionalmente, como o getter é público, tal método pode estar sendo utilizado em qualquer parte do nosso código. Se este é um projeto que disponibilizamos como código aberto na Internet, possivelmente outras pessoas estarão declarando variáveis do tipo ArrayList para poder armazenar o resultado do método getEmpresas() e fazer operações sobre tal lista (como exibir as empresas nela). Se resolvermos utilizar um outro tipo de lista como LinkedList, teremos que alterar todos os locais citados no nosso código e ainda feremos com que qualquer desenvolvedor que esteja utilizando o getEmpresas() tenha que atualizar seu código se for compilar utilizando a versão modificada da nossa classe Cidade.

Para evitar este problema, uma vez que qualquer lista possui os mesmos métodos públicos, não devemos declarar o atributo utilizando um tipo específico como ArrayList, mas sim um tipo mais genérico, neste caso o List. Neste caso, teremos que alterar todos os locais na classe onde há ArrayList para List, como mostrado abaixo:

import java.util.List;

public class Cidade
{
    private String nome;
    private Estado estado;
    private List<Empresa> empresas;
    //Getters e setters para nome e estado omitidos por simplificação

    public Cidade(){
        this.empresas = new List<>();
    }

    public List<Empresa> getEmpresas(){
        return empresas;
    }

    public void addEmpresa(Empresa empresa){
        empresas.add(empresa);
    }
}

Observe que alteramos o import, a declaração do atributo, a instanciação dentro do construtor e por fim o getter. Porém, ao tentar compilar será exibido o erro "java.util.List is abstract; cannot be instantiated", indicando que List é um tipo abstrato e não pode ser instanciado. Vamos falar sobre abstração e tipos abstratos mais adiante. Por enquanto, o que precisa saber é que List (assim como Set) define apenas quais métodos públicos os diferentes tipos de lista como ArrayList e LinkedList obrigatoriamente terão. O tipo List de fato não implementa a grande maioria desses métodos. Assim, List serve apenas como um modelo, um protótipo que não é funcional. ArrayList e LinkedList são classes totalmente funcionais, criadas a partir do modelo fornecido pela List. A List seria como um projeto inacabado de uma algo, enquanto ArrayList seria o projeto final.

Por exemplo, o projeto de uma casa pode iniciar apenas com a parte estrutural, que inclui o alicerce e estruturas de sustentação das paredes e teto. Somente isto torna o projeto incompleto. Em fases seguintes é preciso incluir outros projetos como o elétrico. Assim, a List seria um projeto estrutural, enquanto ArrayList e LinkedList complementariam tal projeto, formando um projeto completo e funcional. Assim, o código completo e final da classe Cidade é como mostrado abaixo. Observe que tivemos que importar tanto List quando ArrayList pois estamos utilizando os dois tipos.

import java.util.List;
import java.util.ArrayList;

public class Cidade
{
    private String nome;
    private Estado estado;
    private List<Empresa> empresas;

    public Cidade(){
        this.empresas = new ArrayList<>();
    }

    public String getNome(){
        return nome;
    }

    public void setNome(String nome){
        this.nome = nome;
    }

    public Estado getEstado(){
        return estado;
    }

    public void setEstado(Estado estado){
        this.estado = estado;
    }

    public List<Empresa> getEmpresas(){
        return empresas;
    }

    public void addEmpresa(Empresa empresa){
        empresas.add(empresa);
    }
}

Ao usarmos um tipo mais genérico como List ao declarar uma variável e um tipo específico como ArrayList apenas no momento de instanciar um objeto para aquela variável, estamos tornando nosso código mais flexível à mudanças. A técnica que aplicamos é baseada em um trabalho científico publicado por Barbara Liskov em 1994, que apresentou o Princípio da substituição de Liskov (LSP da sigla em inglês). Este é um dos 5 importantes princípios chamados SOLID, que é assunto para disciplinas de engenharia de software. O tema foi abordado aqui apenas para mostrar como essas técnicas são extremamente importantes e utilizadas pela industria para produzir software de qualidade, mas muitas vezes ignoradas por programadores.

Re-avaliando relacionamentos com cardinalidade 1..1

Apesar de ser natural pensarmos na cardinalidade de EmpresaCidade como 1..1, na verdade tal cardinalidade é n..1, ou seja, vários objetos Empresa podem estar relacionados à mesma cidade. Logo, podemos ter várias empresas na mesma cidade. Você pode pensar que seria o mesmo que dizer que 1 Empresa está relacionada a 1 Cidade. Se pensarmos assim, podemos representar a cardinalidade e direção da associação entre essas duas classes como 1 → 1. No entanto, se apenas invertermos a seta para avaliarmos o relacionamento no sentido contrário (EmpresaCidade), teremos 1 ← 1. Lendo na direção da seta indica que 1 Cidade possui no máximo 1 Empresa.

Sabemos que isto não é verdade, uma cidade pode ter várias (n) empresas. Por esse motivo, no lugar de interpretarmos relacionamentos semelhantes à EmpresaCidade como 1 → 1, é mais simples interpretarmos como n → 1. Assim, se criarmos o relacionamento no sentido contrário, só precisamos inverter a seta e teremos n ← 1, indicando que 1 cidade pode ter várias empresas.

results matching ""

    No results matching ""