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.