Puoi spiegare il principio di sostituzione di Liskov con un buon esempio in C #? [chiuso]


92

Puoi spiegare il principio di sostituzione di Liskov (la "L" di SOLID) con un buon esempio in C # che copre tutti gli aspetti del principio in modo semplificato? Se è davvero possibile.


9
Ecco un modo semplificato di pensarci in poche parole: se seguo LSP, posso sostituire qualsiasi oggetto nel mio codice con un oggetto Mock e il nulla nel codice chiamante dovrebbe essere regolato o modificato per tenere conto della sostituzione. LSP è un supporto fondamentale per il pattern Test by Mock.
kmote

Ci sono altri esempi di conformità e violazioni in questa risposta
StuartLC

Risposte:


128

(Questa risposta è stata riscritta 2013-05-13, leggi la discussione in fondo ai commenti)

LSP riguarda il rispetto del contratto della classe base.

Ad esempio, non puoi lanciare nuove eccezioni nelle sottoclassi poiché quello che utilizza la classe base non se lo aspetterebbe. Lo stesso vale se la classe base genera ArgumentNullExceptionse manca un argomento e la sottoclasse consente che l'argomento sia nullo, anche una violazione LSP.

Ecco un esempio di una struttura di classe che 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; } }
}

E il codice di chiamata

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

Come puoi vedere, ci sono due esempi di anatre. Un'anatra biologica e un'anatra elettrica. La papera elettrica può nuotare solo se è accesa. Ciò infrange il principio LSP poiché deve essere attivato per poter nuotare poiché il IsSwimming(che fa anche parte del contratto) non sarà impostato come nella classe base.

Ovviamente puoi risolverlo facendo qualcosa del genere

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

Ma ciò infrangerebbe il principio di apertura / chiusura e deve essere implementato ovunque (e quindi genera ancora codice instabile).

La soluzione corretta sarebbe quella di accendere automaticamente la papera nel Swimmetodo e in questo modo fare in modo che la papera elettrica si comporti esattamente come definito IDuckdall'interfaccia

Aggiornare

Qualcuno ha aggiunto un commento e lo ha rimosso. Aveva un punto valido che vorrei affrontare:

La soluzione con l'attivazione del duck all'interno del Swimmetodo può avere effetti collaterali quando si lavora con l'implementazione effettiva ( ElectricDuck). Ma questo può essere risolto utilizzando un'implementazione dell'interfaccia esplicita . imho è più probabile che tu abbia problemi NON accendendolo Swimpoiché ci si aspetta che nuoti quando usi l' IDuckinterfaccia

Aggiorna 2

Riformulato alcune parti per renderlo più chiaro.


1
@jgauffin: l'esempio è semplice e chiaro. Ma la soluzione che proponi, prima: infrange il principio di apertura-chiusura e non si adatta alla definizione di zio Bob (vedi la parte conclusiva del suo articolo) che scrive: "Il principio di sostituzione di Liskov (AKA Design by Contract) è una caratteristica importante di tutti i programmi conformi al principio Open-Closed. " vedi: objectmentor.com/resources/articles/lsp.pdf
pencilCake

1
Non vedo come la soluzione rompa Aperto / Chiuso. Leggi di nuovo la mia risposta se ti riferisci alla if duck is ElectricDuckparte. Giovedì scorso ho tenuto un seminario su SOLID :)
jgauffin

Non proprio sull'argomento, ma potresti cambiare l'esempio in modo da non eseguire il controllo del tipo due volte? Molti sviluppatori non sono a conoscenza della asparola chiave, il che in realtà li salva da un sacco di controllo del tipo. Sto pensando qualcosa di simile al seguente:if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
Siewers

3
@jgauffin - Sono leggermente confuso dall'esempio. Ho pensato che il principio di sostituzione di Liskov sarebbe ancora valido in questo caso perché Duck ed ElectricDuck derivano entrambi da IDuck e puoi mettere un ElectricDuck o un Duck ovunque venga utilizzato IDuck. Se ElectricDuck deve accendere prima che l'anatra possa nuotare, non è responsabilità dell'ElectricDuck o di un codice che istanzia ElectricDuck e quindi imposta la proprietà IsTurnedOn su true. Se questo viola LSP, sembra che LSV sarebbe molto difficile da aderire poiché tutte le interfacce conterrebbero una logica diversa per i suoi metodi.
Xaisoft

1
@MystereMan: imho LSP è incentrato sulla correttezza comportamentale. Con l'esempio rettangolo / quadrato si ottiene l'effetto collaterale dell'altra proprietà impostata. Con l'anatra si ottiene l'effetto collaterale di non nuotare. 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 approccio pratico

Ovunque cerco esempi C # di LSP, le persone hanno usato classi e interfacce immaginarie. Ecco l'implementazione pratica di LSP che ho implementato in uno dei nostri sistemi.

Scenario: supponiamo di avere 3 database (clienti ipotecari, clienti con conti correnti e clienti con conti di risparmio) che forniscono i dati dei clienti e abbiamo bisogno dei dettagli del cliente per il cognome del cliente specificato. Ora possiamo ottenere più di 1 dettaglio del cliente da quei 3 database rispetto al cognome.

Implementazione:

STRATO DEL MODELLO DI BUSINESS:

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

LIVELLO DI ACCESSO AI DATI:

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

L'interfaccia sopra è implementata dalla classe astratta

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
    }
}

Questa classe astratta ha un metodo comune "GetDetails" per tutti e 3 i database che viene esteso da ciascuna delle classi di database come mostrato di seguito

ACCESSO AI DATI DEL CLIENTE MUTUO:

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

ACCESSO AI DATI DEL CLIENTE DELL'ACCOUNT CORRENTE:

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

ACCOUNT DI RISPARMIO ACCESSO AI DATI DEL CLIENTE:

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

Una volta impostate queste 3 classi di accesso ai dati, ora attiriamo la nostra attenzione sul client. Nel livello aziendale abbiamo la classe CustomerServiceManager che restituisce i dettagli del cliente ai suoi clienti.

STRATO DI AFFARI:

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;
   }
}

Non ho mostrato l'inserimento delle dipendenze per mantenerlo semplice dato che si sta già complicando ora.

Ora, se abbiamo un nuovo database dei dettagli dei clienti, possiamo semplicemente aggiungere una nuova classe che estende BaseDataAccess e fornisce il suo oggetto database.

Ovviamente abbiamo bisogno di stored procedure identiche in tutti i database partecipanti.

Infine, il client per CustomerServiceManager classe chiamerà solo il metodo GetCustomerDetails, passerà il lastName e non dovrebbe preoccuparsi di come e da dove provengono i dati.

Spero che questo ti dia un approccio pratico per comprendere LSP.


3
Come può questo esempio di LSP?
somegeek

1
Non vedo l'esempio LSP neanche in questo ... Perché ha così tanti voti positivi?
StaNov

1
@RoshanGhangare IDataAccess ha 3 implementazioni concrete che possono essere sostituite nel livello aziendale.
Yawar Murtaza

1
@YawarMurtaza qualunque sia l'esempio che hai citato è una tipica implementazione del modello di strategia, tutto qui. Puoi per favore chiarire dove si sta rompendo LSP e come risolvi quella violazione di LSP
Yogesh

0

Ecco il codice per applicare il principio sostitutivo di 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 afferma: "Le classi derivate dovrebbero essere sostituibili con le loro classi di base (o interfacce)" e "I metodi che utilizzano riferimenti a classi di base (o interfacce) devono essere in grado di utilizzare metodi delle classi derivate senza saperlo o conoscerne i dettagli . "

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.