¿Puede explicar el principio de sustitución de Liskov con un buen ejemplo de C #? [cerrado]


92

¿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.


9
Aquí hay una forma simplificada de pensarlo en pocas palabras: si sigo LSP, puedo reemplazar cualquier objeto en mi código con un objeto Mock, y la nada en el código de llamada tendría que ajustarse o cambiarse para dar cuenta de la sustitución. LSP es un soporte fundamental para el patrón Test by Mock.
kmote

Hay algunos ejemplos más de conformidad y violaciones en esta respuesta
StuartLC

Respuestas:


128

(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 ArgumentNullExceptionsi 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 Swimmétodo y, al hacerlo, hacer que el pato eléctrico se comporte exactamente como lo define la IDuckinterfaz.

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 Swimmé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, Swimya que se espera que nade al usar la IDuckinterfaz

Actualización 2

Reformulé algunas partes para que quede más claro.


1
@jgauffin: El ejemplo es simple y claro. Pero la solución que propones, primero: rompe el principio abierto-cerrado y no se ajusta a la definición del tío Bob (ver la parte de conclusión de su artículo) que escribe: "El principio de sustitución de Liskov (AKA Design by Contract) es una característica importante de todos los programas que se ajustan al principio Abierto-Cerrado ". ver: objectmentor.com/resources/articles/lsp.pdf
pencilCake

1
No veo cómo la solución se abre / cierra. Lea mi respuesta nuevamente si se refiere a la if duck is ElectricDuckparte. Tuve un seminario sobre SOLID el jueves pasado :)
jgauffin

No estoy realmente en el tema, pero ¿podría cambiar su ejemplo para no hacer la verificación de tipo dos veces? Muchos desarrolladores no son conscientes de la aspalabra 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();
Siewers

3
@jgauffin: el ejemplo me confunde un poco. Pensé que el principio de sustitución de Liskov todavía sería válido en este caso porque Duck y ElectricDuck derivan de IDuck y puedes poner un ElectricDuck o Duck en cualquier lugar donde se use IDuck. Si ElectricDuck tiene que encender el antes de que el pato pueda nadar, ¿no es esa la responsabilidad de ElectricDuck o algún código que instancia ElectricDuck y luego establece la propiedad IsTurnedOn en true? Si esto viola LSP, parece que LSV sería muy difícil de cumplir ya que todas las interfaces contendrían una lógica diferente para sus métodos.
Xaisoft

1
@MystereMan: en mi humilde opinión, LSP tiene que ver con la corrección del comportamiento. Con el ejemplo de rectángulo / cuadrado, obtiene el efecto secundario de la otra propiedad que se establece. Con el pato tienes el efecto secundario de no nadar. LSP: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).
jgauffin

8

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 CustomerServiceManagerclase 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.


3
¿Cómo puede esto ser un ejemplo de LSP?
somegeek

1
Tampoco veo el ejemplo de LSP en eso ... ¿Por qué tiene tantos votos a favor?
StaNov

1
@RoshanGhangare IDataAccess tiene 3 implementaciones concretas que se pueden sustituir en la capa empresarial.
Yawar Murtaza

1
@YawarMurtaza, cualquiera que sea su ejemplo que haya citado, es una implementación típica del patrón de estrategia, eso es todo. ¿Puede aclarar dónde está rompiendo LSP y cómo resuelve esa violación de LSP
Yogesh

0

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 . "

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.