Zasadniczo chcemy, aby rzeczy zachowywały się rozsądnie.
Rozważ następujący problem:
Dano mi grupę prostokątów i chcę zwiększyć ich powierzchnię o 10%. Więc to, co robię, to ustawiam długość prostokąta na 1,1 razy większą niż wcześniej.
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
foreach(var rectangle in rectangles)
{
rectangle.Length = rectangle.Length * 1.1;
}
}
Teraz w tym przypadku wszystkie moje prostokąty mają teraz długość zwiększoną o 10%, co zwiększy ich powierzchnię o 10%. Niestety, ktoś faktycznie przekazał mi mieszaninę kwadratów i prostokątów, a kiedy zmieniła się długość prostokąta, zmieniła się również szerokość.
Moje testy jednostkowe zdały, ponieważ napisałem wszystkie moje testy jednostkowe, aby użyć zbioru prostokątów. Wprowadziłem teraz do mojej aplikacji subtelny błąd, który może pozostać niezauważony przez wiele miesięcy.
Co gorsza, Jim z księgowości widzi moją metodę i pisze inny kod, który wykorzystuje fakt, że jeśli przekazuje kwadraty do mojej metody, uzyskuje bardzo ładny wzrost wielkości o 21%. Jim jest szczęśliwy i nikt nie jest mądrzejszy.
Jim zostaje awansowany za doskonałą pracę do innego oddziału. Alfred dołącza do firmy jako junior. W swoim pierwszym raporcie o błędach Jill z działu reklamy podał, że przekazanie kwadratów tej metodzie powoduje wzrost o 21% i chce, aby błąd został naprawiony. Alfred widzi, że kwadraty i prostokąty są używane wszędzie w kodzie, i zdaje sobie sprawę, że przerwanie łańcucha dziedziczenia jest niemożliwe. Nie ma również dostępu do kodu źródłowego Księgowości. Więc Alfred naprawia błąd w ten sposób:
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
foreach(var rectangle in rectangles)
{
if (typeof(rectangle) == Rectangle)
{
rectangle.Length = rectangle.Length * 1.1;
}
if (typeof(rectangle) == Square)
{
rectangle.Length = rectangle.Length * 1.04880884817;
}
}
}
Alfred jest zadowolony ze swoich umiejętności hakerskich, a Jill sygnalizuje, że błąd został naprawiony.
W przyszłym miesiącu nikt nie otrzyma zapłaty, ponieważ księgowość była zależna od możliwości przekazania kwadratów do IncreaseRectangleSizeByTenPercent
metody i zwiększenia powierzchni o 21%. Cała firma przechodzi w tryb „poprawki błędu 1”, aby wyśledzić źródło problemu. Śledzą problem do rozwiązania Alfreda. Wiedzą, że muszą zadowolić zarówno księgowość, jak i reklamę. Rozwiązują więc problem, identyfikując użytkownika za pomocą wywołania metody w następujący sposób:
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
IncreaseRectangleSizeByTenPercent(
rectangles,
new User() { Department = Department.Accounting });
}
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
foreach(var rectangle in rectangles)
{
if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
{
rectangle.Length = rectangle.Length * 1.1;
}
else if (typeof(rectangle) == Square)
{
rectangle.Length = rectangle.Length * 1.04880884817;
}
}
}
I tak dalej i tak dalej.
Ta anegdota oparta jest na rzeczywistych sytuacjach, z którymi codziennie spotykają się programiści. Naruszenie zasady substytucji Liskowa może wprowadzić bardzo subtelne błędy, które zostaną wykryte dopiero po latach od ich napisania, do tego czasu naprawienie naruszenia spowoduje uszkodzenie wielu rzeczy, a nie naprawienie go rozwścieczy twojego największego klienta.
Istnieją dwa realistyczne sposoby rozwiązania tego problemu.
Pierwszym sposobem jest uczynienie Rectangle niezmiennym. Jeśli użytkownik Prostokąta nie może zmienić właściwości Długość i Szerokość, problem zniknie. Jeśli chcesz mieć prostokąt o innej długości i szerokości, utwórz nowy. Kwadraty z radością dziedziczą po prostokątach.
Drugim sposobem jest przerwanie łańcucha spadkowego między kwadratami i prostokątami. Jeżeli kwadrat jest definiowana jako posiadające jedną SideLength
nieruchomość i prostokąty mają Length
i Width
mienia i nie ma dziedziczenia, to niemożliwe, aby przypadkowo złamać rzeczy oczekując prostokąt i coraz kwadrat. W języku C # możesz seal
zdefiniować klasę prostokąta, co gwarantuje, że wszystkie prostokąty, które kiedykolwiek otrzymasz, są w rzeczywistości prostokątami.
W tym przypadku podoba mi się sposób „rozwiązania problemu”. Tożsamość prostokąta to jego długość i szerokość. Ma sens, że gdy chcesz zmienić tożsamość obiektu, tak naprawdę chcesz nowego obiektu. Jeśli stracisz starego klienta i zyskasz nowego, nie zmienisz Customer.Id
pola ze starego klienta na nowego, tworzysz nowego Customer
.
Naruszenia zasady substytucji Liskowa są powszechne w świecie rzeczywistym, głównie dlatego, że wiele kodu jest napisanych przez ludzi, którzy są niekompetentni / pod presją czasu / nie dbają / popełniają błędy. Może i prowadzi do bardzo nieprzyjemnych problemów. W większości przypadków zamiast tego preferujesz kompozycję zamiast dziedziczenia .