CodeGym /Cursos /JAVA 25 SELF /Implementación de abstracciones y jerarquías

Implementación de abstracciones y jerarquías

JAVA 25 SELF
Nivel 19 , Lección 2
Disponible

1. Construir una jerarquía: de la abstracción al detalle

Implementar abstracciones y jerarquías es una manera de estructurar el código desde reglas generales hasta detalles concretos. Primero describimos qué deben poder hacer todos los objetos (abstracción) y luego concretamos cómo lo hace cada clase específica.

En programación, como en la vida, todo empieza con preguntas. Por ejemplo: «¿Qué tienen en común el círculo y el rectángulo?» Respuesta: ambos son figuras. ¿Y qué tienen en común las figuras? Normalmente tienen área y se pueden dibujar.

En Java esto se expresa mediante una clase abstract:

public abstract class Shape {
    public abstract double area();
    public abstract void draw();
}

Aquí decimos:

  • Cualquier figura debe saber calcular su área (area()).
  • Cualquier figura debe poder dibujarse (draw()).
  • Cómo lo hace exactamente — no es asunto nuestro (por ahora).

Ahora creemos 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("Dibujamos un círculo de radio " + 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("Dibujamos un rectángulo " + width + "x" + height);
    }
}

¿Qué hemos hecho?

  • Hemos extraído lo común a la clase abstracta.
  • Hemos detallado el comportamiento en las subclases.

Esquemáticamente:


.          Shape
         /     \
     Circle   Rectangle

Tabla: qué está implementado dónde

Clase area() draw() Campos propios
Shape
abstract
abstract
-
Circle implementado implementado
radius
Rectangle implementado implementado
width, height

2. ¿Por qué es útil? (y por qué funciona)

Interfaz unificada para trabajar con distintos objetos

Supongamos que tienes una colección de figuras:

Shape[] shapes = {
    new Circle(5),
    new Rectangle(3, 4),
    new Circle(2.5)
};

Puedes iterarlas de la misma manera, sin pensar en el tipo:

for (Shape shape : shapes) {
    shape.draw();
    System.out.println("Área: " + shape.area());
}

Que la JVM se encargue de decidir quién es círculo y quién rectángulo. Eso es polimorfismo (ya hemos hablado de ello y volveremos a tratarlo en detalle — en el siguiente bloque).

Facilidad de ampliación

¿Quieres añadir un triángulo? Simplemente escribe (tipo nuevo — código antiguo sin cambios): 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("Dibujamos un triángulo: base " + base + ", altura " + height);
    }
}

El resto del código (por ejemplo, el bucle que recorre la lista de figuras) no necesita cambios.

Evitar la duplicación de código

Si todas las figuras comparten una propiedad (por ejemplo, el color), conviene extraerla a la clase abstracta:

public abstract class Shape {
    private String color = "negro";

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

    public abstract double area();
    public abstract void draw();
}

Ahora cualquier subclase —sea un círculo o un triángulo— heredará el color «por herencia».

3. Práctica: desarrollamos un mini editor gráfico

Probemos a juntar todo. Imagina que estás creando un editor gráfico sencillo.

Clase abstracta 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("Dibujamos una línea de longitud " + length + " con color " + getColor());
    }

    @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 + " con color " + getColor());
    }

    @Override
    public void resize(double factor) {
        a *= factor;
        b *= factor;
        System.out.println("Nuevos ejes de la elipse: " + a + ", " + b);
    }
}

¿Quieres añadir una herramienta nueva, por ejemplo Polygon? Solo crea una clase nueva — todo el código del editor funciona a través de la clase abstracta Figure.

Uso en el código

Figure[] figures = {
    new Line(10),
    new Ellipse(5, 3)
};

for (Figure figure : figures) {
    figure.setColor("red");
    figure.draw();
    figure.resize(1.5);
}

Salida:

Dibujamos una línea de longitud 10.0 con color red
Nueva longitud de la línea: 15.0
Dibujamos una elipse con ejes 5.0 y 3.0 con color red
Nuevos ejes de la elipse: 7.5, 4.5

Visualización de la jerarquía


.            Figure
             /    \
          Line   Ellipse

