Czy zasada podstawienia Liskowa jest niezgodna z introspekcją lub pisaniem kaczek?

11

Czy rozumiem poprawnie, że nie można przestrzegać zasady substytucji Liskowa w językach, w których obiekty mogą się sprawdzać, tak jak w przypadku języków pisanych kaczorkiem?

Na przykład, w Ruby, jeśli klasa Bdziedziczy z klasy A, a następnie za każdy przedmiot xz A, x.classjedzie do powrotu A, ale jeśli xjest przedmiotem B, x.classnie ma powrotu A.

Oto oświadczenie LSP:

Niech q (x) być właściwością dowodliwe o obiekty x typu T . Następnie Q (y) powinny być udowodnione dla obiektów Y typu S , gdzie S jest podtypem T .

Na przykład w Ruby

class T; end
class S < T; end

naruszają LSP w tej formie, o czym świadczy właściwość q (x) =x.class.name == 'T'


Dodanie. Jeśli odpowiedź brzmi „tak” (LSP niezgodny z introspekcją), moje inne pytanie brzmiałoby: czy istnieje jakaś zmodyfikowana „słaba” forma LSP, która może być w stanie utrzymać dynamiczny język, być może pod pewnymi dodatkowymi warunkami i tylko ze specjalnymi typami o właściwościach .


Aktualizacja. Dla porównania, oto inne sformułowanie LSP, które znalazłem w sieci:

Funkcje korzystające ze wskaźników lub referencji do klas podstawowych muszą mieć możliwość korzystania z obiektów klas pochodnych bez ich znajomości.

I kolejny:

Jeśli S jest zadeklarowanym podtypem T, obiekty typu S powinny zachowywać się tak, jak obiekty typu T powinny się zachowywać, jeśli są traktowane jako obiekty typu T.

Ostatni jest opatrzony adnotacjami:

Zauważ, że LSP dotyczy oczekiwanego zachowania obiektów. Można śledzić LSP tylko wtedy, gdy jest jasne, jakie jest oczekiwane zachowanie obiektów.

Wydaje się, że jest to słabsze niż pierwotne i może być możliwe do zaobserwowania, ale chciałbym, aby było to sformalizowane, w szczególności wyjaśnione, kto decyduje o oczekiwanym zachowaniu.

Czy zatem LSP nie jest własnością pary klas w języku programowania, ale pary klas wraz z danym zestawem właściwości, spełnianych przez klasę przodka? Czy w praktyce oznacza to, że aby skonstruować podklasę (klasę potomną) w odniesieniu do LSP, wszystkie możliwe zastosowania klasy przodka muszą być znane? Według LSP klasa przodków ma być zastępowalna dowolną klasą potomną, prawda?


Aktualizacja. Przyjąłem już odpowiedź, ale chciałbym dodać jeszcze jeden konkretny przykład od Ruby, aby zilustrować pytanie. W Ruby każda klasa jest modułem w tym sensie, że Classklasa jest potomkiem Moduleklasy. Jednak:

class C; end
C.is_a?(Module) # => true
C.class # => Class
Class.superclass # => Module

module M; end
M.class # => Module

o = Object.new

o.extend(M) # ok
o.extend(C) # => TypeError: wrong argument type Class (expected Module)
Alexey
źródło
2
Prawie wszystkie współczesne języki zapewniają pewien stopień introspekcji, więc pytanie nie jest tak naprawdę specyficzne dla Ruby.
Joachim Sauer
Rozumiem, podałem Ruby jako przykład. Nie wiem, może w niektórych innych językach z introspekcją istnieją pewne „słabe formy” LSP, ale jeśli dobrze zrozumiałem zasadę, jest to niezgodne z introspekcją.
Alexey
Usunąłem „Ruby” z tytułu.
Alexey
2
Krótka odpowiedź jest zgodna. Oto post na blogu, z którym w większości się zgadzam: Zasada podstawienia
Liskova
2
@Alexey Właściwości w tym kontekście są niezmiennikami obiektu. Na przykład niezmienne obiekty mają właściwość, której ich wartości się nie zmieniają. Jeśli spojrzysz na dobre testy jednostkowe, powinny one dokładnie sprawdzić te właściwości.
K.Steff

