Blog Formação DEV

Composição no Java: O que é e como funciona?

Composição é uma estratégia muito popular e saber usá-la no lugar e na hora certa permite melhorias na legibilidade e reutilização do seu código.
Composição no Java: O que é e como funciona?
Texto de: Geraldo Daros

Introdução

A composição é muito presente no nosso dia a dia. Nós somos compostos de órgãos, casas de tijolos e cimento e a tela que você está lendo agora por diversos materiais. A composição na programação orientada a objetos não é diferente, e no artigo de hoje veremos com mais detalhes o que é a composição, os benefícios que ela nos traz e como utilizá-la.

O que é a composição?

Primeiramente, a composição é um conceito que envolve a criação de objetos através da combinação de outros objetos. Em outras palavras, é a prática de construir classes utilizando instâncias de outras classes como componentes. Um bom exemplo é uma classe "Carro" que possui um atributo que é do tipo "Motor" e esse motor por si só tem diversas funcionalidades e comportamentos.

Algo importante de mencionar é que a utilização de um objeto para compor outro objeto só é de fato considerada como composição quando o objeto que está compondo o outro não tem ciclo de vida próprio, ou seja, sem compor outros objetos. Se você tem um objeto que possui um ciclo de vídeo próprio e faz parte da composição de outro objeto estará estruturando uma associação.

Por que usar composição?

Joshua Bloch explica na 3ª edição de seu livro Effective Java, que a principal razão para usar a composição é que ela permite reutilizar código sem modelar uma associação do tipo "É um", como se faz usando herança, o que permite um encapsulamento mais forte e torna seu código mais fácil de manter. Esse, por si só, já é um ótimo motivo para usar composição, mas também existem outros motivos que vou listar abaixo:

  • Separação de Responsabilidades: Ao dividir a funcionalidade em componentes menores, cada componente pode se concentrar em uma única responsabilidade. Isso promove um código mais limpo e fácil de entender.
  • Manutenção Simplificada: Quando uma mudança é necessária, é muito mais simples mudar na classe usada para compor outra do que fazer o mesmo em inúmeras classes que seriam compostas. Isso torna a manutenção do código mais simples e menos propensa a erros.

Quando usar composição?

A composição é muitas vezes contrastada com a herança, onde uma classe pode herdar comportamentos e características de outra. Não sabe o que é herança? Leia aqui.

O uso da composição pode não ser tão intuitivo quando o uso da herança, mas existe uma técnica simples para identificar momentos para o uso de composição que é fazer uma simples pergunta usando as duas classes em questão:

  • Classe 1 é Classe 2?
    Se sim, deve ser usado herança.
  • Classe 1 tem Classe 2?
    Se sim, deve ser usado composição.

Exemplo:

Fusca é uma porta ou tem porta?
Fusca é um carro ou tem carro?

Com a resposta dessas duas perguntas, você saberá qual é a definição correta da classe Fusca. A especificação é que o Fusca é um carro e terá portas, e a implementação seria uma classe Fusca, que herdará de Carro e possuirá um array de Porta.

Mãos no código

Pensando na solução em um cenário de programação, criaremos algumas classes para representar o carro e diversos componentes que normalmente compõem um carro.

Cenário

O cenário terá as seguintes classes:

  • Carro;
  • Parafuso;
  • Bateria;
  • Porta;
  • Motor;

Bateria:

public class Bateria {
    String modelo;
    String capacidade;

    public Bateria() {
    }
   
    public Bateria(String modelo, String capacidade) {
        this.modelo = modelo;
        this.capacidade = capacidade;
    }

    @Override
    public String toString() {
        return "Bateria [ modelo = " + modelo + ", capacidade = " + capacidade + " ]";
    }
} 

Parafuso:

public class Parafuso {
    Double tamanho;
    Double peso;

    public Parafuso() {
    }

    public Parafuso(Double tamanho, Double peso) {
        this.tamanho = tamanho;
        this.peso = peso;
    }

    @Override
    public String toString() {
        return "\n Parafuso [ tamanho = " + tamanho + ", peso = " + peso + " ]";
    }
    
} 

Porta:

public class Porta {
    boolean aberta = false;

    void abrir() {
        System.out.println("Abrindo porta...");
        this.aberta = true;
    }

    void fechar() {
        System.out.println("Fechando porta...");
        this.aberta = false;
    }

    @Override
    public String toString() {
        return "Porta [aberta = " + aberta + "]";
    }
} 

Motor:

import java.util.List;

public class Motor {
    String modelo;
    String potencia;
    List<Parafuso> parafusos;

    public Motor() {
    }

    public Motor(String modelo, String potencia, List<Parafuso> parafusos) {
        this.modelo = modelo;
        this.potencia = potencia;
        this.parafusos = parafusos;
    }

