Skip to content
IRC-CodingIRC-Coding
OOPHerenciaInheritancePolimorfismoLiskov SubstitutionComposiciónOverrideInterfaceJava

Herencia OOP: Inheritance y Polimorfismo

Aprende herencia, inheritance, polimorfismo, Liskov Substitution y composición. Ejemplos en Java para relaciones es-un.

S

schutzgeist

13 min read
Herencia OOP: Inheritance y Polimorfismo

Fundamentos de Herencia en POO: Inheritance y Polimorfismo

La herencia es un principio central de la programación orientada a objetos que permite definir propiedades y comportamientos comunes en una clase base y reutilizarlos en clases derivadas.

¿Qué es la herencia?

La herencia establece una relación “es un” entre tipos, donde una clase derivada hereda todos los miembros públicos y protegidos de la clase base, pudiendo extenderlos u override.

Conceptos clave de la herencia

  • Relación es-un: la clase derivada es una especialización de la clase base
  • Reutilización de código: el comportamiento común se define en un único lugar
  • Polimorfismo: los objetos pueden tratarse como el tipo base
  • Extensibilidad: se puede añadir nueva funcionalidad de forma controlada

Herencia básica en Java

Jerarquía de herencia simple

// Clase base abstracta
public abstract class Shape {
    protected String color;
    protected double x, y; // Posición
    
    public Shape(String color, double x, double y) {
        this.color = Objects.requireNonNull(color);
        this.x = x;
        this.y = y;
    }
    
    // Método abstracto - debe implementarse
    public abstract double area();
    public abstract double perimeter();
    
    // Método concreto - puede overridearse
    public void move(double dx, double dy) {
        this.x += dx;
        this.y += dy;
        System.out.println("Forma movida a (" + x + ", " + y + ")");
    }
    
    // Getters
    public String getColor() { return color; }
    public double getX() { return x; }
    public double getY() { return y; }
    
    @Override
    public String toString() {
        return "Shape{color='" + color + "', x=" + x + ", y=" + y + "}";
    }
}

// Clase derivada concreta
public class Rectangle extends Shape {
    private double width, height;
    
    public Rectangle(String color, double x, double y, double width, double height) {
        super(color, x, y); // Llamada al constructor de la clase base
        setDimensions(width, height);
    }
    
    @Override
    public double area() {
        return width * height;
    }
    
    @Override
    public double perimeter() {
        return 2 * (width + height);
    }
    
    @Override
    public void move(double dx, double dy) {
        super.move(dx, dy); // Invoca el método base
        System.out.println("Rectángulo movido");
    }
    
    // Métodos adicionales
    public void setDimensions(double width, double height) {
        if (width <= 0 || height <= 0) {
            throw new IllegalArgumentException("Las dimensiones deben ser positivas");
        }
        this.width = width;
        this.height = height;
    }
    
    public double getWidth() { return width; }
    public double getHeight() { return height; }
    
    @Override
    public String toString() {
        return "Rectangle{" + super.toString() + 
               ", width=" + width + ", height=" + height + "}";
    }
}

// Otra clase derivada
public class Circle extends Shape {
    private double radius;
    
    public Circle(String color, double x, double y, double radius) {
        super(color, x, y);
        setRadius(radius);
    }
    
    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
    
    @Override
    public double perimeter() {
        return 2 * Math.PI * radius;
    }
    
    public void setRadius(double radius) {
        if (radius <= 0) {
            throw new IllegalArgumentException("El radio debe ser positivo");
        }
        this.radius = radius;
    }
    
    public double getRadius() { return radius; }
    
    @Override
    public String toString() {
        return "Circle{" + super.toString() + ", radius=" + radius + "}";
    }
}

Uso polimórfico

public class ShapeDemo {
    public static void main(String[] args) {
        // Referencias polimórficas
        List<Shape> shapes = new ArrayList<>();
        
        shapes.add(new Rectangle("red", 0, 0, 5, 3));
        shapes.add(new Circle("blue", 10, 10, 2));
        shapes.add(new Rectangle("green", 5, 5, 2, 2));
        
        // Procesamiento polimórfico
        double totalArea = 0;
        for (Shape shape : shapes) {
            System.out.println(shape);
            System.out.println("Area: " + shape.area());
            System.out.println("Perimeter: " + shape.perimeter());
            totalArea += shape.area();
            System.out.println("---");
        }
        
        System.out.println("Total area: " + totalArea);
        
        // Invocaciones dinámicas de métodos
        processShapes(shapes);
    }
    