Odpowiedzi:

29

Oto faktyczna zasada :

Pozwól q(x)być właściwością dającą się udowodnić o obiektach xtypu T. Następnie q(y)należy udowodnić dla obiektów ytypu Sgdzie Sjest podtypem T.

I doskonałe streszczenie Wikipedii :

Stwierdza się, że w programie komputerowym, jeśli S jest podtypem T, wówczas obiekty typu T można zastąpić obiektami typu S (tj. Obiekty typu S można zastąpić obiektami typu T) bez zmiany żadnego z pożądane właściwości tego programu (poprawność, wykonane zadanie itp.).

I kilka istotnych cytatów z artykułu:

Potrzebny jest silniejszy wymóg, który ogranicza zachowanie podtypów: właściwości, które można udowodnić za pomocą specyfikacji domniemanego typu obiektu, powinny zostać zachowane, nawet jeśli obiekt faktycznie jest członkiem podtypu tego typu ...

Specyfikacja typu zawiera następujące informacje:
- nazwę typu;
- Opis przestrzeni wartości typu;
- Dla każdej z metod tego typu:
--- Jego nazwa;
--- Jego podpis (w tym sygnalizowane wyjątki);
--- Jego zachowanie w zakresie warunków wstępnych i dodatkowych.

Przejdźmy do pytania:

Czy rozumiem poprawnie, że nie można przestrzegać zasady substytucji Liskowa w językach, w których obiekty mogą się sprawdzać, tak jak w przypadku języków pisanych kaczorkiem?

Nie.

A.classzwraca klasę.
B.classzwraca klasę.

Ponieważ możesz wykonać to samo połączenie na bardziej konkretnym typie i uzyskać zgodny wynik, LSP wstrzymuje. Problem polega na tym, że w przypadku dynamicznych języków nadal można wywoływać różne wyniki, oczekując, że będą dostępne.

Ale zastanówmy się nad statycznym, strukturalnym językiem (kaczym). W takim przypadku A.classzwróci typ z ograniczeniem, które musi być, Alub podtypem A. Daje to statyczną gwarancję, że każdy podtyp Amusi zapewniać metodę, T.classktórej wynikiem jest typ spełniający to ograniczenie.

Zapewnia to silniejsze twierdzenie, że LSP utrzymuje się w językach, które obsługują pisanie kaczek, i że każde naruszenie LSP w czymś takim jak Ruby występuje częściej z powodu normalnego niewłaściwego użycia dynamicznego niż niezgodności projektu językowego.

Telastyn
źródło
1
„Ponieważ możesz wykonać to samo połączenie na bardziej konkretnym typie i uzyskać zgodny wynik, LSP wstrzymuje”. LSP utrzymuje się, jeśli wyniki są identyczne, jeśli dobrze to zrozumiałem. Być może istnieje jakaś „tygodniowa” forma LSP w odniesieniu do danych ograniczeń, wymagająca zamiast wszystkich właściwości, aby spełnione były tylko dane ograniczenia. W każdym razie byłbym wdzięczny za wszelkie odniesienia.
Alexey
@Alexey zredagowano, aby uwzględnić znaczenie LSP. Jeśli mogę użyć B tam, gdzie oczekuję A, to LSP ma. Jestem ciekawy, jak myślisz, że klasa Ruby może to naruszać.
Telastyn
3
@Alexey - Jeśli Twój program zawiera fail unless x.foo == 42i podtyp zwraca 0, to to samo. To nie jest awaria LSP, to normalne działanie twojego programu. Polimorfizm nie stanowi naruszenia LSP.
Telastyn
1
@Alexey - Jasne. Załóżmy, że jest to właściwość. W takim przypadku kod narusza LSP, ponieważ nie pozwala podtypom zachowywać tego samego zachowania semantycznego. Ale nie jest to szczególnie wyjątkowe w przypadku języków pisanych dynamicznie lub kaczych. W ich języku nie ma nic, co powoduje naruszenie. Kod, który napisałeś, robi. Pamiętaj, że LSP jest zasadą projektowania programów (stąd powinno być w definicji), a nie matematyczną właściwością programów.
Telastyn
6
@Alexey: jeśli napiszesz coś zależnego od x.class == A, to twój kod narusza LSP , a nie język. Możliwe jest pisanie kodu, który narusza LSP w prawie każdym języku programowania.
Andres F.,
7

