Puoi spiegare il Principio di sostituzione di Liskov con un buon esempio di C #?

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

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

LSP sta per seguire il contratto della class base.

Ad esempio, non puoi lanciare nuove eccezioni nelle sottoclassi dato che quello che usa la class base non se lo aspetterebbe. Lo stesso vale se la class base genera ArgumentNullException se manca un argomento e la sottoclass consente all’argomento di essere nullo, anche una violazione LSP.

Ecco un esempio di una struttura di class 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 chiamante

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

Come puoi vedere, ci sono due esempi di anatre. Un’anatra organica e un’anatra elettrica. L’anatra elettrica può nuotare solo se è accesa. Questo rompe il principio LSP poiché deve essere triggersto per poter nuotare mentre l’ IsSwimming (che fa anche parte del contratto) non sarà impostato come nella class base.

Ovviamente puoi risolverlo facendo qualcosa di simile

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

Ma questo potrebbe infrangere il principio Open / Closed e deve essere implementato ovunque (e quindi genera ancora codice instabile).

La soluzione corretta sarebbe quella di accendere automaticamente l’anatra nel metodo Swim e facendo in modo che l’anatra elettrica si comporti esattamente come definito dall’interfaccia IDuck

Aggiornare

Qualcuno ha aggiunto un commento e lo ha rimosso. Aveva un punto valido a cui vorrei rivolgermi:

La soluzione con l’triggerszione dell’anatra all’interno del metodo Swim può avere effetti collaterali quando si lavora con l’implementazione effettiva ( ElectricDuck ). Ma ciò può essere risolto utilizzando un’implementazione esplicita dell’interfaccia . è più probabile che tu abbia problemi a NON accenderlo in Swim poiché è previsto che nuoterà durante l’ IDuck dell’interfaccia IDuck

Aggiornamento 2

Ha riformulato alcune parti per renderlo più chiaro.

LSP un approccio pratico

Ovunque cerco esempi di C # di LSP, le persone hanno utilizzato 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 di conti correnti e clienti di conti di risparmio) che forniscono i dati dei clienti e abbiamo bisogno dei dettagli del cliente per il cognome del cliente dato. Ora possiamo ottenere più di 1 dettaglio del cliente da quei 3 database contro il cognome dato.

Implementazione:

STRATO DI MODELLO DI BUSINESS:

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

STRATO DI ACCESSO DATI:

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

L’interfaccia sopra è implementata dalla class astratta

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

Questa class 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 AL DATO DEI CLIENTI MUTUI:

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

ACCOUNT CORRENTE ACCESSO AI CLIENTI DEL CLIENTE:

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

ACQUISIZIONE DATI CLIENTI RISPARMIO:

 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 cliente. Nel livello aziendale abbiamo la class CustomerServiceManager che restituisce i dettagli del cliente ai suoi clienti.

STRATO AZIENDALE:

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

Non ho mostrato l’iniezione di dipendenza per mantenerla semplice poiché è già complicata ora.

Ora, se disponiamo di un nuovo database dei dettagli dei clienti, possiamo semplicemente aggiungere una nuova class che estende BaseDataAccess e fornisce il suo object di database.

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

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

Spero che questo ti fornisca un approccio pratico per capire LSP.

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 per le loro classi di base (o interfacce)” & “I metodi che usano i riferimenti alle classi di base (o interfacce) devono essere in grado di utilizzare i metodi delle classi derivate senza conoscerne o conoscere i dettagli “.