    public static void processShapes(List<Shape> shapes) {
        for (Shape shape : shapes) {
            // Dispatching dinámico: depende del tipo real del objeto
            shape.move(1, 1);
            
            // Verificación de tipo y casting
            if (shape instanceof Rectangle) {
                Rectangle rect = (Rectangle) shape;
                System.out.println("Rectangle: " + rect.getWidth() + "x" + rect.getHeight());
            } else if (shape instanceof Circle) {
                Circle circle = (Circle) shape;
                System.out.println("Circle with radius: " + circle.getRadius());
            }
        }
    }
}

Herencia en C#

Herencia en C# con Interfaces

// Interface para definir el contrato común
public interface IShape
{
    string Color { get; }
    double Area { get; }
    double Perimeter { get; }
    void Move(double dx, double dy);
}

// Clase base abstracta
public abstract class Shape : IShape
{
    protected string Color { get; set; }
    protected double X { get; set; }
    protected double Y { get; set; }
    
    protected Shape(string color, double x, double y)
    {
        Color = color ?? throw new ArgumentNullException(nameof(color));
        X = x;
        Y = y;
    }
    
    // Implementación de interface
    public string GetColor() => Color;
    
    // Propiedades abstractas
    public abstract double Area { get; }
    public abstract double Perimeter { get; }
    
    // Método virtual: puede sobreescribirse
    public virtual void Move(double dx, double dy)
    {
        X += dx;
        Y += dy;
        Console.WriteLine($"Forma movida a ({X}, {Y})");
    }
    
    public override string ToString()
    {
        return $"Shape{{Color='{Color}', X={X}, Y={Y}}}";
    }
}

// Clase concreta
public class Rectangle : Shape
{
    public double Width { get; private set; }
    public double Height { get; private set; }
    
    public Rectangle(string color, double x, double y, double width, double height) 
        : base(color, x, y)
    {
        SetDimensions(width, height);
    }
    
    public override double Area => Width * Height;
    public override double Perimeter => 2 * (Width + Height);
    
    public override void Move(double dx, double dy)
    {
        base.Move(dx, dy);
        Console.WriteLine("Rectángulo movido");
    }
    
    public void SetDimensions(double width, double height)
    {
        if (width <= 0 || height <= 0)
            throw new ArgumentException("Las dimensiones deben ser positivas");
        
        Width = width;
        Height = height;
    }
    
    public override string ToString()
    {
        return $"Rectangle{{{base.ToString()}, Width={Width}, Height={Height}}}";
    }
}

Herencia en Python

from abc import ABC, abstractmethod
from typing import List

# Clase base abstracta
class Shape(ABC):
    def __init__(self, color: str, x: float, y: float):
        if not color:
            raise ValueError("Color darf nicht leer sein")
        self.color = color
        self.x = x
        self.y = y
    
    @abstractmethod
    def area(self) -> float:
        """Berechnet die Fläche der Form"""
        pass
    
    @abstractmethod
    def perimeter(self) -> float:
        """Berechnet den Umfang der Form"""
        pass
    
    def move(self, dx: float, dy: float):
        """Verschiebt die Form"""
        self.x += dx
        self.y += dy
        print(f"Form verschoben nach ({self.x}, {self.y})")
    
    def __str__(self):
        return f"Shape{{color='{self.color}', x={self.x}, y={self.y}}}"

# Subclase concreta
class Rectangle(Shape):
    def __init__(self, color: str, x: float, y: float, width: float, height: float):
        super().__init__(color, x, y)  # Aufruf des Basisklassen-Konstruktors
        self.set_dimensions(width, height)
    
    def area(self) -> float:
        return self.width * self.height
    
    def perimeter(self) -> float:
        return 2 * (self.width + self.height)
    
    def move(self, dx: float, dy: float):
        super().move(dx, dy)  # Basismethode aufrufen
        print("Rechteck bewegt")
    
    def set_dimensions(self, width: float, height: float):
        if width <= 0 or height <= 0:
            raise ValueError("Maße müssen positiv sein")
        self.width = width
        self.height = height
    
    def __str__(self):
        return f"Rectangle{{{super().__str__()}, width={self.width}, height={self.height}}}"