W kontekście LSP „właściwość” to coś, co można zaobserwować na typie (lub obiekcie). W szczególności mówi się o „własności do udowodnienia”.

Taka „właściwość” mogłaby istnieć w przypadku foo()metody, która nie ma wartości zwrotnej (i jest zgodna z umową określoną w jej dokumentacji).

Upewnij się, że nie pomylisz tego terminu z „właściwość”, ponieważ „ classjest własnością każdego obiektu w Rubim”. Taka „właściwość” może być „właściwością LSP”, ale nie jest automatycznie taka sama!

Teraz odpowiedź na twoje pytania zależy w dużej mierze od tego, jak surowo zdefiniujesz „właściwość”. Jeśli powiesz „własność klasy Ajest to, że .classpowróci typ obiektu”, wtedy Brzeczywiście nie mają tę właściwość.

Jeśli jednak zdefiniować „własność” być „ .classpowraca A”, to oczywiście Bnie nie posiadają tę właściwość.

Jednak druga definicja nie jest zbyt przydatna, ponieważ zasadniczo znalazłeś okrągły sposób na zadeklarowanie stałej.

Joachim Sauer
źródło
Mogę wymyślić tylko jedną definicję „właściwości” programu: dla danego wejścia zwraca określoną wartość, lub, bardziej ogólnie, gdy jest używany jako blok w innym programie, ten inny program dla danego wejścia zwraca podane wartości. Dzięki tej definicji nie widzę, co to znaczy, że „ .classzwróci typ obiektu”. Jeśli to oznacza x.class == x.class, nie jest to interesująca właściwość.
Alexey
1
@Alexey: Zaktualizowałem swoje pytanie, wyjaśniając, co oznacza „własność” w kontekście LSP.
Joachim Sauer
2
@Alexey: patrząc na gazetę nie znalazłem konkretnej definicji ani „właściwości”. Prawdopodobnie dlatego, że termin ten jest używany w ogólnym sensie CS „coś, co można zaobserwować / udowodnić na temat jakiegoś obiektu”. Nie ma to nic wspólnego z drugim oznaczającym „pole obiektu”.
Joachim Sauer
4
@Alexey: Nie wiem, co więcej mogę powiedzieć. Używam definicji „właściwość jest cechą lub atutem obiektu”. „kolor” jest własnością fizycznego, widzialnego obiektu. „gęstość” jest właściwością materiału. „posiadanie określonej metody” jest właściwością klasy / obiektu.
Joachim Sauer
4
@Alexey: Myślę, że wylewasz dziecko z kąpielą: tylko dlatego, że w przypadku niektórych właściwości LSP nie może być utrzymany, nie oznacza, że ​​jest bezużyteczny lub „nie trzyma się żadnego języka”. Ale ta dyskusja zaszedłaby daleko tutaj.
Joachim Sauer
5

Jak rozumiem, w introspekcji nie ma nic, co byłoby niezgodne z LSP. Zasadniczo, o ile obiekt obsługuje te same metody co inne, oba powinny być wymienne. Oznacza to, że jeśli kod oczekuje Addressobiekt, to nie ma znaczenia, czy jest to CustomerAddresslub WarehouseAddress, o ile oba dostarczają (na przykład) getStreetAddress(), getCityName(), getRegion()i getPostalCode(). Z pewnością możesz stworzyć dekorator, który pobiera inny typ obiektu i wykorzystuje introspekcję, aby zapewnić wymagane metody (np. DestinationAddressKlasa, która przyjmuje Shipmentobiekt i przedstawia adres dostawy jako Address), ale nie jest to wymagane i na pewno nie zapobiegają stosowaniu LSP.

TMN
źródło
2
@Alexey: Obiekty są „takie same”, jeśli obsługują te same metody. Oznacza to tę samą nazwę, tę samą liczbę i typ argumentów, ten sam typ zwrotu i te same skutki uboczne (o ile są one widoczne dla kodu wywołującego). Metody mogą zachowywać się zupełnie inaczej, ale tak długo, jak dotrzymują umowy, nie ma problemu.
TMN
1
@Alexey: ale dlaczego miałbym mieć taką umowę? Jakie faktyczne wykorzystanie spełnia ta umowa? Gdybym miał taką umowę może po prostu zastąpić każde wystąpienie x.class.namez 'A' , co skutecznie x.class.name bezużyteczny .
Joachim Sauer
1
@Alexey: znowu: tylko dlatego, że możesz zdefiniować kontrakt, którego nie można wypełnić przez rozszerzenie innej klasy, nie łamie LSP. Oznacza to po prostu, że zbudowałeś klasę, której nie można rozszerzać. Jeśli zdefiniuję metodę „powrotu, jeśli dostarczony blok kodu kończy się w skończonym czasie ”, to mam również umowę, której nie można spełnić. Nie oznacza to, że programowanie jest bezużyteczne.
Joachim Sauer
2
@Alexey próbuje ustalić, czy x.class.name == 'A'jest to anty-wzór w pisaniu kaczek: w końcu pisanie kaczki pochodzi od „Jeśli drży i chodzi jak kaczka, to jest kaczka”. Jeśli więc zachowuje się Ai honoruje przedstawione umowy A, to jest to Aprzypadek
K.Steff
1
@Alexey Otrzymałeś jasne definicje. Klasa obiektu nie jest częścią jego zachowania, kontraktu ani niczego, co chcesz nazwać. Zastępujesz „właściwość” za pomocą „pola obiektu”, takiego jak x.myField ”, co, jak już wspomniano, NIE jest takie samo. W tym kontekście właściwość bardziej przypomina matematyczną właściwość, taką jak niezmienniki typu. Co więcej, jest to anty-wzorzec, aby sprawdzić dokładny typ, jeśli chcesz pisać kaczkę. Jaki masz problem z LSP i pisaniem kaczek? ;)
Andres F.,
4

Po przejrzeniu oryginalnej pracy Barbary Liskov odkryłem, jak uzupełnić definicję Wikipedii, aby LSP rzeczywiście był zadowolony w prawie każdym języku.

Przede wszystkim słowo „możliwe do udowodnienia” jest ważne w definicji. Nie jest to wyjaśnione w artykule na Wikipedii, a „ograniczenia” są wymienione gdzie indziej, bez odniesienia do niego.

Oto pierwszy ważny cytat z artykułu:

Potrzebny jest silniejszy wymóg, który ogranicza zachowanie podtypów: właściwości, które można udowodnić za pomocą specyfikacji domniemanego typu obiektu, powinny zostać zachowane, nawet jeśli obiekt faktycznie jest członkiem podtypu tego typu ...

A oto druga, wyjaśniająca, czym jest specyfikacja typu :

Specyfikacja typu zawiera następujące informacje:

  • Nazwa typu;
  • Opis przestrzeni wartości typu;
  • Dla każdej z metod tego typu:
    • Jego nazwa;
    • Podpis (w tym sygnalizowane wyjątki);
    • Jego zachowanie w zakresie warunków wstępnych i dodatkowych.

Zatem LSP ma sens tylko w odniesieniu do danych specyfikacji danego typu , a dla odpowiedniej specyfikacji typu (na przykład dla pustej) może być spełniony prawdopodobnie w dowolnym języku.

Uważam, że odpowiedź Telastyn jest najbliższa temu, czego szukałem, ponieważ „ograniczenia” zostały wyraźnie wymienione.

