Können Sie das Liskov-Substitutionsprinzip anhand eines guten C # -Beispiels erklären? [geschlossen]


91

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.


9
Hier ist eine vereinfachte Sichtweise: Wenn ich LSP folge, kann ich jedes Objekt in meinem Code durch ein Mock-Objekt ersetzen, und das Nichts im aufrufenden Code müsste angepasst oder geändert werden, um die Substitution zu berücksichtigen. LSP ist eine grundlegende Unterstützung für das Test by Mock-Muster.
kmote

Es gibt einige weitere Beispiele für Konformität und Verstöße in dieser Antwort
StuartLC

Antworten:


128

(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, ArgumentNullExceptionwenn 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 SwimMethode automatisch einzuschalten und dadurch die elektrische Ente genau so zu verhalten, wie sie von der IDuckSchnittstelle 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 SwimMethode 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, Swimda erwartet wird, dass es schwimmt, wenn Sie die IDuckSchnittstelle verwenden

Update 2

Einige Teile wurden umformuliert, um es klarer zu machen.


1
@jgauffin: Beispiel ist einfach und klar. Aber die Lösung, die Sie zuerst vorschlagen: bricht das Open-Closed-Prinzip und passt nicht zu Onkel Bobs Definition (siehe den abschließenden Teil seines Artikels), in der es heißt: "Das Liskov-Substitutionsprinzip (AKA Design by Contract) ist ein wichtiges Merkmal aller Programme, die dem Open-Closed-Prinzip entsprechen. " Siehe: objectmentor.com/resources/articles/lsp.pdf
Bleistiftkuchen

1
Ich sehe nicht, wie die Lösung offen / geschlossen bricht. Lesen Sie meine Antwort noch einmal, wenn Sie sich auf das if duck is ElectricDuckTeil beziehen . Ich hatte letzten Donnerstag ein Seminar über SOLID :)
jgauffin

Nicht wirklich zum Thema, aber könnten Sie bitte Ihr Beispiel ändern, damit Sie die Typprüfung nicht zweimal durchführen? Viele Entwickler kennen das asSchlü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();
Siewers

3
@jgauffin - Ich bin etwas verwirrt von dem Beispiel. Ich dachte, das Liskov-Substitutionsprinzip wäre in diesem Fall immer noch gültig, da Duck und ElectricDuck beide von IDuck stammen und Sie eine ElectricDuck oder Duck überall dort einsetzen können, wo IDuck verwendet wird. Wenn ElectricDuck die Funktion einschalten muss, bevor die Ente schwimmen kann, liegt dies nicht in der Verantwortung der ElectricDuck oder eines Codes, der ElectricDuck instanziiert und dann die IsTurnedOn-Eigenschaft auf true setzt. Wenn dies gegen LSP verstößt, scheint es sehr schwierig zu sein, LSV einzuhalten, da alle Schnittstellen unterschiedliche Logik für seine Methoden enthalten würden.
Xaisoft

1
@MystereMan: Imho LSP dreht sich alles um Verhaltenskorrektheit. Mit dem Beispiel Rechteck / Quadrat erhalten Sie den Nebeneffekt der anderen Eigenschaft, die festgelegt wird. Mit der Ente bekommt man den Nebeneffekt, dass sie nicht schwimmt. 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 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 CustomerServiceManagerruft 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.


3
Wie kann dies ein Beispiel für LSP sein?
Somegeek

1
Ich sehe das LSP-Beispiel auch darin nicht ... Warum hat es so viele positive Stimmen?
StaNov

1
@RoshanGhangare IDataAccess verfügt über 3 konkrete Implementierungen, die in der Business-Schicht ersetzt werden können.
Yawar Murtaza

1
@YawarMurtaza Was auch immer Sie als Beispiel zitiert haben, ist eine typische Implementierung des Strategiemusters. Können Sie bitte klarstellen, wo es LSP bricht und wie Sie diese Verletzung von LSP lösen
Yogesh

0

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.