# Otra subclase
class Circle(Shape):
    def __init__(self, color: str, x: float, y: float, radius: float):
        super().__init__(color, x, y)
        self.set_radius(radius)
    
    def area(self) -> float:
        return math.pi * self.radius ** 2
    
    def perimeter(self) -> float:
        return 2 * math.pi * self.radius
    
    def set_radius(self, radius: float):
        if radius <= 0:
            raise ValueError("Radius muss positiv sein")
        self.radius = radius
    
    def __str__(self):
        return f"Circle{{{super().__str__()}, radius={self.radius}}}"

# Uso polimórfico
def process_shapes(shapes: List[Shape]):
    total_area = 0
    for shape in shapes:
        print(shape)
        print(f"Fläche: {shape.area():.2f}")
        print(f"Umfang: {shape.perimeter():.2f}")
        total_area += shape.area()
        
        # Type Checking mit isinstance
        if isinstance(shape, Rectangle):
            print(f"Rechteck: {shape.width}x{shape.height}")
        elif isinstance(shape, Circle):
            print(f"Kreis mit Radius: {shape.radius}")
        
        print("---")
    
    print(f"Gesamtfläche: {total_area:.2f}")

# Verwendung
shapes = [
    Rectangle("rot", 0, 0, 5, 3),
    Circle("blau", 10, 10, 2),
    Rectangle("grün", 5, 5, 2, 2)
]

process_shapes(shapes)

Principio de Sustitución de Liskov (LSP)

Entendiendo el principio

El Principio de Sustitución de Liskov establece que las subclases deben poder reemplazar a sus clases base sin que el comportamiento del programa cambie de forma inesperada.

Ejemplo de violación del LSP

// Mal diseño - Viola LSP
public class Rectangle {
    protected double width, height;
    
    public void setWidth(double width) {
        this.width = width;
    }
    
    public void setHeight(double height) {
        this.height = height;
    }
    
    public double getWidth() { return width; }
    public double getHeight() { return height; }
    
    public double area() {
        return width * height;
    }
}

// Subclase problemática
public class Square extends Rectangle {
    @Override
    public void setWidth(double width) {
        super.setWidth(width);
        super.setHeight(width); // Quadrat muss gleiche Seiten haben
    }
    
    @Override
    public void setHeight(double height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}

// Violación de LSP en la práctica
public void testRectangle(Rectangle rect) {
    rect.setWidth(5);
    rect.setHeight(4);
    // Erwartung: area() == 20
    // Bei Square: area() == 16 (unerwartet!)
    assert rect.area() == 20 : "LSP verletzt!";
}

Diseño conforme con LSP

// Mejor: Clase base abstracta
public abstract class Shape {
    public abstract double area();
    public abstract double perimeter();
}

// Rectangle como clase independiente
public class Rectangle extends Shape {
    private double width, height;
    
    public Rectangle(double width, double height) {
        setDimensions(width, height);
    }
    
    public void setDimensions(double width, double height) {
        if (width <= 0 || height <= 0) {
            throw new IllegalArgumentException("Maße müssen positiv sein");
        }
        this.width = width;
        this.height = height;
    }
    
    @Override
    public double area() {
        return width * height;
    }
    
    @Override
    public double perimeter() {
        return 2 * (width + height);
    }
    
    public double getWidth() { return width; }
    public double getHeight() { return height; }
}

// Square como clase independiente
public class Square extends Shape {
    private double side;
    
    public Square(double side) {
        setSide(side);
    }
    
    public void setSide(double side) {
        if (side <= 0) {
            throw new IllegalArgumentException("Seite muss positiv sein");
        }
        this.side = side;
    }
    
    @Override
    public double area() {
        return side * side;
    }
    
    @Override
    public double perimeter() {
        return 4 * side;
    }
    
