Können Sie das Liskov-Substitutionsprinzip (Das 'L' von SOLID) anhand eines guten C # -Beispiels erklären, das alle Aspekte des Prinzips auf vereinfachte Weise abdeckt? Wenn es wirklich möglich ist.
Können Sie das Liskov-Substitutionsprinzip (Das 'L' von SOLID) anhand eines guten C # -Beispiels erklären, das alle Aspekte des Prinzips auf vereinfachte Weise abdeckt? Wenn es wirklich möglich ist.
Antworten:
(Diese Antwort wurde am 13.05.2013 umgeschrieben. Lesen Sie die Diskussion unten in den Kommentaren.)
Bei LSP geht es darum, dem Vertrag der Basisklasse zu folgen.
Sie können beispielsweise keine neuen Ausnahmen in den Unterklassen auslösen, da diejenige, die die Basisklasse verwendet, dies nicht erwarten würde. Gleiches gilt, wenn die Basisklasse auslöst, ArgumentNullException
wenn ein Argument fehlt und die Unterklasse zulässt, dass das Argument null ist, was ebenfalls eine LSP-Verletzung darstellt.
Hier ist ein Beispiel für eine Klassenstruktur, die gegen LSP verstößt:
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; } }
}
Und der aufrufende Code
void MakeDuckSwim(IDuck duck)
{
duck.Swim();
}
Wie Sie sehen können, gibt es zwei Beispiele für Enten. Eine Bio-Ente und eine elektrische Ente. Die elektrische Ente kann nur schwimmen, wenn sie eingeschaltet ist. Dies verstößt gegen das LSP-Prinzip, da es eingeschaltet sein muss, um als schwimmen zu könnenIsSwimming
(was ebenfalls Bestandteil des Vertrags ist) nicht wie in der Basisklasse festgelegt wird.
Sie können es natürlich lösen, indem Sie so etwas tun
void MakeDuckSwim(IDuck duck)
{
if (duck is ElectricDuck)
((ElectricDuck)duck).TurnOn();
duck.Swim();
}
Dies würde jedoch das Open / Closed-Prinzip brechen und muss überall implementiert werden (und generiert daher immer noch instabilen Code).
Die richtige Lösung wäre, die Ente in der Swim
Methode automatisch einzuschalten und dadurch die elektrische Ente genau so zu verhalten, wie sie von der IDuck
Schnittstelle definiert wird
Aktualisieren
Jemand hat einen Kommentar hinzugefügt und ihn entfernt. Es gab einen gültigen Punkt, den ich ansprechen möchte:
Die Lösung mit dem Einschalten der Ente innerhalb der Swim
Methode kann Nebenwirkungen haben, wenn mit der tatsächlichen Implementierung gearbeitet wird ( ElectricDuck
). Dies kann jedoch mithilfe einer expliziten Schnittstellenimplementierung gelöst werden . Imho ist es wahrscheinlicher, dass Sie Probleme bekommen, wenn Sie es NICHT einschalten, Swim
da erwartet wird, dass es schwimmt, wenn Sie die IDuck
Schnittstelle verwenden
Update 2
Einige Teile wurden umformuliert, um es klarer zu machen.
if duck is ElectricDuck
Teil beziehen . Ich hatte letzten Donnerstag ein Seminar über SOLID :)
as
Schlüsselwort nicht, was sie vor vielen Typprüfungen bewahrt. Ich denke so etwas wie das Folgende: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 ein praktischer Ansatz
Überall, wo ich nach LSPs C # -Beispielen suche, haben Leute imaginäre Klassen und Schnittstellen verwendet. Hier ist die praktische Implementierung von LSP, die ich in einem unserer Systeme implementiert habe.
Szenario: Angenommen, wir haben 3 Datenbanken (Hypothekenkunden, Girokontokunden und Sparkontokunden), die Kundendaten bereitstellen, und wir benötigen Kundendaten für den Nachnamen des Kunden. Jetzt erhalten wir möglicherweise mehr als 1 Kundendetail aus diesen 3 Datenbanken gegen den angegebenen Nachnamen.
Implementierung:
GESCHÄFTSMODELLSCHICHT:
public class Customer
{
// customer detail properties...
}
Datenzugriffsschicht:
public interface IDataAccess
{
Customer GetDetails(string lastName);
}
Die obige Schnittstelle wird von der abstrakten Klasse implementiert
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
}
}
Diese abstrakte Klasse hat eine gemeinsame Methode "GetDetails" für alle 3 Datenbanken, die wie unten gezeigt um jede der Datenbankklassen erweitert wird
MORTGAGE KUNDENDATENZUGRIFF:
public class MortgageCustomerDataAccess : BaseDataAccess
{
public MortgageCustomerDataAccess(IDatabaseFactory factory)
{
this.Database = factory.GetMortgageCustomerDatabase();
}
}
AKTUELLER KONTO KUNDENDATENZUGRIFF:
public class CurrentAccountCustomerDataAccess : BaseDataAccess
{
public CurrentAccountCustomerDataAccess(IDatabaseFactory factory)
{
this.Database = factory.GetCurrentAccountCustomerDatabase();
}
}
SAVINGS ACCOUNT KUNDENDATENZUGRIFF:
public class SavingsAccountCustomerDataAccess : BaseDataAccess
{
public SavingsAccountCustomerDataAccess(IDatabaseFactory factory)
{
this.Database = factory.GetSavingsAccountCustomerDatabase();
}
}
Sobald diese 3 Datenzugriffsklassen festgelegt sind, lenken wir unsere Aufmerksamkeit auf den Client. In der Business-Schicht haben wir die CustomerServiceManager-Klasse, die die Kundendaten an ihre Kunden zurückgibt.
Geschäftsschicht:
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;
}
}
Ich habe die Abhängigkeitsinjektion nicht gezeigt, um sie einfach zu halten, da sie jetzt bereits kompliziert wird.
Wenn wir jetzt eine neue Kundendatenbank haben, können wir einfach eine neue Klasse hinzufügen, die BaseDataAccess erweitert und das Datenbankobjekt bereitstellt.
Natürlich benötigen wir in allen teilnehmenden Datenbanken identische gespeicherte Prozeduren.
Schließlich CustomerServiceManager
ruft der Client für die Klasse nur die GetCustomerDetails-Methode auf, übergibt den Nachnamen und sollte sich nicht darum kümmern, wie und woher die Daten stammen.
Ich hoffe, dies gibt Ihnen einen praktischen Ansatz zum Verständnis von LSP.
Hier ist der Code für die Anwendung des Liskov-Ersatzprinzips.
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-Zustände: "Abgeleitete Klassen sollten ihre Basisklassen (oder Schnittstellen) ersetzen können" & "Methoden, die Verweise auf Basisklassen (oder Schnittstellen) verwenden, müssen in der Lage sein, Methoden der abgeleiteten Klassen zu verwenden, ohne davon zu wissen oder die Details zu kennen . "