¿Puede explicar el principio de sustitución de Liskov (la 'L' de SOLID) con un buen ejemplo de C # que cubra todos los aspectos del principio de una manera simplificada? Si es realmente posible.
¿Puede explicar el principio de sustitución de Liskov (la 'L' de SOLID) con un buen ejemplo de C # que cubra todos los aspectos del principio de una manera simplificada? Si es realmente posible.
Respuestas:
(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 lo esperaría. Lo mismo ocurre si la clase base arroja 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 LSP, ya que debe estar encendido para poder nadar ya queIsSwimming
que (que también es parte del contrato) no se establecerá como en la clase base.
Por supuesto, puede 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 por lo tanto aún genera código inestable).
La solución adecuada sería encender automáticamente el pato en el Swim
método y, al hacerlo, hacer que el pato eléctrico se comporte exactamente como lo define la IDuck
interfaz.
Actualizar
Alguien agregó un comentario y lo eliminó. Tenía un punto válido que me gustaría abordar:
La solución con encender el pato dentro del Swim
método 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 . En mi humilde opinión, es más probable que tenga problemas al NO encenderlo, Swim
ya que se espera que nade al usar la IDuck
interfaz
Actualización 2
Reformulé algunas partes para que quede más claro.
if duck is ElectricDuck
parte. Tuve un seminario sobre SOLID el jueves pasado :)
as
palabra clave, lo que en realidad los salva de una gran cantidad de verificación de tipos. Estoy pensando en algo como lo siguiente:if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).
LSP un enfoque práctico
Dondequiera que busque ejemplos de C # de LSP, la gente ha 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 corrientes y Clientes de cuentas de ahorro) que proporcionan datos del cliente y necesitamos detalles del cliente para el apellido del cliente dado. Ahora podemos obtener más de 1 detalle de cliente de esas 3 bases de datos con el apellido dado.
Implementación:
CAPA DEL MODELO DE NEGOCIO:
public class Customer
{
// customer detail properties...
}
CAPA DE ACCESO A DATOS:
public interface IDataAccess
{
Customer GetDetails(string lastName);
}
La interfaz anterior es implementada por la clase abstracta
public abstract class BaseDataAccess : IDataAccess
{
/// <summary> Enterprise library data block Database object. </summary>
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 base de datos como se muestra a continuación
ACCESO A DATOS DEL CLIENTE HIPOTECARIO:
public class MortgageCustomerDataAccess : BaseDataAccess
{
public MortgageCustomerDataAccess(IDatabaseFactory factory)
{
this.Database = factory.GetMortgageCustomerDatabase();
}
}
ACCESO A LOS DATOS DEL CLIENTE DE LA CUENTA ACTUAL:
public class CurrentAccountCustomerDataAccess : BaseDataAccess
{
public CurrentAccountCustomerDataAccess(IDatabaseFactory factory)
{
this.Database = factory.GetCurrentAccountCustomerDatabase();
}
}
CUENTA DE AHORROS ACCESO A DATOS DEL CLIENTE:
public class SavingsAccountCustomerDataAccess : BaseDataAccess
{
public SavingsAccountCustomerDataAccess(IDatabaseFactory factory)
{
this.Database = factory.GetSavingsAccountCustomerDatabase();
}
}
Una vez establecidas estas 3 clases de acceso a datos, ahora llamamos nuestra atención al cliente. En la capa Business tenemos la clase CustomerServiceManager que devuelve los detalles del cliente a sus clientes.
CAPA DE NEGOCIOS:
public class CustomerServiceManager : ICustomerServiceManager, BaseServiceManager
{
public IEnumerable<Customer> GetCustomerDetails(string lastName)
{
IEnumerable<IDataAccess> dataAccess = new List<IDataAccess>()
{
new MortgageCustomerDataAccess(new DatabaseFactory()),
new CurrentAccountCustomerDataAccess(new DatabaseFactory()),
new SavingsAccountCustomerDataAccess(new DatabaseFactory())
};
IList<Customer> customers = new List<Customer>();
foreach (IDataAccess nextDataAccess in dataAccess)
{
Customer customerDetail = nextDataAccess.GetDetails(lastName);
customers.Add(customerDetail);
}
return customers;
}
}
No he mostrado la inyección de dependencia para que sea simple, ya que ahora se está complicando.
Ahora, si tenemos una nueva base de datos de detalles de clientes, podemos 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 de la CustomerServiceManager
clase solo llamará al método GetCustomerDetails, pasará el apellido y no debería preocuparse por cómo y de dónde provienen los datos.
Espero que esto le brinde un enfoque práctico para comprender LSP.
Aquí está el código para aplicar el principio de sustitución 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 establece: "Las clases derivadas deben ser sustituibles por sus clases base (o interfaces)" & "Los métodos que usan referencias a clases base (o interfaces) deben poder usar métodos de las clases derivadas sin saberlo ni conocer los detalles . "