    public double getSide() { return side; }
}

Composición antes que herencia

El problema de las jerarquías rígidas

// Mal: Jerarquía de herencia profunda
public class Animal {
    public void eat() { System.out.println("Eating"); }
}

public class Mammal extends Animal {
    public void walk() { System.out.println("Walking"); }
}

public class Dog extends Mammal {
    public void bark() { System.out.println("Barking"); }
}

public class RobotDog extends Dog {
    // Problema: RobotDog ist kein echtes Tier!
    @Override
    public void eat() { 
        throw new UnsupportedOperationException("Robots don't eat"); 
    }
}

Mejor: Composición

// Interfaces para comportamientos
public interface Eater {
    void eat();
}

public interface Walker {
    void walk();
}

public interface Barker {
    void bark();
}

// Clase base con comportamiento fundamental
public class Animal implements Eater {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    @Override
    public void eat() {
        System.out.println(name + " is eating");
    }
}

// Composición para comportamientos
public class Dog implements Walker, Barker {
    private Animal animal;
    
    public Dog(String name) {
        this.animal = new Animal(name);
    }
    
    @Override
    public void walk() {
        System.out.println(animal.name + " is walking");
    }
    
    @Override
    public void bark() {
        System.out.println(animal.name + " says: Woof!");
    }
    
    // Delegation an Animal
    public void eat() {
        animal.eat();
    }
}

// Más flexible para RobotDog
public class RobotDog implements Walker, Barker {
    private String name;
    private int batteryLevel;
    
    public RobotDog(String name) {
        this.name = name;
        this.batteryLevel = 100;
    }
    
    @Override
    public void walk() {
        if (batteryLevel > 10) {
            System.out.println(name + " is walking on wheels");
            batteryLevel -= 5;
        } else {
            System.out.println(name + " needs charging");
        }
    }
    
    @Override
    public void bark() {
        System.out.println(name + " says: Electronic Woof!");
    }
    
    public void charge() {
        batteryLevel = 100;
        System.out.println(name + " is fully charged");
    }
}

Clases abstractas vs Interfaces

Clase abstracta

// Clase abstracta con implementación común
public abstract class Vehicle {
    protected String brand;
    protected int year;
    
    public Vehicle(String brand, int year) {
        this.brand = brand;
        this.year = year;
    }
    
    // Método concreto
    public void startEngine() {
        System.out.println("Engine starting...");
    }
    
    // Métodos abstractos
    public abstract void accelerate();
    public abstract void brake();
    
    // Template Method Pattern
    public final void drive() {
        startEngine();
        accelerate();
        System.out.println("Driving...");
        brake();
    }
    
    // Getters
    public String getBrand() { return brand; }
    public int getYear() { return year; }
}

public class Car extends Vehicle {
    public Car(String brand, int year) {
        super(brand, year);
    }
    
    @Override
    public void accelerate() {
        System.out.println("Car accelerating");
    }
    
    @Override
    public void brake() {
        System.out.println("Car braking");
    }
}

Interface

// Interface para comportamiento
public interface Electric {
    void charge();
    int getBatteryLevel();
}

public interface Autonomous {
    void enableAutopilot();
    boolean isAutopilotActive();
}

// Clase implementa múltiples interfaces
public class Tesla extends Vehicle implements Electric, Autonomous {
    private int batteryLevel = 80;
    private boolean autopilotActive = false;
    
    public Tesla(String brand, int year) {
        super(brand, year);
    }
    
    @Override
    public void accelerate() {
        System.out.println("Tesla accelerating silently");
    }
    
    @Override
    public void brake() {
        System.out.println("Tesla regenerative braking");
    }
    
    // Implementaciones de interfaces
    @Override
    public void charge() {
        System.out.println("Tesla charging...");
        batteryLevel = 100;
    }
    
    @Override
    public int getBatteryLevel() {
        return batteryLevel;
    }
    
    @Override
    public void enableAutopilot() {
        autopilotActive = true;
        System.out.println("Autopilot enabled");
    }
    
    @Override
    public boolean isAutopilotActive() {
        return autopilotActive;
    }
}

El problema del diamante

Herencia múltiple en C++

#include <iostream>

class Animal {
public:
    void eat() { std::cout << "Animal eating" << std::endl; }
};

class Mammal : virtual public Animal {
public:
    void walk() { std::cout << "Mammal walking" << std::endl; }
};

class Bird : virtual public Animal {
public:
    void fly() { std::cout << "Bird flying" << std::endl; }
};

// Problema del diamante resuelto con herencia virtual
class Bat : public Mammal, public Bird {
public:
    void echolocate() { std::cout << "Bat echolocating" << std::endl; }
};

int main() {
    Bat bat;
    bat.eat();        // Sin ambigüedad gracias a herencia virtual
    bat.walk();       // De Mammal
    bat.fly();        // De Bird
    bat.echolocate(); // Método propio
    return 0;
}

Solución basada en interfaces en Java/C#

// Interface para capacidades
public interface Flyable {
    void fly();
}

public interface Walkable {
    void walk();
}

// Clase base
public abstract class Animal {
    public abstract void eat();
}

// Clase implementa múltiples interfaces
public class Bat extends Animal implements Flyable, Walkable {
    @Override
    public void eat() {
        System.out.println("Bat eating insects");
    }
    
