Composición sobre herencia
Parte de la sección Programación Orientada a Objetos del Journey de C# de Coddy — lección 41 de 70.
La herencia crea un acoplamiento fuerte entre clases. Cuando Car hereda de Vehicle, cualquier cambio en Vehicle puede romper Car. La composición ofrece una alternativa más flexible: en lugar de heredar el comportamiento, una clase contiene objetos que proporcionan dicho comportamiento.
Considera un personaje de juego que puede moverse y atacar. Con la herencia, podrías crear una jerarquía compleja. Con la composición, simplemente le das al personaje los componentes que necesita:
public class MovementBehavior
{
public void Move() => Console.WriteLine("Moving...");
}
public class AttackBehavior
{
public void Attack() => Console.WriteLine("Attacking!");
}
public class Character
{
private MovementBehavior movement = new MovementBehavior();
private AttackBehavior attack = new AttackBehavior();
public void Move() => movement.Move();
public void Attack() => attack.Attack();
}La diferencia clave es la relación. La herencia representa "es-un" (un Perro es un Animal), mientras que la composición representa "tiene-un" (un Personaje tiene un comportamiento de movimiento). La composición te permite intercambiar comportamientos en tiempo de ejecución, combinar capacidades libremente y evitar el problema de la clase base frágil.
Usa la herencia cuando exista una relación jerárquica genuina. Elige la composición cuando necesites flexibilidad, cuando los comportamientos puedan compartirse entre clases no relacionadas o cuando quieras cambiar el comportamiento de forma dinámica.
Desafío
FácilVamos a construir un sistema de notificación que demuestre el poder de la composición sobre la herencia. En lugar de crear una jerarquía compleja de clases de notificación, compondrás un NotificationSender dándole componentes de comportamiento intercambiables.
Organizarás tu código en tres archivos:
Behaviors.cs: Crea dos clases de comportamiento en el espacio de nombresNotificationsque manejen diferentes aspectos del envío de notificaciones:FormattingBehavior- tiene un métodoFormat(string message)que devuelve el mensaje envuelto con[ALERT]al inicio y[END]al finalDeliveryBehavior- tiene un métodoDeliver(string channel, string message)que devuelve una cadena en el formatoSending via {channel}: {message}
NotificationSender.cs: Crea una claseNotificationSenderen el espacio de nombresNotificationsque utilice la composición para combinar comportamientos. Tu remitente debe:- Contener instancias tanto de
FormattingBehaviorcomo deDeliveryBehaviorcomo campos privados - Tener una propiedad
Channel(string) que almacene el nombre del canal de entrega - Tener un constructor que acepte el nombre del canal
- Tener un método
Send(string message)que primero formatee el mensaje usando el comportamiento de formateo, luego lo entregue usando el comportamiento de entrega y devuelva el resultado final
- Contener instancias tanto de
Program.cs: En tu archivo principal, crea unNotificationSendercon un canal proveniente de la entrada, luego envía un mensaje proveniente de la entrada. Imprime el resultado para mostrar cómo los comportamientos compuestos trabajan juntos.
Recibirás dos entradas:
- El nombre del canal de entrega (por ejemplo,
Email,SMS) - El mensaje a enviar
Imprime el resultado de llamar a Send() en tu remitente de notificaciones.
Por ejemplo, si las entradas son Email y Server is down, la salida debería ser:
Sending via Email: [ALERT] Server is down [END]Observa cómo el NotificationSender no hereda de nada; simplemente "tiene" comportamientos de formateo y entrega. ¡Este enfoque de composición significa que podrías intercambiar fácilmente cualquiera de los comportamientos sin cambiar la clase remitente, o reutilizar estos comportamientos en clases completamente diferentes!
Hoja de referencia
La composición es una alternativa a la herencia que crea código más flexible al hacer que una clase contenga objetos que proporcionan comportamiento, en lugar de heredar el comportamiento de una clase padre.
Herencia vs Composición:
- La herencia representa relaciones de tipo "es-un" (un Perro es un Animal)
- La composición representa relaciones de tipo "tiene-un" (un Personaje tiene un comportamiento de movimiento)
Beneficios de la Composición:
- Intercambiar comportamientos en tiempo de ejecución
- Combinar capacidades libremente
- Evitar el acoplamiento fuerte y los problemas de clase base frágil
- Compartir comportamientos entre clases no relacionadas
Ejemplo con componentes de comportamiento:
public class MovementBehavior
{
public void Move() => Console.WriteLine("Moving...");
}
public class AttackBehavior
{
public void Attack() => Console.WriteLine("Attacking!");
}
public class Character
{
private MovementBehavior movement = new MovementBehavior();
private AttackBehavior attack = new AttackBehavior();
public void Move() => movement.Move();
public void Attack() => attack.Attack();
}Cuándo usar cada enfoque:
- Usa la herencia para relaciones jerárquicas genuinas
- Elige la composición para obtener flexibilidad, comportamientos compartidos o cambios de comportamiento dinámicos
Pruébalo tú mismo
using System;
using Notifications;
class Program
{
public static void Main(string[] args)
{
// Leer entrada
string channel = Console.ReadLine();
string message = Console.ReadLine();
// TODO: Crear un NotificationSender con el canal
// TODO: Enviar el mensaje e imprimir el resultado
}
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Programación Orientada a Objetos
1Fundamentos de OOP
Archivos externosNamespaces y directivasIntro a clases y objetosLa palabra clave 'this'Métodos y parámetrosCampos vs PropiedadesConstructoresInicializadores de objetosResumen - Calculadora simple4Herencia
Sintaxis básica de herencia (:)La palabra clave 'base'Palabras clave Virtual y OverrideClases selladas (Sealed)La clase base 'object'Resumen: Jerarquía de empleados7Características avanzadas
Sobrecarga de operadoresIndexadores (this[])Sobrescritura de ToString()Métodos de extensiónResumen - Lista personalizada10Patrones de diseño - Parte 1
Introducción a los patrones de diseñoSingleton Thread-SafePatrón FactoryPatrón Observer (Eventos)Patrón Strategy2Propiedades y miembros estáticos
Propiedades autoimplementadasPropiedades de solo lectura y escrituraCampos y métodos estáticosClases estáticasMiembros con cuerpo de expresión5Polimorfismo e Interfaces
Polimorfismo: Compilación vs. EjecuciónInterfaz vs. Clase AbstractaInterfaces MúltiplesInterfaces ExplícitasUpcasting y DowncastingRepaso: Calculadora de Figuras8Conceptos avanzados de POO
Composición sobre herenciaGenéricos (clases y métodos)Delegados y eventosAtributos y ReflexiónIDisposable y la sentencia usingFundamentos de Inyección de dependencias11Patrones de diseño, parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón Composite3Arquitectura de clases
Datos de instancia vs. estáticosPalabras clave 'readonly' y 'const'Campos de respaldoResumen - Gestor de cuentas bancarias6Encapsulamiento
Modificadores de accesoPropiedades para el encapsulamientoImplementación de ocultamiento de datosPatrones de inmutabilidadRepaso - Registros de estudiantes