1. Abstracción en aplicaciones reales: para qué sirve
En lecciones anteriores ya nos familiarizamos con el concepto de abstracción y vimos ejemplos sencillos. Ahora veremos cómo funciona este enfoque en tareas más reales. En proyectos reales casi siempre hay que tratar con objetos distintos pero parecidos. Por ejemplo, distintos métodos de pago, diferentes tipos de transporte, distintas figuras en un editor gráfico. Si no aplicas abstracción, el código se convierte rápidamente en un conjunto de «if-else» y copypaste. Con la abstracción — todo es más estricto, limpio y, lo más importante, cómodo para evolucionar y mantener.
La abstracción permite:
- Ocultar los detalles de implementación: trabajar con objetos mediante una interfaz común sin interesarse por cómo están hechos por dentro.
- Evitar la duplicación de código: el comportamiento y los campos comunes se extraen a una clase base.
- Ampliar el sistema con facilidad: añadir nuevos tipos de objetos no requiere reescribir el código existente.
- Hacer el código flexible: se puede sustituir una implementación por otra sin cambiar el resto del sistema.
Veamos varios ejemplos de diferentes áreas.
2. Ejemplo 1: Sistemas de pago
Planteamiento del problema
Supongamos que escribes un módulo para una tienda online. Tu tarea es implementar el procesamiento de distintos tipos de pagos: tarjeta bancaria, PayPal, criptomonedas. Todos deben poder «procesar un pago», pero cada uno con sus propios detalles.
Abstracción: clase Payment
public abstract class Payment {
protected double amount;
public Payment(double amount) {
this.amount = amount;
}
// Método abstracto: cómo procesar el pago lo deciden las subclases
public abstract void process();
// Método común para todos los pagos
public void printAmount() {
System.out.println("Importe del pago: " + amount + " rublos");
}
}
Implementaciones 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("Procesamiento del pago con tarjeta: " + cardNumber);
// Aquí podría ir la integración con el 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("Procesamiento del pago de PayPal para la cuenta: " + email);
// Y aquí — llamada al API de PayPal
}
}
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("Procesamiento del pago con criptomonedas al monedero: " + walletAddress);
// Aquí podría haber magia de blockchain
}
}
Uso de la abstracción
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 de la ejecución:
Importe del pago: 1500.0 rublos
Procesamiento del pago con tarjeta: 1234 5678 9012 3456
---
Importe del pago: 500.0 rublos
Procesamiento del pago de PayPal para la cuenta: user@example.com
---
Importe del pago: 0.05 rublos
Procesamiento del pago con criptomonedas al monedero: 0xABCD...
---
Ventajas:
- Se puede añadir un nuevo método de pago sin cambiar el código antiguo (por ejemplo, Apple Pay).
- El código que trabaja con pagos no depende de su tipo concreto.
- La lógica común (por ejemplo, imprimir el importe mediante printAmount()) está implementada en un solo lugar.
3. Ejemplo 2: Transporte
Planteamiento del problema
En un juego o simulador tienes distintos tipos de transporte: coches, bicicletas, trenes. Todos pueden «moverse», pero lo hacen de formas diferentes. Algunos requieren repostaje, otros no.
Abstracción: clase Transport
public abstract class Transport {
protected String name;
public Transport(String name) {
this.name = name;
}
public abstract void move();
// No todos los tipos de transporte requieren repostaje; por defecto — no
public void fuelUp() {
System.out.println(name + ": no requiere repostaje.");
}
}
Implementaciones concretas
public class Car extends Transport {
public Car(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " circula por la carretera.");
}
@Override
public void fuelUp() {
System.out.println(name + ": repostamos gasolina.");
}
}
public class Bicycle extends Transport {
public Bicycle(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " pedalea.");
}
// No sobrescribimos fuelUp — la bicicleta no necesita repostaje
}
public class Train extends Transport {
public Train(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " va a toda velocidad por las vías.");
}
@Override
public void fuelUp() {
System.out.println(name + ": repostamos diésel o electricidad.");
}
}
Uso de la abstracción
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 de la ejecución:
Toyota circula por la carretera.
Toyota: repostamos gasolina.
---
Stels pedalea.
Stels: no requiere repostaje.
---
Sapsan va a toda velocidad por las vías.
Sapsan: repostamos diésel o electricidad.
---
Ventajas:
- Se puede procesar cualquier transporte de la misma manera sin comprobar su tipo.
- Es fácil añadir un nuevo tipo de transporte (por ejemplo, un patinete eléctrico).
4. Ejemplo 3: Editor gráfico
Planteamiento del problema
Estás escribiendo un mini editor gráfico. Tiene líneas, elipses, polígonos — y todo ello son «figuras» que se pueden dibujar y redimensionar. Al mismo tiempo, cada figura implementa estas acciones a su manera.
Abstracción: clase 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;
}
}
Implementaciones concretas
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("Dibujamos una línea de longitud " + length + " con color " + color);
}
@Override
public void resize(double factor) {
length *= factor;
System.out.println("Nueva longitud de la línea: " + 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("Dibujamos una elipse con ejes " + a + " y " + b + " y color " + color);
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("Nuevas dimensiones de la 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("Dibujamos un polígono con " + sides + " lados y color " + color);
}
@Override
public void resize(double factor) {
System.out.println("Cambiamos el tamaño del polígono con " + sides + " lados por " + factor);
}
}
Uso de la abstracción
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 de la ejecución:
Dibujamos una línea de longitud 10.0 con color green
Nueva longitud de la línea: 20.0
---
Dibujamos una elipse con ejes 5.0 y 3.0 y color green
Nuevas dimensiones de la elipse: 10.0 x 6.0
---
Dibujamos un polígono con 6 lados y color green
Cambiamos el tamaño del polígono con 6 lados por 2.0
---
Ventajas:
- Todas las figuras se pueden guardar en una sola lista y procesar de la misma forma.
- Es fácil añadir una figura nueva (por ejemplo, una estrella o un corazón).
- Los métodos comunes (por ejemplo, establecer el color mediante setColor()) se implementan una sola vez.
5. Cómo ayuda la abstracción a simplificar el código
En cada ejemplo anterior hay un esquema común:
- Una clase base abstracta establece el contrato (lo que el objeto sabe hacer).
- Las subclases concretas implementan los detalles.
- El código que trabaja con la abstracción no depende del tipo de objeto, lo que hace al sistema flexible y extensible.
Tabla comparativa de enfoques
| Sin abstracción (if-else) | Con abstracción (POO) |
|---|---|
| Muchas condiciones por tipo | Nuevo tipo — cambiamos el código |
| Duplicación de lógica | La lógica — en un solo lugar |
| Difícil de ampliar | Agregar — fácil |
| Difícil de probar | Fácil sustituir implementaciones |
6. Errores típicos al diseñar abstracciones
Error n.º 1: Abstracción por la abstracción.
Si solo tienes un tipo de objeto y no se planea ampliación, no necesitas una clase abstracta. No compliques el código sin motivo.
Error n.º 2: Abstracción demasiado general.
Si la clase base es demasiado «difusa», las subclases pueden no tener nada en común salvo el nombre. Por ejemplo, la abstracción «Thing» para todo. Esto dificulta el mantenimiento y la comprensión del código.
Error n.º 3: Duplicación de código en las subclases.
Si todas las subclases tienen la misma implementación de un método, conviene extraerlo a la clase base (hacerlo no abstracto).
Error n.º 4: Violación del principio «de lo general a lo particular».
Si en la clase abstracta aparecen detalles que solo necesita una subclase, significa que la abstracción se ha elegido mal.
Error n.º 5: Olvidar implementar los métodos abstractos.
Si no se implementan todos los métodos abstractos en la subclase, el compilador obligará a hacer la clase también abstracta. A veces esto resulta inesperado :)
GO TO FULL VERSION