CodeGym /Cursos /JAVA 25 SELF /Exemplos de construção de abstrações em cenários reais

Exemplos de construção de abstrações em cenários reais

JAVA 25 SELF
Nível 19 , Lição 3
Disponível

1. Abstração em aplicativos reais: por que ela é necessária

Nas aulas anteriores, já vimos o conceito de abstração e analisamos exemplos simples. Agora veremos como essa abordagem funciona em problemas mais próximos da realidade. Em projetos reais, quase sempre lidamos com objetos diferentes, porém semelhantes. Por exemplo, diferentes formas de pagamento, diferentes tipos de transporte, diferentes figuras em um editor gráfico. Se não aplicarmos abstração, o código rapidamente se torna um amontoado de “if-else” e de cópia e cola. Com abstração — tudo fica consistente, bonito e, principalmente, conveniente para evoluir e manter.

A abstração permite:

  • Ocultar detalhes de implementação: trabalhar com objetos por meio de uma interface comum, sem se preocupar com o que há por dentro.
  • Evitar duplicação de código: o comportamento e os campos comuns são extraídos para a classe base.
  • Expandir o sistema com facilidade: adicionar novos tipos de objetos não exige reescrever o código antigo.
  • Tornar o código flexível: é possível substituir uma implementação por outra sem alterar o restante do sistema.

Vamos analisar alguns exemplos de áreas diferentes.

2. Exemplo 1: Sistemas de pagamento

Formulação do problema

Suponha que você esteja escrevendo um módulo para uma loja online. Sua tarefa é implementar o processamento de diferentes tipos de pagamentos: cartão bancário, PayPal, criptomoeda. Todos devem ser capazes de “processar um pagamento”, mas os detalhes variam.

Abstração: classe Payment

public abstract class Payment {
    protected double amount;

    public Payment(double amount) {
        this.amount = amount;
    }

    // Método abstrato: como exatamente processar o pagamento — os herdeiros decidem
    public abstract void process();

    // Método comum para todos os pagamentos
    public void printAmount() {
        System.out.println("Valor do pagamento: " + amount + " RUB");
    }
}

Implementações concretas

public class CreditCardPayment extends Payment {
    private String cardNumber;

    public CreditCardPayment(double amount, String cardNumber) {
        super(amount);
        this.cardNumber = cardNumber;
    }

    @Override
    public void process() {
        System.out.println("Processando pagamento por cartão: " + cardNumber);
        // Aqui poderia haver integração com o banco :)
    }
}
public class PaypalPayment extends Payment {
    private String email;

    public PaypalPayment(double amount, String email) {
        super(amount);
        this.email = email;
    }

    @Override
    public void process() {
        System.out.println("Processando pagamento do PayPal para a conta: " + email);
        // E aqui — chamada ao PayPal API
    }
}
public class CryptoPayment extends Payment {
    private String walletAddress;

    public CryptoPayment(double amount, String walletAddress) {
        super(amount);
        this.walletAddress = walletAddress;
    }

    @Override
    public void process() {
        System.out.println("Processando pagamento em cripto para a carteira: " + walletAddress);
        // Aqui poderia haver a magia do blockchain
    }
}

Uso da abstração

import java.util.*;

public class PaymentDemo {
    public static void main(String[] args) {
        List<Payment> payments = new ArrayList<>();
        payments.add(new CreditCardPayment(1500.0, "1234 5678 9012 3456"));
        payments.add(new PaypalPayment(500.0, "user@example.com"));
        payments.add(new CryptoPayment(0.05, "0xABCD..."));

        for (Payment payment : payments) {
            payment.printAmount();
            payment.process();
            System.out.println("---");
        }
    }
}

Resultado da execução:

Valor do pagamento: 1500.0 RUB
Processando pagamento por cartão: 1234 5678 9012 3456
---
Valor do pagamento: 500.0 RUB
Processando pagamento do PayPal para a conta: user@example.com
---
Valor do pagamento: 0.05 RUB
Processando pagamento em cripto para a carteira: 0xABCD...
---

Vantagens:

  • É possível adicionar um novo método de pagamento sem alterar o código antigo (por exemplo, Apple Pay).
  • O código que trabalha com pagamentos não depende do tipo específico.
  • A lógica comum (por exemplo, imprimir o valor via printAmount()) é implementada em um único lugar.

3. Exemplo 2: Transporte

Formulação do problema

Em um jogo ou simulador, você tem diferentes tipos de transporte: carros, bicicletas, trens. Todos podem “se mover”, mas fazem isso de maneiras diferentes. Alguns exigem abastecimento, outros — não.

Abstração: classe Transport

public abstract class Transport {
    protected String name;

    public Transport(String name) {
        this.name = name;
    }

    public abstract void move();

    // Nem todos os tipos de transporte precisam de abastecimento; por padrão — não
    public void fuelUp() {
        System.out.println(name + ": abastecimento não é necessário.");
    }
}

Implementações concretas

public class Car extends Transport {
    public Car(String name) {
        super(name);
    }

    @Override
    public void move() {
        System.out.println(name + " segue pela estrada.");
    }

    @Override
    public void fuelUp() {
        System.out.println(name + ": abastecendo com gasolina.");
    }
}
public class Bicycle extends Transport {
    public Bicycle(String name) {
        super(name);
    }

