1. Construindo a hierarquia: da abstração aos detalhes
Implementar abstrações e hierarquias — é uma forma de estruturar o código do geral para o específico. Primeiro descrevemos o que todos os objetos devem saber fazer (abstração) e, depois, detalhamos como cada classe concreta faz isso.
Na programação, como na vida, tudo começa com perguntas. Por exemplo: “O que há em comum entre um círculo e um retângulo?” Resposta: ambos são figuras. E o que há em comum entre figuras? Normalmente elas têm área e podem ser desenhadas.
Em Java, isso se expressa por meio de uma abstract-class:
public abstract class Shape {
public abstract double area();
public abstract void draw();
}
Aqui estamos afirmando:
- Toda figura deve ser capaz de calcular sua área (area()).
- Toda figura deve ser capaz de ser desenhada (draw()).
- Como exatamente ela faz isso — não é da nossa conta (por enquanto).
Agora vamos criar figuras concretas:
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public void draw() {
System.out.println("Desenhando um círculo com raio " + radius);
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public void draw() {
System.out.println("Desenhando um retângulo " + width + "x" + height);
}
}
O que fizemos?
- Extraímos o comum para a classe abstrata.
- Detalhamos o comportamento nas subclasses.
Esquematicamente:
. Shape
/ \
Circle Rectangle
Tabela: o que é implementado onde
| Classe | area() | draw() | Campos próprios |
|---|---|---|---|
| Shape | |
|
- |
| Circle | implementado | implementado | |
| Rectangle | implementado | implementado | |
2. Por que isso é conveniente? (e por que isso funciona)
Interface única para trabalhar com objetos diferentes
Suponha que você tenha uma coleção de figuras:
Shape[] shapes = {
new Circle(5),
new Rectangle(3, 4),
new Circle(2.5)
};
Você pode percorrê-las do mesmo jeito, sem pensar no tipo:
for (Shape shape : shapes) {
shape.draw();
System.out.println("Área: " + shape.area());
}
Deixe a JVM se virar para descobrir quem é círculo e quem é retângulo! É isso que chamamos de polimorfismo (já falamos sobre ele e ainda falaremos com mais detalhes — no próximo bloco).
Facilidade de extensão
Quer adicionar um triângulo? Basta escrever (tipo novo — código antigo intocado): Triangle extends Shape.
public class Triangle extends Shape {
private double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
@Override
public void draw() {
System.out.println("Desenhando triângulo: base " + base + ", altura " + height);
}
}
O restante do código (por exemplo, o loop que percorre a lista de figuras) não precisa ser alterado.
Evitando duplicação de código
Se todas as figuras passarem a ter uma propriedade comum (por exemplo, cor), é conveniente extraí-la para a classe abstrata:
public abstract class Shape {
private String color = "preto";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract double area();
public abstract void draw();
}
Agora qualquer subclasse — seja um círculo ou um triângulo — herdará a cor.
3. Prática: desenvolvendo um mini editor gráfico
Vamos juntar tudo. Imagine que você está criando um editor gráfico simples.
Classe abstrata Figure
public abstract class Figure {
private String color = "black";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract void draw();
public abstract void resize(double factor);
}
Figuras concretas
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("Desenhando linha de comprimento " + length + " com a cor " + getColor());
}
@Override
public void resize(double factor) {
length *= factor;
System.out.println("Novo comprimento da linha: " + length);
}
}
public class Ellipse extends Figure {
private double a, b;
public Ellipse(double a, double b) {
this.a = a;
this.b = b;
}
@Override
public void draw() {
System.out.println("Desenhando elipse com eixos " + a + " e " + b + " com a cor " + getColor());
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("Novos eixos da elipse: " + a + ", " + b);
}
}
Quer adicionar uma nova ferramenta, como Polygon? Basta criar uma nova classe — todo o editor trabalha via a classe abstrata Figure.
Uso no código
Figure[] figures = {
new Line(10),
new Ellipse(5, 3)
};
for (Figure figure : figures) {
figure.setColor("red");
figure.draw();
figure.resize(1.5);
}
Saída:
Desenhando linha de comprimento 10.0 com a cor red
Novo comprimento da linha: 15.0
Desenhando elipse com eixos 5.0 e 3.0 com a cor red
Novos eixos da elipse: 7.5, 4.5
Visualização da hierarquia
. Figure
/ \
Line Ellipse
4. Como evitar duplicação: campos e métodos comuns
Às vezes, todas as subclasses têm não só métodos comuns, mas também campos comuns (por exemplo, as coordenadas do centro). A classe abstrata é o lugar ideal para isso:
public abstract class Figure {
private double x, y; // coordenadas do centro
public Figure(double x, double y) {
this.x = x;
this.y = y;
}
public void moveTo(double newX, double newY) {
x = newX;
y = newY;
System.out.println("Figura movida para o ponto (" + x + ", " + y + ")");
}
public abstract void draw();
}
Agora quaisquer Line ou Ellipse podem se mover sem reimplementar esse método.
5. Mais um exemplo: sistemas de pagamento
Abstração não é só sobre figuras! Imagine que você está criando um sistema de processamento de pagamentos.
Classe abstrata Payment
public abstract class Payment {
public abstract void process();
}
Implementações concretas
public class CreditCardPayment extends Payment {
@Override
public void process() {
System.out.println("Processando pagamento com cartão de crédito");
}
}
public class PaypalPayment extends Payment {
@Override
public void process() {
System.out.println("Processando pagamento via PayPal");
}
}
Uso
Payment[] payments = {
new CreditCardPayment(),
new PaypalPayment()
};
for (Payment payment : payments) {
payment.process();
}
Saída:
Processando pagamento com cartão de crédito
Processando pagamento via PayPal
6. Vantagens dessa abordagem
- Interface única: é possível trabalhar com objetos diferentes do mesmo jeito.
- Extensibilidade: adicionar novos tipos de objetos não exige reescrever o código antigo.
- Mínima duplicação: o comum fica na classe base abstrata.
- Flexibilidade: é possível usar coleções de tipos abstratos sem se preocupar com detalhes.
7. Exemplo do mundo real: transporte
A abstração aparece não só nos livros. Por exemplo, se você estiver projetando um sistema para gerenciar transportes:
public abstract class Transport {
public abstract void move();
public abstract void fuelUp();
}
Tipos concretos de transporte implementam os detalhes:
public class Car extends Transport {
@Override
public void move() {
System.out.println("O carro anda na estrada");
}
@Override
public void fuelUp() {
System.out.println("Abastecendo com gasolina");
}
}
public class Bicycle extends Transport {
@Override
public void move() {
System.out.println("A bicicleta pedala");
}
@Override
public void fuelUp() {
System.out.println("A bicicleta não precisa de combustível, só de sanduíches para o ciclista!");
}
}
8. Esquema útil: como construir uma hierarquia de abstrações
[Classe abstrata]
|
[Subclasse concreta]
|
[Subclasse ainda mais específica] (se necessário)
- Tudo que é comum — no topo!
- Tudo que é único — embaixo!
9. Erros comuns na implementação de abstrações e hierarquias
Erro nº 1: Duplicação de código nas subclasses.
Se você notar que em cada subclasse está escrevendo os mesmos campos ou métodos — é um sinal de que vale a pena extraí-los para a classe abstrata. Não tenha medo de tornar a abstração “mais ampla” se isso reduzir a duplicação.
Erro nº 2: Violação do princípio “do geral para o específico”.
Às vezes iniciantes começam a construir a hierarquia “pelos detalhes”, esquecendo do que é comum. Como resultado, surgem classes estranhas como RedCircleWithShadow, que não se encaixam bem na estrutura geral. Sempre destaque a abstração primeiro e depois os detalhes.
Erro nº 3: Hierarquias profundas demais.
Se sua cadeia de herança tiver mais de 3–4 níveis, pense se não é hora de usar composição ou interfaces em vez de herança.
Erro nº 4: Forçar a implementação de métodos não pertinentes.
Se a classe abstrata contém muitos métodos abstratos que não fazem sentido para alguns herdeiros, talvez seja hora de revisar a estrutura. Por exemplo, nem todo meio de transporte precisa de um método fuelUp() (não faz sentido para uma bicicleta).
Erro nº 5: Confundir classe abstrata com interface.
Classe abstrata — quando há estado comum e/ou implementação parcial. Interface — quando é preciso apenas “prometer” a existência de métodos, sem armazenar dados ou implementar comportamento. Não misture essas abordagens sem necessidade.
GO TO FULL VERSION