    @Override
    public void fly() {
        System.out.println("Bat flying");
    }
    
    @Override
    public void walk() {
        System.out.println("Bat walking");
    }
    
    public void echolocate() {
        System.out.println("Bat echolocating");
    }
}

Buenas prácticas en herencia

1. Evitar jerarquías profundas

// Mal: Demasiado profundo
class Animal -> Mammal -> Dog -> Labrador -> GoldenRetriever

// Mejor: Más plano
class Animal -> Dog
class Dog -> Labrador
class Dog -> GoldenRetriever

2. Usar final para clases estables

public final class ImmutablePoint {
    private final double x, y;
    
    public ImmutablePoint(double x, double y) {
        this.x = x;
        this.y = y;
    }
    
    // No puede heredarse, lo que garantiza estabilidad
}

3. Template Method Pattern

public abstract class DataProcessor {
    
    // Template Method: define el flujo
    public final void processData() {
        loadData();
        validateData();
        transformData();
        saveData();
        cleanup();
    }
    
    // Métodos concretos
    private void loadData() {
        System.out.println("Loading data...");
    }
    
    private void cleanup() {
        System.out.println("Cleaning up...");
    }
    
    // Métodos abstractos, implementados por subclases
    protected abstract void validateData();
    protected abstract void transformData();
    protected abstract void saveData();
}

public class CSVProcessor extends DataProcessor {
    @Override
    protected void validateData() {
        System.out.println("Validating CSV data");
    }
    
    @Override
    protected void transformData() {
        System.out.println("Transforming CSV data");
    }
    
    @Override
    protected void saveData() {
        System.out.println("Saving CSV data");
    }
}

Conceptos relevantes para exámenes

Distinciones importantes

  1. Sobrescritura vs Sobrecarga

    • Sobrescritura: misma firma en la subclase
    • Sobrecarga: mismo nombre, parámetros diferentes
  2. Clase abstracta vs Interface

    • Clase abstracta: implementación común
    • Interface: contrato puro
  3. Es-un vs Tiene-un

    • Es-un: herencia
    • Tiene-un: composición
  4. Covarianza vs Contravarianza

    • Covarianza: tipos de retorno más específicos
    • Contravarianza: tipos de parámetros más generales

Tareas típicas de examen

// Ejemplo de polimorfismo
public class PolymorphismDemo {
    public static void main(String[] args) {
        Shape[] shapes = {
            new Rectangle("rojo", 0, 0, 5, 3),
            new Circle("azul", 10, 10, 2)
        };
        
        for (Shape shape : shapes) {
            // Dispatch dinámico
            System.out.println(shape.area());
            
            // Casting de tipo
            if (shape instanceof Rectangle) {
                Rectangle rect = (Rectangle) shape;
                System.out.println("Width: " + rect.getWidth());
            }
        }
    }
}

Conclusión

La herencia es una herramienta poderosa, pero requiere aplicación cuidadosa:

  • Usa herencia para relaciones es-un reales
  • Respeta el Liskov Substitution Principle
  • Prefiere composición cuando solo necesitas reutilizar código
  • Mantén las jerarquías planas y estables
  • Utiliza interfaces para contratos flexibles
  • Emplea clases abstractas para implementación compartida

Una herencia bien diseñada promueve reutilización y polimorfismo, mientras que una herencia deficiente conduce a acoplamiento fuerte y arquitecturas frágiles.

Continuamos con la ruta de aprendizaje de OOP

Ya hemos cubierto todos los artículos de OOP. Vuelve al primer artículo: Fundamentos de Programación Orientada a Objetos OOP.

Volver al blog
Share:

Nächster Artikel in Programación Orientada a Objetos

Weiterlesen
Fundamentos OOP: Clases, Objetos, Instancias y Métodos

Entradas relacionadas