Alexey
źródło
Telastyn, jeśli możesz dodać te cytaty do swojej odpowiedzi, wolę zaakceptować twoje niż moje własne.
Alexey,
2
znaczniki nie są takie same i zmieniłem trochę empatii, ale cytaty zostały uwzględnione.
Telastyn
Przepraszamy, Joachim Sauer wspomniał już o właściwościach „możliwych do udowodnienia”, których nie lubiłeś. Ogólnie rzecz biorąc, po prostu przekształciłeś istniejące odpowiedzi. Szczerze mówiąc, nie wiem, czego szukasz ...
Andres F.
Nie, nie wyjaśniono „co można udowodnić?”. Właściwość x.class.name = 'A'można udowodnić dla wszystkich xklas, Ajeśli pozwolisz na zbyt dużą wiedzę. Specyfikacja typu nie została zdefiniowana, a jej dokładny związek z LSP również nie był określony, chociaż nieoficjalnie podano pewne wskazówki. Znalazłem już to, czego szukałem w pracy Liskova i odpowiedziałem na moje pytanie powyżej.
Alexey
Myślę, że słowa, które ośmieliłeś, są kluczem. Jeśli nadtyp udokumentuje, że którykolwiek xz tego typu x.woozleda undefined, to żaden typ, dla którego x.woozlenie daje, nie undefinedbędzie właściwym podtypem. Jeśli nadtyp nie udokumentuje niczego x.woozle, fakt, że użycie x.woozlenadtypu przydarzy się undefined, nie dałoby nic do zrozumienia, co mogłoby zrobić na podtypie.
supercat
3

Cytując artykuł Wikipedii na temat LSP , „podstawialność jest zasadą w programowaniu obiektowym”. Jest to zasada i część projektu twojego programu. Jeśli piszesz kod, który zależy x.class == A, to twój kod narusza LSP. Uwaga: ten rodzaj zepsutego kodu jest również możliwy w Javie, nie trzeba wpisywać kaczki.

Nic w wpisywaniu kaczek z natury nie łamie LSP. Tylko jeśli użyjesz go niewłaściwie, jak w twoim przykładzie.

Dodatkowa myśl: czy jawne sprawdzanie, czy klasa obiektu nie spełnia celu pisania kaczką?

Andres F.
źródło
Andres, czy możesz podać swoją definicję LSP?
Alexey
1
@Alexey Dokładna definicja LSP podana jest w Wikipedii w kategoriach podtypów. Nieformalna definicja to Liskov's notion of a behavioral subtype defines a notion of substitutability for mutable objects; that is, 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).. Dokładna klasa obiektu NIE jest jedną z „pożądanych właściwości programu”; w przeciwnym razie byłoby to sprzeczne nie tylko z pisaniem kaczek, ale ogólnie z podtytułem, włącznie ze smakiem Javy.
Andres F.,
2
@Alexey Należy również pamiętać, że Twój przykład x.class == Anarusza zarówno LSP, jak i pisanie na klawiaturze. Nie ma sensu używać pisania kaczego, jeśli chcesz sprawdzić rzeczywiste typy.
Andres F.,
Andres, ta definicja nie jest wystarczająco precyzyjna, aby zrozumieć. Który program, dany czy jakikolwiek? Jaka jest pożądana właściwość? Jeśli klasa znajduje się w bibliotece, różne aplikacje mogą uznać za pożądane inne właściwości. Nie widzę, w jaki sposób wiersz kodu może naruszać LSP, ponieważ myślałem, że LSP jest własnością pary klas w danym języku programowania: albo ( A, B) spełnia LSP, albo nie. Jeśli LSP zależy od kodu używanego w innym miejscu, nie jest wyjaśnione, jaki kod jest dozwolony. Mam nadzieję znaleźć coś tutaj: cse.ohio-state.edu/~neelam/courses/788/lwb.pdf
Alexey
2
@Alexey LSP przechowuje (lub nie) dla określonego projektu. Jest to coś, czego można szukać w projekcie; ogólnie nie jest własnością języka. To nie ma nic bardziej precyzyjne niż rzeczywistej definicji: Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T. Oczywiste jest, że x.classnie jest to jedna z interesujących właściwości; w przeciwnym razie polimorfizm Javy też nie działałby. W „problemie x.class” nie ma nic nieodłącznego od pisania kaczką. Czy zgadzasz się do tej pory?
Andres F.,