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.
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.
Odpowiedzi:
(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, ArgumentNullException
jeś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 Swim
metodzie i dzięki temu sprawienie, by kaczka elektryczna zachowywała się dokładnie tak, jak zdefiniowano w IDuck
interfejsie
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 Swim
metody 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 Swim
jest, że wystąpią problemy, NIE włączając go, ponieważ oczekuje się, że będzie pływać podczas korzystania z IDuck
interfejsu
Zaktualizuj 2
Zmieniono niektóre części, aby było jaśniejsze.
if duck is ElectricDuck
części. W zeszły czwartek miałem seminarium na temat SOLID :)
as
sł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();
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 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 CustomerServiceManager
klasy 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.
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 ”.