4. Cómo evitar la duplicación: campos y métodos comunes

A veces, todos los descendientes comparten no solo métodos, sino también campos (por ejemplo, las coordenadas del centro). La clase abstracta es el lugar ideal para esto:

public abstract class Figure {
    private double x, y; // coordenadas del 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("La figura se ha movido al punto (" + x + ", " + y + ")");
    }

    public abstract void draw();
}

Ahora cualquier Line o Ellipse puede moverse sin volver a implementar este método.

5. Otro ejemplo: sistemas de pago

¡La abstracción no es solo para figuras! Imagina que escribes un sistema de procesamiento de pagos.

Clase abstracta Payment

public abstract class Payment {
    public abstract void process();
}

Implementaciones concretas

public class CreditCardPayment extends Payment {
    @Override
    public void process() {
        System.out.println("Procesamiento de pago con tarjeta de crédito");
    }
}

public class PaypalPayment extends Payment {
    @Override
    public void process() {
        System.out.println("Procesamiento de pago a través de PayPal");
    }
}

Uso

Payment[] payments = {
    new CreditCardPayment(),
    new PaypalPayment()
};

for (Payment payment : payments) {
    payment.process();
}

Salida:

Procesamiento de pago con tarjeta de crédito
Procesamiento de pago a través de PayPal

6. Ventajas de este enfoque

  • Interfaz unificada: puedes trabajar con objetos distintos de la misma manera.
  • Extensibilidad: añadir nuevos tipos de objetos no requiere reescribir el código existente.
  • Mínima duplicación: lo común se ha extraído a la clase base abstracta.
  • Flexibilidad: puedes usar colecciones de tipos abstractos sin preocuparte por los detalles.

7. Ejemplo de la vida real: transporte

La abstracción aparece no solo en los libros. Por ejemplo, si diseñas un sistema para gestionar transporte:

public abstract class Transport {
    public abstract void move();
    public abstract void fuelUp();
}

Los tipos de transporte concretos implementan los detalles:

public class Car extends Transport {
    @Override
    public void move() {
        System.out.println("El coche circula por la carretera");
    }

    @Override
    public void fuelUp() {
        System.out.println("Repostamos gasolina");
    }
}

public class Bicycle extends Transport {
    @Override
    public void move() {
        System.out.println("La bicicleta avanza pedaleando");
    }

    @Override
    public void fuelUp() {
        System.out.println("La bicicleta no necesita combustible, ¡solo bocadillos para el ciclista!");
    }
}

8. Esquema útil: cómo construir una jerarquía de abstracciones

[Clase abstracta]
        |
   [Subclase concreta]
        |
   [Subclase aún más concreta] (si hace falta)

- ¡Todo lo común — arriba!
- ¡Todo lo específico — abajo!

9. Errores típicos al implementar abstracciones y jerarquías

Error n.º 1: duplicación de código en las subclases.
Si notas que en cada subclase escribes los mismos campos o métodos, es una señal de que deberías extraerlos a la clase abstracta. No temas hacer la abstracción «más amplia» si eso reduce la duplicación.

Error n.º 2: violar el principio «de lo general a lo particular».
A veces los programadores principiantes empiezan a construir la jerarquía «desde los detalles», olvidando lo común. Como resultado aparecen clases extrañas como RedCircleWithShadow, que encajan mal en la estructura general. Primero identifica la abstracción y después los detalles.

Error n.º 3: jerarquías demasiado profundas.
Si tu cadena de herencia tiene más de 3–4 niveles, plantéate si no ha llegado el momento de usar composición o interfaces en lugar de herencia.

Error n.º 4: implementación forzada de métodos no pertinentes.
Si la clase abstracta contiene demasiados métodos abstractos que no son pertinentes para algunos descendientes, quizá debas revisar la estructura. Por ejemplo, no todos los vehículos necesitan el método fuelUp() (a la bicicleta no le hace falta).

Error n.º 5: confundir clase abstracta e interfaz.
Clase abstracta — cuando hay estado común y/o una implementación parcial. Interfaz — cuando solo necesitas «prometer» la existencia de métodos, sin almacenar datos ni implementar comportamiento. No mezcles estos enfoques sin necesidad.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION