Atlasingeniería

Algoritmos y programaciónProgramación orientada a objetosTema 2

Herencia, composición y polimorfismo

Herencia modela sustitución, composición arma comportamiento con colaboradores y polimorfismo permite usar implementaciones distintas detrás de un mismo contrato.

Para este tema conviene tener claro:Clases, objetos y estado

Reutilizar código no alcanza para justificar herencia. La pregunta es más exigente: ¿cada instancia de la subclase puede reemplazar a la clase base sin sorprender a quien usa el contrato?

Heredar es prometer sustitución

La herencia crea una relación de subtipo. Una implementación derivada hereda una interfaz y debe respetar sus expectativas: aceptar al menos las mismas entradas y conservar las garantías de salida.

Si una subclase necesita prohibir operaciones normales de la base o reinterpretar su significado, la jerarquía está modelando parecido de datos, no sustitución real.

Una función vieja del sistema recibe un rectángulo, le pone ancho 5 y alto 4, y espera área 20.

1 / 6
El código que rompe no sabe que existe el cuadrado: pide un rectángulo y hace lo que un rectángulo permite. Por eso la herencia no se juzga mirando la subclase, se juzga mirando lo que ya escribió otro y va a recibirla sin enterarse.

Antes de seguir, predecí

Un Cuadrado hereda de Rectángulo. Una función vieja le pone ancho 5 y alto 4. ¿Qué área devuelve?

Componer es tener colaboradores

Composición significa construir un objeto con colaboradores. Un servicio de reportes puede recibir un repositorio y un renderer sin ser una subclase de ninguno. Delega tareas mediante contratos pequeños.

Esto permite reemplazar piezas independientemente y evita acoplar el comportamiento a una jerarquía rígida. La relación es “tiene un” o “usa un”, no “es un”.

La misma operación, implementaciones distintas

Polimorfismo es enviar la misma operación a implementaciones distintas:

type NotificationChannel = {
  send(recipient: string, message: string): Promise<void>;
};

const notify = (
  channel: NotificationChannel,
  recipient: string,
  message: string,
): Promise<void> => channel.send(recipient, message);

Correo y mensajería pueden cumplir ese contrato sin compartir una clase base concreta.

Escena 1 — Una sola línea, tres métodos distintos

paso a paso

Cargando la escena…

La línea que llama es siempre la misma y el método que corre lo decide la clase del objeto, en tiempo de ejecución. La última clase no define nada: ahí se ve la búsqueda subiendo por la cadena de herencia.

Cuándo cada una

Usá herencia cuando existe una relación estable de sustitución y la base fue diseñada para extenderse. Usá composición cuando querés combinar capacidades, cambiar políticas o aislar infraestructura.

El polimorfismo no exige herencia: interfaces, funciones y tipos suma también permiten variar comportamiento. Elegí el mecanismo más chico que preserve el contrato.

La misma funcionalidad, de tres formas

// 1. Herencia: la subclase promete lo mismo que la base, y algo más
abstract class Notifier {
  abstract send(message: string): Promise<void>;

  async notifyAll(messages: readonly string[]): Promise<void> {
    for (const message of messages) await this.send(message);
  }
}

class EmailNotifier extends Notifier {
  async send(message: string): Promise<void> { /* ... */ }
}

// 2. Composición: se recibe lo que hace falta, sin ser nada
type Sender = { send(message: string): Promise<void> };

const notifyAll = async (sender: Sender, messages: readonly string[]): Promise<void> => {
  for (const message of messages) await sender.send(message);
};

// 3. Función: cuando la abstracción es una sola operación
const notifyWith = async (
  send: (message: string) => Promise<void>,
  messages: readonly string[],
): Promise<void> => {
  for (const message of messages) await send(message);
};

Las tres resuelven lo mismo y se prueban distinto. Para la primera hay que crear una subclase de prueba; para la segunda, un objeto con un método; para la tercera, una función. La dificultad de escribir el test es, otra vez, la mejor señal sobre el diseño.

La tercera es la que más se subestima: cuando la abstracción tiene una sola operación, una interfaz y una clase son un envoltorio alrededor de una función.

La regla de Liskov, en la práctica

Una subclase no puede...EjemploQué hacer
pedir más que la basela base acepta cualquier entero, la subclase sólo positivosno heredar: el contrato no se cumple
prometer menosla base devuelve una lista ordenada, la subclase nolo mismo
lanzar errores nuevosla subclase falla donde la base funcionabadevolver un resultado explícito en la base
prohibir una operaciónuna pila de sólo lectura que hereda de pilapartir la interfaz
cambiar el significadoel cuadrado que hereda del rectángulocomposición
La cuarta fila es la señal más clara: si una subclase necesita que un método de la base tire un error de «no soportado», la jerarquía está modelando parecido de datos y no sustitución.

Cierre

Herencia define qué puede sustituir a qué; composición define quién colabora con quién; polimorfismo permite depender del contrato y no de la implementación. Separar esos conceptos evita jerarquías creadas sólo para reutilizar unas líneas.

Autoevaluación

¿Lo entendiste?

¿Cuál es la pregunta que justifica usar herencia?
Una subclase necesita prohibir una operación normal de la base. ¿Qué indica eso?
¿Qué relación expresa la composición?
Correo y mensajería cumplen el mismo contrato de notificación. ¿Necesitan una clase base común?