¿Puedes explicar el Principio de Sustitución de Liskov con un buen ejemplo de C #?

¿Puedes explicar el Principio de Sustitución de Liskov (La ‘L’ de SOLID) con un buen ejemplo de C # que cubre todos los aspectos del principio de una manera simplificada? Si es realmente posible.

(Esta respuesta ha sido reescrita 2013-05-13, lea la discusión en la parte inferior de los comentarios)

LSP se trata de seguir el contrato de la clase base.

Por ejemplo, no puede lanzar nuevas excepciones en las subclases ya que el que usa la clase base no esperaría eso. Lo mismo ocurre si la clase base lanza ArgumentNullException si falta un argumento y la subclase permite que el argumento sea nulo, también una violación de LSP.

Aquí hay un ejemplo de una estructura de clase que viola LSP:

 public interface IDuck { void Swim(); // contract says that IsSwimming should be true if Swim has been called. bool IsSwimming { get; } } public class OrganicDuck : IDuck { public void Swim() { //do something to swim } bool IsSwimming { get { /* return if the duck is swimming */ } } } public class ElectricDuck : IDuck { bool _isSwimming; public void Swim() { if (!IsTurnedOn) return; _isSwimming = true; //swim logic } bool IsSwimming { get { return _isSwimming; } } } 

Y el código de llamada

 void MakeDuckSwim(IDuck duck) { duck.Swim(); } 

Como puede ver, hay dos ejemplos de patos. Un pato orgánico y un pato eléctrico. El pato eléctrico solo puede nadar si está encendido. Esto rompe el principio de LSP ya que debe IsSwimming para poder nadar ya que el IsSwimming (que también forma parte del contrato) no se establecerá como en la clase base.

Por supuesto, puedes resolverlo haciendo algo como esto

 void MakeDuckSwim(IDuck duck) { if (duck is ElectricDuck) ((ElectricDuck)duck).TurnOn(); duck.Swim(); } 

Pero eso rompería el principio Abierto / Cerrado y tiene que implementarse en todas partes (y aún así genera código inestable).

La solución adecuada sería activar automáticamente el pato en el método Swim y, al hacerlo, hacer que el pato eléctrico se comporte exactamente como lo define la interfaz IDuck

Actualizar

Alguien agregó un comentario y lo eliminó. Tenía un punto válido que me gustaría abordar:

La solución para encender el pato dentro del método Swim puede tener efectos secundarios cuando se trabaja con la implementación real ( ElectricDuck ). Pero eso se puede resolver utilizando una implementación de interfaz explícita . Es más probable que tengas problemas al NO encenderlo en Swim ya que se espera que nade cuando IDuck interfaz IDuck

Actualización 2

Reformuló algunas partes para hacerlo más claro.

LSP un enfoque práctico

Dondequiera que busco los ejemplos de C # de LSP, las personas han usado clases e interfaces imaginarias. Aquí está la implementación práctica de LSP que implementé en uno de nuestros sistemas.

Escenario: Supongamos que tenemos 3 bases de datos (Clientes hipotecarios, Clientes de cuentas streams y Clientes de cuentas de ahorro) que proporcionan datos de clientes y necesitamos los datos del cliente para el apellido del cliente. Ahora podemos obtener más de 1 detalle de cliente de esas 3 bases de datos contra el apellido dado.

Implementación:

MODELO DE NEGOCIO DE LA CAPA:

 public class Customer { // customer detail properties... } 

CAPA DE ACCESO A LOS DATOS:

 public interface IDataAccess { Customer GetDetails(string lastName); } 

La interfaz anterior está implementada por la clase abstracta

 public abstract class BaseDataAccess : IDataAccess { ///  Enterprise library data block Database object.  public Database Database; public Customer GetDetails(string lastName) { // use the database object to call the stored procedure to retrieve the customer details } } 

Esta clase abstracta tiene un método común “GetDetails” para las 3 bases de datos que se extiende por cada una de las clases de la base de datos como se muestra a continuación

ACCESO A LOS DATOS DEL CLIENTE DE LA HIPOTECA:

 public class MortgageCustomerDataAccess : BaseDataAccess { public MortgageCustomerDataAccess(IDatabaseFactory factory) { this.Database = factory.GetMortgageCustomerDatabase(); } } 

ACCESO A LOS DATOS DEL CLIENTE DE LA CUENTA CORRIENTE:

 public class CurrentAccountCustomerDataAccess : BaseDataAccess { public CurrentAccountCustomerDataAccess(IDatabaseFactory factory) { this.Database = factory.GetCurrentAccountCustomerDatabase(); } } 

CUENTA DE AHORROS ACCESO A LOS DATOS DEL CLIENTE:

 public class SavingsAccountCustomerDataAccess : BaseDataAccess { public SavingsAccountCustomerDataAccess(IDatabaseFactory factory) { this.Database = factory.GetSavingsAccountCustomerDatabase(); } } 

Una vez que se establecen estas 3 clases de acceso a datos, ahora llamamos nuestra atención sobre el cliente. En la capa Business tenemos la clase CustomerServiceManager que devuelve los detalles del cliente a sus clientes.

CAPA DE NEGOCIO:

 public class CustomerServiceManager : ICustomerServiceManager, BaseServiceManager { public IEnumerable GetCustomerDetails(string lastName) { IEnumerable dataAccess = new List() { new MortgageCustomerDataAccess(new DatabaseFactory()), new CurrentAccountCustomerDataAccess(new DatabaseFactory()), new SavingsAccountCustomerDataAccess(new DatabaseFactory()) }; IList customers = new List(); foreach (IDataAccess nextDataAccess in dataAccess) { Customer customerDetail = nextDataAccess.GetDetails(lastName); customers.Add(customerDetail); } return customers; } } 

No he mostrado la dependency injection para mantenerlo simple ya que ahora se está complicando.

Ahora, si tenemos una nueva base de datos de detalles del cliente, podemos simplemente agregar una nueva clase que amplíe BaseDataAccess y proporcione su objeto de base de datos.

Por supuesto, necesitamos procedimientos almacenados idénticos en todas las bases de datos participantes.

Por último, el cliente para la clase CustomerServiceManager solo llamará al método GetCustomerDetails, pasará el apellido y no le importará cómo y dónde provienen los datos.

Espero que esto te brinde un enfoque práctico para entender LSP.

Aquí está el código para aplicar el Principio Sustituto de Liskov.

 public abstract class Fruit { public abstract string GetColor(); } public class Orange : Fruit { public override string GetColor() { return "Orange Color"; } } public class Apple : Fruit { public override string GetColor() { return "Red color"; } } class Program { static void Main(string[] args) { Fruit fruit = new Orange(); Console.WriteLine(fruit.GetColor()); fruit = new Apple(); Console.WriteLine(fruit.GetColor()); } } 

LSV afirma: “Las clases derivadas deben ser sustituibles por sus clases base (o interfaces)” & “Los métodos que utilizan referencias a clases base (o interfaces) deben poder utilizar métodos de las clases derivadas sin saberlo o sin conocer los detalles ”