    @Override
    public void move() {
        System.out.println(name + " pedala.");
    }
    // Não sobrescrevemos fuelUp — a bicicleta não precisa de abastecimento
}
public class Train extends Transport {
    public Train(String name) {
        super(name);
    }

    @Override
    public void move() {
        System.out.println(name + " corre pelos trilhos.");
    }

    @Override
    public void fuelUp() {
        System.out.println(name + ": abastecendo com diesel ou eletricidade.");
    }
}

Uso da abstração

import java.util.*;

public class TransportDemo {
    public static void main(String[] args) {
        List<Transport> vehicles = Arrays.asList(
            new Car("Toyota"),
            new Bicycle("Stels"),
            new Train("Sapsan")
        );

        for (Transport t : vehicles) {
            t.move();
            t.fuelUp();
            System.out.println("---");
        }
    }
}

Resultado da execução:

Toyota segue pela estrada.
Toyota: abastecendo com gasolina.
---
Stels pedala.
Stels: abastecimento não é necessário.
---
Sapsan corre pelos trilhos.
Sapsan: abastecendo com diesel ou eletricidade.
---

Vantagens:

  • É possível tratar qualquer transporte da mesma forma, sem verificar seu tipo.
  • É fácil adicionar um novo tipo de transporte (por exemplo, patinete elétrico).

4. Exemplo 3: Editor gráfico

Formulação do problema

Você está escrevendo um mini editor gráfico. Nele há linhas, elipses, polígonos — e tudo isso são “figuras” que podem ser desenhadas e redimensionadas. Ao mesmo tempo, cada figura implementa essas ações à sua maneira.

Abstração: classe Figure

public abstract class Figure {
    protected String color = "black";

    public abstract void draw();

    public abstract void resize(double factor);

    public void setColor(String color) {
        this.color = color;
    }
}

Implementações concretas

public class Line extends Figure {
    private double length;

    public Line(double length) {
        this.length = length;
    }

    @Override
    public void draw() {
        System.out.println("Desenhando uma linha de comprimento " + length + " com a cor " + color);
    }

    @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 uma elipse com eixos " + a + " e " + b + " com a cor " + color);
    }

    @Override
    public void resize(double factor) {
        a *= factor;
        b *= factor;
        System.out.println("Novas dimensões da elipse: " + a + " x " + b);
    }
}
public class Polygon extends Figure {
    private int sides;

    public Polygon(int sides) {
        this.sides = sides;
    }

    @Override
    public void draw() {
        System.out.println("Desenhando um polígono com " + sides + " lados com a cor " + color);
    }

    @Override
    public void resize(double factor) {
        System.out.println("Redimensionando o polígono com " + sides + " lados por " + factor);
    }
}

Uso da abstração

import java.util.*;

public class EditorDemo {
    public static void main(String[] args) {
        List<Figure> figures = new ArrayList<>();
        figures.add(new Line(10));
        figures.add(new Ellipse(5, 3));
        figures.add(new Polygon(6));

        for (Figure f : figures) {
            f.setColor("green");
            f.draw();
            f.resize(2);
            System.out.println("---");
        }
    }
}

Resultado da execução:

Desenhando uma linha de comprimento 10.0 com a cor green
Novo comprimento da linha: 20.0
---
Desenhando uma elipse com eixos 5.0 e 3.0 com a cor green
Novas dimensões da elipse: 10.0 x 6.0
---
Desenhando um polígono com 6 lados com a cor green
Redimensionando o polígono com 6 lados por 2.0
---

Vantagens:

  • Todas as figuras podem ser armazenadas em uma única lista e processadas da mesma forma.
  • É fácil adicionar uma nova figura (por exemplo, uma estrela ou um coração).
  • Métodos comuns (por exemplo, definir a cor via setColor()) são implementados uma única vez.

5. Como a abstração ajuda a simplificar o código

Em cada exemplo acima há um padrão comum:

  • Uma classe base abstrata define o contrato (o que o objeto sabe fazer).
  • Classes filhas concretas implementam os detalhes.
  • O código que trabalha com a abstração não depende do tipo do objeto, o que torna o sistema flexível e extensível.

Tabela de comparação de abordagens

Sem abstração (if-else) Com abstração (POO)
Muitas condições por tipo Novo tipo — mudamos o código
Duplicação de lógica Lógica — em um só lugar
Difícil de expandir Adicionar — fácil
Difícil de testar Fácil substituir implementações

6. Erros comuns ao projetar abstrações

Erro nº 1: Abstração pela abstração.
Se você tem apenas um tipo de objeto e não planeja expandir — uma classe abstrata não é necessária. Não complique o código sem motivo.

Erro nº 2: Abstração genérica demais.
Se a classe base for muito “vaga”, os herdeiros podem não ter nada em comum além do nome. Por exemplo, a abstração “Thing” para qualquer coisa. Isso dificulta a manutenção e a compreensão do código.

Erro nº 3: Duplicação de código nas classes filhas.
Se todos os herdeiros tiverem a mesma implementação de um método, vale a pena extraí-lo para a classe base (torná-lo não abstrato).

Erro nº 4: Violação do princípio “do geral para o específico”.
Se na classe abstrata aparecerem detalhes necessários apenas para um herdeiro — a abstração foi escolhida incorretamente.

Erro nº 5: Esqueceu de implementar métodos abstratos.
Se não implementar todos os métodos abstratos na classe filha, o compilador forçará torná-la também abstrata. Às vezes isso surpreende :)

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION