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.
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.
Risposte:
(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 ArgumentNullException
se 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 Swim
metodo e in questo modo fare in modo che la papera elettrica si comporti esattamente come definito IDuck
dall'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 Swim
metodo 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 Swim
poiché ci si aspetta che nuoti quando usi l' IDuck
interfaccia
Aggiorna 2
Riformulato alcune parti per renderlo più chiaro.
if duck is ElectricDuck
parte. Giovedì scorso ho tenuto un seminario su SOLID :)
as
parola 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();
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 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.
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 . "