    @Override
    public String toString() {
        return "Motor [ modelo = " + modelo + ", potencia = " + potencia + ", parafusos = " + parafusos + " ]";
    }
} 

Criando a classe Carro

Com o nosso cenário criado, agora partiremos para a implementação da Classe Carro. Vamos compor nosso Carro com todas as classes que criamos acima:

import java.util.List;

public class Carro {
    String modelo;
    String placa;
    String qtdDeLugares;

    Motor motor; // Carro tem um Motor
    Bateria bateria; // Carro tem uma Bateria
    List<Porta> portas; // Carro tem Portas.

    public Carro() {
    }

    public Carro(String modelo, String placa, String qtdDeLugares, Motor motor, Bateria bateria,
            List<Porta> portas) {
        this.modelo = modelo;
        this.placa = placa;
        this.qtdDeLugares = qtdDeLugares;
        this.motor = motor;
        this.bateria = bateria;
        this.portas = portas;
    }

    @Override
    public String toString() {
        return "Carro: \n modelo = " + modelo + ", \n placa = " + placa + ", \n qtdDeLugares = " + qtdDeLugares
                + ", \n motor = " + motor + ", \n bateria = " + bateria + ", \n portas = " + portas + "]";
    }
} 

Criamos, então, uma classe que representa um carro. Usamos todas as classes que criamos no cenário, direta ou indiretamente, para compor o carro. Agora, note que além do Carro ter atributos normais, como modelo, placa e qtdDeLugares ele também faz o uso da composição quando possui motor, bateria e portas e motor, por sua vez, está sendo composto por parafuso.

Usabilidade

Para podermos testar na prática, podemos então fazer uma classe com o método main, instanciar alguns objetos e com isso construir um carro usando a composição, veja a seguir:

import java.util.Arrays;
import java.util.List;

public class Usabilidade {
    public static void main(String[] args) {
        Parafuso parafusoPequeno = new Parafuso(2.5, 0.30);
        Parafuso parafusoGrande = new Parafuso(6.0, 0.90);
        List<Parafuso> parafusos = Arrays.asList(parafusoGrande, parafusoPequeno);

        Porta portaMotorista = new Porta();
        Porta portaCaroneiro = new Porta();
        List<Porta> portas = Arrays.asList(portaMotorista, portaCaroneiro);

        Bateria bateria = new Bateria("Modelo ABC123", "70 Ah");

        Motor motor = new Motor("V8", "198 cv", parafusos);
        Carro carro = new Carro("SUV", "ABC-123", "4", motor, bateria, portas);
        System.out.println(carro);
    }
} 

No código acima preparamos todos os objetos que compõem o carro. Então, ao criar um novo carro, passamos todos os objetos para compor o nosso carro, com isso, teremos um carro composto de diversos outros atributos e objetos, esses outros objetos por si conseguem ter comportamentos e atributos, e assim por diante.

Saída do código visto anteriormente:

Carro:
modelo = SUV,
placa = ABC-123,
qtdDeLugares = 4,
motor = Motor [ modelo = V8, potencia = 198 cv, parafusos = [
Parafuso [ tamanho = 6.0, peso = 0.9 ],
Parafuso [ tamanho = 2.5, peso = 0.3 ]] ],
bateria = Bateria [ modelo = Modelo ABC123, capacidade = 70 Ah ],
portas = [Porta [aberta = false], Porta [aberta = false]]] 

Podemos também mudar qualquer valor na instância de carro para mudarmos os atributos de qualquer instancia de compõem o carro, como fechar e abrir as portas, mudar o modelo do motor ou a potência. A composição por si separa a lógica e deixa a responsabilidade de cada componente para cada componente, promovendo o princípio de responsabilidade única (SRP). Se você precisar mudar a regra para abrir e fechar uma porta, algo como verificar se ela está trancada ou não, e adicionar um atributo trancado, você pode fazer isso facilmente alterando a classe Porta para que ela tenha essa nova funcionalidade, sem precisar modificar nenhuma outra classe.

Conclusão

A composição é uma estratégia inevitável para criar sistemas em programação orientada a objetos. Nesse artigo é possível ver como é o deve ser o pensamento antes de usar a composição, diversas vantagens de usar composição e principalmente, como utilizá-la.

Sobre o autor
Cod3r

Cod3r

Com mais de 400 mil alunos, a Cod3r é uma das principais escolas de tecnologia do País. Um de seus produtos mais importantes é a Formação DEV, com objetivo de preparar os profissionais para o mercado.

Ótimo! Inscreveu-se com sucesso.

Bem-vindo de volta! Registou-se com sucesso.

Assinou com sucesso o Blog Formação DEV .

Sucesso! Verifique o seu e-mail para obter o link mágico para se inscrever.

As suas informações de pagamento foram atualizadas.

Seu pagamento não foi atualizado.