Czy możesz wyjaśnić zasadę podstawienia Liskova na dobrym przykładzie w języku C #? [Zamknięte]

93

Czy możesz wyjaśnić zasadę podstawienia Liskova („L” SOLID) dobrym przykładem w języku C # obejmującym wszystkie aspekty tej zasady w uproszczony sposób? Jeśli to naprawdę możliwe.

ołówekCake
źródło
9
Oto uproszczony sposób myślenia o tym w pigułce: Jeśli postępuję zgodnie z LSP, mogę zamienić dowolny obiekt w moim kodzie na obiekt Mock, a nic w kodzie wywołującym musiałoby zostać dostosowane lub zmienione, aby uwzględnić podstawienie. LSP jest podstawowym wsparciem dla wzorca Test by Mock.
kmote
W tej odpowiedzi
StuartLC

Odpowiedzi:

128

(Ta odpowiedź została przepisana 2013-05-13, przeczytaj dyskusję na dole komentarzy)

LSP polega na przestrzeganiu kontraktu klasy bazowej.

Nie możesz na przykład zgłaszać nowych wyjątków w podklasach, ponieważ osoba korzystająca z klasy bazowej nie spodziewałaby się tego. To samo dotyczy sytuacji, gdy klasa bazowa zgłasza, ArgumentNullExceptionjeśli brakuje argumentu, a podklasa zezwala na zerową wartość argumentu, co również stanowi naruszenie LSP.

Oto przykład struktury klas, która narusza 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; } }
}

I kod telefoniczny

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

Jak widać, są dwa przykłady kaczek. Jedna kaczka ekologiczna i jedna kaczka elektryczna. Kaczka elektryczna może pływać tylko wtedy, gdy jest włączona. Łamie to zasadę LSP, ponieważ musi być włączony, aby móc pływać, ponieważ IsSwimming(który również jest częścią kontraktu) nie zostanie ustawiony tak jak w klasie bazowej.

Możesz oczywiście rozwiązać ten problem, robiąc coś takiego

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

Ale to złamałoby zasadę Open / Closed i musi być wdrażane wszędzie (a zatem nadal generuje niestabilny kod).

Właściwym rozwiązaniem byłoby automatyczne włączenie kaczki w Swimmetodzie i dzięki temu sprawienie, by kaczka elektryczna zachowywała się dokładnie tak, jak zdefiniowano w IDuckinterfejsie

Aktualizacja

Ktoś dodał komentarz i usunął go. Miał ważny punkt, do którego chciałbym się odnieść:

Rozwiązanie z włączeniem kaczki wewnątrz Swimmetody może mieć skutki uboczne podczas pracy z rzeczywistą implementacją ( ElectricDuck). Ale można to rozwiązać za pomocą jawnej implementacji interfejsu . imho, bardziej prawdopodobne Swimjest, że wystąpią problemy, NIE włączając go, ponieważ oczekuje się, że będzie pływać podczas korzystania z IDuckinterfejsu

Zaktualizuj 2

Zmieniono niektóre części, aby było jaśniejsze.

jgauffin
źródło
1
@jgauffin: Przykład jest prosty i jasny. Ale rozwiązanie, które proponujesz, po pierwsze: łamie zasadę otwartego-zamkniętego i nie pasuje do definicji wuja Boba (patrz końcowa część jego artykułu), która pisze: „Zasada zastępowania Liskova (AKA Design by Contract) jest ważną cechą wszystkich programów, które są zgodne z zasadą Open-Closed. ” patrz: objectmentor.com/resources/articles/lsp.pdf
pencilCake
1
Nie widzę, jak rozwiązanie psuje Otwarte / Zamknięte. Przeczytaj moją odpowiedź ponownie, jeśli odnosisz się do tej if duck is ElectricDuckczęści. W zeszły czwartek miałem seminarium na temat SOLID :)
jgauffin
Niezupełnie na temat, ale czy mógłbyś zmienić swój przykład, aby nie sprawdzać typu dwa razy? Wielu programistów nie zna assłowa kluczowego, co w rzeczywistości chroni ich przed licznym sprawdzaniem typów. Myślę o czymś takim:if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
Siewers
3
@jgauffin - Jestem trochę zdezorientowany przykładem. Pomyślałem, że Zasada Zastępstwa Liskova będzie nadal obowiązywać w tym przypadku, ponieważ zarówno Duck, jak i ElectricDuck wywodzą się z IDuck i można umieścić ElectricDuck lub Duck wszędzie tam, gdzie jest używany IDuck. Jeśli ElectricDuck musi włączyć się, zanim kaczka będzie mogła pływać, czy nie jest to obowiązkiem ElectricDuck lub jakiegoś kodu uruchamiającego instancję ElectricDuck, a następnie ustawiającego właściwość IsTurnedOn na true. Jeśli to narusza LSP, wydaje się, że LSV byłoby bardzo trudne do przestrzegania, ponieważ wszystkie interfejsy miałyby inną logikę dla jego metod.
Xaisoft
1
@MystereMan: imho LSP polega na poprawności zachowania. W przykładzie prostokąta / kwadratu uzyskuje się efekt uboczny ustawiania innej właściwości. Z kaczką uzyskuje się efekt uboczny nie pływania. 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
9

LSP to praktyczne podejście

Wszędzie, gdzie szukam przykładów C # LSP, ludzie używali wyimaginowanych klas i interfejsów. Oto praktyczna implementacja LSP, którą zaimplementowałem w jednym z naszych systemów.

Scenariusz: załóżmy, że mamy 3 bazy danych (klienci hipoteczni, klienci rachunków bieżących i klienci kont oszczędnościowych), które zawierają dane klientów i potrzebujemy danych klienta dla nazwiska klienta. Teraz możemy uzyskać więcej niż 1 szczegół klienta z tych 3 baz danych dla podanego nazwiska.

Realizacja:

WARSTWA MODELU BIZNESOWEGO:

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

WARSTWA DOSTĘPU DO DANYCH:

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

Powyższy interfejs jest zaimplementowany przez klasę abstrakcyjną

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

Ta klasa abstrakcyjna ma wspólną metodę „GetDetails” dla wszystkich 3 baz danych, która jest rozszerzana przez każdą z klas baz danych, jak pokazano poniżej

DOSTĘP DO DANYCH KLIENTÓW HIPOTECZNYCH:

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

AKTUALNY DOSTĘP DO DANYCH KLIENTÓW NA KONCIE:

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

KONTO OSZCZĘDNOŚCI DOSTĘP DO DANYCH KLIENTA:

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

Po ustawieniu tych 3 klas dostępu do danych zwracamy teraz uwagę na klienta. W warstwie Business mamy klasę CustomerServiceManager, która zwraca dane klienta do swoich klientów.

WARSTWA BIZNESOWA:

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

Nie pokazałem zastrzyku zależności, aby było to proste, ponieważ teraz już się komplikuje.

Teraz, jeśli mamy nową bazę danych klientów, możemy po prostu dodać nową klasę, która rozszerza BaseDataAccess i udostępnia swój obiekt bazy danych.

Oczywiście potrzebujemy identycznych procedur składowanych we wszystkich uczestniczących bazach danych.

Na koniec klient CustomerServiceManagerklasy wywoła tylko metodę GetCustomerDetails, przekaże lastName i nie powinien przejmować się tym, skąd i skąd pochodzą dane.

Mam nadzieję, że to da ci praktyczne podejście do zrozumienia LSP.

Yawar Murtaza
źródło
3
Jak to może być przykład LSP?
somegeek
1
W tym też nie widzę przykładu LSP ... Dlaczego ma tyle głosów za?
StaNov
1
@RoshanGhangare IDataAccess ma 3 konkretne implementacje, które można zastąpić w warstwie biznesowej.
Yawar Murtaza
1
@YawarMurtaza cokolwiek zacytowałeś, jest typową implementacją wzorca strategii i to wszystko. Czy możesz wyjaśnić, gdzie to łamie LSP i jak rozwiązujesz to naruszenie LSP
Yogesh,
@Yogesh - Możesz zamienić implementację IDataAccess na dowolną konkretną klasę, a to nie wpłynie na kod klienta - tak w skrócie jest LSP. Tak, niektóre wzorce projektowe pokrywają się. Po drugie, powyższa odpowiedź ma na celu jedynie pokazanie, jak LSP jest wdrażane w systemie produkcyjnym dla aplikacji bankowej. Nie chciałem pokazywać, jak można zepsuć LSP i jak to naprawić - to byłby samouczek szkoleniowy, którego setki można znaleźć w sieci.
Yawar Murtaza
0

Oto kod stosowania zasady zastępczej Liskova.

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 stwierdza: "Klasy pochodne powinny być substytucyjne dla ich klas bazowych (lub interfejsów)" & "Metody używające odwołań do klas bazowych (lub interfejsów) muszą być w stanie używać metod klas pochodnych bez wiedzy o tym lub znajomości szczegółów ”.

mark333 ... 333 ... 333
źródło