Comprender la programación orientada a objetos de C#
La herencia y las interfaces son parte integrante de la programación orientada a objetos (POO). Tim Corey, en su vídeo "Inheritance vs Interfaces in C#: Object Oriented Programming", ofrece una explicación detallada de cuándo utilizar la herencia y cuándo optar por las interfaces.
Este artículo sirve como guía completa del vídeo de Tim Corey. Desglosa los conceptos clave, los ejemplos y las explicaciones de código que se ofrecen en el vídeo, destacando las diferencias entre herencia e interfaces y cuándo utilizar cada una de ellas.
Introducción
Tim (0:00) comienza destacando la importancia de distinguir entre herencia e interfaces. Hace hincapié en la necesidad de entender cuándo utilizar cada concepto para lograr los mejores resultados. Su objetivo es demostrarlo mediante ejemplos, empezando por el uso incorrecto de la herencia única y corrigiéndolo después.
Creación del proyecto
En (1:08), Tim crea una sencilla aplicación de consola utilizando .NET 5. Nombra al proyecto "OODemo". Nombra al proyecto "OODemoApp" y explica que el objetivo principal es demostrar los conceptos más que crear código listo para producción.
Entender la herencia
Tim (1:55) profundiza en los conceptos básicos de la herencia. Define la herencia como un mecanismo por el cual las propiedades y métodos de una clase base son heredados por una clase derivada. Subraya que la herencia no debe utilizarse únicamente para reutilizar y compartir código, sino para establecer una relación lógica "es-una".
Puntos clave:
- Is-a Relationship: Asegura que la clase derivada es un tipo de la clase base.
- Lógica común: Las clases heredadas deben compartir una lógica común, no solo propiedades o firmas de métodos.
Clase de ejemplo: Coche de alquiler
Tim en (7:52) crea una clase RentalCar para ilustrar el concepto fundamental de herencia. Esta clase representa un coche de alquiler en una agencia de alquiler de Miami, Florida.
public class RentalCar
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public int NumberOfPassengers { get; set; }
public void StartEngine()
{
Console.WriteLine("Turn key to ignition setting");
Console.WriteLine("Turn key to on");
}
public void StopEngine()
{
Console.WriteLine("Turn key to off");
}
}
public class RentalCar
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public int NumberOfPassengers { get; set; }
public void StartEngine()
{
Console.WriteLine("Turn key to ignition setting");
Console.WriteLine("Turn key to on");
}
public void StopEngine()
{
Console.WriteLine("Turn key to off");
}
}
Demostración de las trampas de la herencia incorrecta
Tim (10:15) explica cómo el uso incorrecto de la herencia puede dar lugar a problemas. Destaca que si la herencia se utiliza mal, puede dar lugar a un código difícil de gestionar y ampliar. Aconseja no utilizar la herencia solo para compartir código.
Agregación de Enums para tipos de coches y camiones
Tim en (10:45) añade una enumeración para los tipos de coche. Crea un nuevo archivo de clase llamado Enums.cs para mantener todas las enumeraciones en un solo lugar. Este enum ayudará a diferenciar entre los distintos estilos de vehículos.
// Enums.cs
public enum CarType
{
Hatchback,
Sedan,
Compact
}
// Enums.cs
public enum CarType
{
Hatchback,
Sedan,
Compact
}
A continuación, añade una propiedad a la clase RentalCar para especificar el tipo de coche.
public class RentalCar : RentalVehicle
{
public CarType Style { get; set; }
// Other properties and methods
}
public class RentalCar : RentalVehicle
{
public CarType Style { get; set; }
// Other properties and methods
}
Ampliación para incluir camiones
Como explica Tim en (12:27), la agencia de alquiler decide añadir camiones a su flota, lo que introduce nuevos requisitos. Crea una clase RentalTruck que herede de la clase padre RentalVehicle.
public class RentalTruck : RentalVehicle
{
public TruckType Style { get; set; }
// Other properties and methods
}
public class RentalTruck : RentalVehicle
{
public TruckType Style { get; set; }
// Other properties and methods
}
A continuación, define un nuevo enum para los tipos de camión.
// Enums.cs
public enum TruckType
{
ShortBed,
LongBed
}
// Enums.cs
public enum TruckType
{
ShortBed,
LongBed
}
Manejo de diferentes tipos de propiedades
Tim (15:28) subraya que el hecho de que dos propiedades compartan el mismo nombre no significa que sean iguales. Lo ilustra con la propiedad Style, que podría significar diferentes enums (CarType para coches y TruckType para camiones).
Presentación de barcos a la flota
La agencia de alquiler amplía su flota para incluir barcos. Tim demuestra cómo manejar esto mediante la creación de una clase RentalBoat. Inicialmente, parece manejable, ya que los barcos pueden compartir algunas propiedades con los coches y los camiones.
public class RentalBoat : RentalVehicle
{
// Properties and methods specific to boats
}
public class RentalBoat : RentalVehicle
{
// Properties and methods specific to boats
}
Tratando con veleros
La introducción de los veleros presenta un reto, ya que los veleros no tienen motor. Tim (19:57) ilustra las limitaciones de la herencia en este caso.
public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}
public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}
Resolver el problema con interfaces
Tim sugiere hacer que los métodos StartEngine y StopEngine sean virtuales en la clase base para permitir su anulación en clases derivadas que no utilicen estos métodos.
public abstract class RentalVehicle
{
// Common properties
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}
public abstract class RentalVehicle
{
// Common properties
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}
Manejo de métodos que no se deben llamar
Tim explica en (21:56) los peligros de tener métodos en clases heredadas que no deben llamarse. Para el ejemplo de la clase RentalSailboat, que no tiene motor, hereda los métodos StartEngine y StopEngine de la clase RentalVehicle. Esta situación puede dar lugar a problemas si estos métodos se llaman involuntariamente, ya que deben lanzar excepciones para indicar que no son aplicables.
public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}
public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}
Reconociendo las limitaciones de la herencia
Tim en (24:06) hace hincapié en cómo la herencia puede conducir a una base de código enrevesada y desordenada cuando deja de tener sentido lógico. Por ejemplo, un velero no debe tratarse como un RentalVehicle con motor. Esto demuestra las limitaciones de la herencia y la necesidad de un mejor enfoque de diseño.
Hacia un mejor diseño con interfaces
Para resolver estos problemas, Tim sugiere un mejor diseño mediante interfaces. Comienza creando un nuevo proyecto de aplicación de consola llamado "BetterOODemo" para demostrar el enfoque mejorado.
Definición de la interfaz
Tim presenta la interfaz IRental para encapsular propiedades comunes a todos los alquileres.
public interface IRental
{
int RentalId { get; set; }
string CurrentRenter { get; set; }
decimal PricePerDay { get; set; }
}
public interface IRental
{
int RentalId { get; set; }
string CurrentRenter { get; set; }
decimal PricePerDay { get; set; }
}
Creación de la clase base para vehículos terrestres
A continuación, Tim crea una clase base para vehículos terrestres, separando el concepto de alquiler de vehículos del vehículo en sí.
public abstract class LandVehicle
{
public int NumberOfPassengers { get; set; }
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}
public abstract class LandVehicle
{
public int NumberOfPassengers { get; set; }
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}
Al cambiar el nombre de la clase base del vehículo a LandVehicle, Tim se asegura de que solo los vehículos apropiados hereden los métodos relacionados con el motor.
Implementación de clases de coches y camiones
Tim crea las clases Car y Truck que heredan de LandVehicle e implementan la interfaz IRental.
public class Car : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public CarType Style { get; set; }
}
public class Truck : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public TruckType Style { get; set; }
}
public class Car : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public CarType Style { get; set; }
}
public class Truck : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public TruckType Style { get; set; }
}
Este diseño mantiene una clara separación de intereses, garantizando que cada clase solo tenga propiedades y métodos relevantes para su tipo.
Evitar la duplicación de código
Tim (31:41) habla de la importancia de evitar la duplicación innecesaria de código. Explica que, aunque la interfaz IRental requiere las mismas propiedades en varias clases, esto no se considera una violación del principio DRY (Don't Repeat Yourself) porque no se duplica ninguna lógica, solo las declaraciones de propiedades.
Implementación de la clase de velero de alquiler
Tim en (35:09) explica cómo manejar la clase RentalSailboat por separado implementando la interfaz IRental, en lugar de heredar de LandVehicle. Este enfoque ayuda a evitar los escollos asociados a una herencia inapropiada.
public class Sailboat : IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
// Additional properties and methods specific to sailboats
}
public class Sailboat : IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
// Additional properties and methods specific to sailboats
}
Gestión de diferentes alquileres
Tim crea una lista para gestionar diferentes tipos de alquileres, utilizando la interfaz IRental para almacenar varios tipos de alquileres, incluidos camiones, veleros y coches.
List<IRental> rentals = new List<IRental>
{
new Truck { CurrentRenter = "Truck Renter" },
new Sailboat { CurrentRenter = "Sailboat Renter" },
new Car { CurrentRenter = "Car Renter" }
};
List<IRental> rentals = new List<IRental>
{
new Truck { CurrentRenter = "Truck Renter" },
new Sailboat { CurrentRenter = "Sailboat Renter" },
new Car { CurrentRenter = "Car Renter" }
};
Este diseño permite recorrer los alquileres y acceder a propiedades comunes como CurrentRenter, PricePerDay y RentalId.
foreach (var rental in rentals)
{
Console.WriteLine($"Renter: {rental.CurrentRenter}, Price Per Day: {rental.PricePerDay}");
}
foreach (var rental in rentals)
{
Console.WriteLine($"Renter: {rental.CurrentRenter}, Price Per Day: {rental.PricePerDay}");
}
Casting a tipos específicos
Para acceder a propiedades y métodos específicos de diferentes tipos de alquiler, Tim demuestra cómo usar la palabra clave is para verificar y convertir objetos a sus respectivos tipos.
foreach (var rental in rentals)
{
if (rental is Truck truck)
{
Console.WriteLine($"Truck Style: {truck.Style}, Passengers: {truck.NumberOfPassengers}");
}
else if (rental is Sailboat sailboat)
{
// Access sailboat-specific properties
}
else if (rental is Car car)
{
// Access car-specific properties
}
}
foreach (var rental in rentals)
{
if (rental is Truck truck)
{
Console.WriteLine($"Truck Style: {truck.Style}, Passengers: {truck.NumberOfPassengers}");
}
else if (rental is Sailboat sailboat)
{
// Access sailboat-specific properties
}
else if (rental is Car car)
{
// Access car-specific properties
}
}
Flexibilidad con interfaces
Tim hace hincapié en que el uso de interfaces proporciona flexibilidad para futuros cambios. Por ejemplo, añadir nuevos tipos de alquileres, como tanques o televisores, no alteraría la estructura existente.
Evitar el uso excesivo de la herencia
Tim desaconseja el uso excesivo de la herencia para compartir código, ya que puede dar lugar a una base de código enrevesada e inflexible. En su lugar, recomienda aprovechar las interfaces y la composición para lograr los resultados deseados sin estirar la relación "is-a" más allá de sus límites lógicos.
Conclusión
La explicación de Tim Corey sobre la herencia y las interfaces en la programación orientada a objetos ofrece un camino claro para crear código flexible y fácil de mantener. Al mostrar los errores más comunes y ofrecer un diseño refinado con interfaces, garantiza que los desarrolladores puedan tomar decisiones informadas sobre la estructuración eficaz de sus aplicaciones.
Para profundizar en estos conceptos y verlos en acción, vea el vídeo completo de Tim. Su canal es una mina de oro de tutoriales de programación. ¡No se lo pierda!
