Czy przypadki szczególne z awariami naruszają zasadę substytucji Liskowa?

20

Załóżmy, że mam interfejs FooInterfacez następującą sygnaturą:

interface FooInterface {
    public function doSomething(SomethingInterface something);
}

I konkretna klasa, ConcreteFooktóra implementuje ten interfejs:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
    }

}

Chciałbym ConcreteFoo::doSomething()zrobić coś wyjątkowego, jeśli przejdzie on przez specjalny typ SomethingInterfaceobiektu (powiedzmy, że się nazywa SpecialSomething).

Zdecydowanie jest to naruszenie LSP, jeśli wzmocnię warunki wstępne metody lub wyrzucę nowy wyjątek, ale czy nadal byłoby to naruszenie LSP, jeśli stworzę SpecialSomethingobiekty specjalne , zapewniając rezerwę dla SomethingInterfaceobiektów ogólnych ? Coś jak:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
        if (something instanceof SpecialSomething) {
            // Do SpecialSomething magic
        }
        else {
            // Do generic SomethingInterface magic
        }
    }

}
Evan
źródło

Odpowiedzi:

19

Może to być naruszenie LSP w zależności od tego, co jeszcze jest zawarte w umowie dotyczącej tej doSomethingmetody. Ale prawie na pewno pachnie kodem, nawet jeśli nie narusza LSP.

Na przykład, jeśli częścią umowy doSomethingjest to, że zadzwoni something.commitUpdates()co najmniej raz przed powrotem, a w szczególnym przypadku zadzwoni commitSpecialUpdates()zamiast tego, jest to naruszenie LSP. Nawet jeśli SpecialSomethingjest commitSpecialUpdates()metoda została świadomie zaprojektowane, aby zrobić wszystko, z tego samego materiału co commitUpdates(), to tylko zapobiegawczo hacking wokół naruszenie LSP, a jest dokładnie ten rodzaj hackery nie miałaby zrobić, gdy jeden po LSP konsekwentnie. To, czy coś takiego dotyczy twojej sprawy, musisz dowiedzieć się, sprawdzając umowę dotyczącą tej metody (wyraźnej lub dorozumianej).

Powodem tego jest zapach kodu, ponieważ sprawdzenie konkretnego typu jednego z argumentów pomija sens zdefiniowania dla niego interfejsu / typu abstrakcyjnego i ponieważ w zasadzie nie można już zagwarantować, że metoda nawet działa (wyobraź sobie jeśli ktoś napisze podklasę SpecialSomethingz założeniem, że commitUpdates()zostanie wywołany). Przede wszystkim staraj się, aby te specjalne aktualizacje działały w ramach istniejącychSomethingInterface; to najlepszy możliwy wynik. Jeśli naprawdę jesteś pewien, że nie możesz tego zrobić, musisz zaktualizować interfejs. Jeśli nie kontrolujesz interfejsu, może być konieczne rozważenie napisania własnego interfejsu, który robi to, co chcesz. Jeśli nie możesz nawet wymyślić interfejsu, który działa dla nich wszystkich, być może powinieneś całkowicie zeskrobać interfejs i mieć wiele metod przyjmujących różne konkretne typy, a może nawet lepszy jest refaktor. Musielibyśmy dowiedzieć się więcej o magii, którą skomentowałeś, aby powiedzieć, która z nich jest odpowiednia.

Ixrec
źródło
Dzięki! To pomaga. Hipotetycznie celem doSomething()metody byłaby konwersja typu na SpecialSomething: jeśli ją otrzyma SpecialSomething, po prostu zwróci obiekt niezmodyfikowany, natomiast jeśli otrzyma SomethingInterfaceobiekt ogólny , uruchomi algorytm, aby przekonwertować go na SpecialSomethingobiekt. Ponieważ warunki wstępne i dodatkowe pozostają takie same, nie sądzę, aby umowa została naruszona.
Evan,
1
@Evan Oh wow ... to ciekawy przypadek. To może być całkowicie bezproblemowe. Jedyną rzeczą, o której mogę myśleć, jest to, że jeśli zwracasz istniejący obiekt zamiast budować nowy obiekt, być może ktoś zależy od tej metody zwracania zupełnie nowego obiektu ... ale to, czy tego rodzaju rzeczy mogą złamać ludzie, może zależeć od język. Czy jest możliwe dla kogoś zadzwonić y = doSomething(x), a potem x.setFoo(3), a potem okaże się, że y.getFoo()zwraca 3?
Ixrec,
Jest to problematyczne w zależności od języka, choć należy go łatwo obejść, zwracając SpecialSomethingzamiast tego kopię obiektu. Chociaż ze względu na czystość, mogłem również zrezygnować z optymalizacji przypadków specjalnych, gdy obiekt przechodzi, SpecialSomethingi po prostu uruchomić go przez większy algorytm konwersji, ponieważ nadal powinien działać, ponieważ jest on również SomethingInterfaceobiektem.
Evan
1

To nie jest naruszenie LSP. Nadal jednak narusza zasadę „nie rób sprawdzeń typów”. Lepiej byłoby zaprojektować kod w taki sposób, aby specjalny kod pojawiał się naturalnie; może SomethingInterfacepotrzebuje innego członka, który mógłby to osiągnąć, a może trzeba gdzieś wstrzyknąć abstrakcyjną fabrykę.

Jednak nie jest to trudna i szybka reguła, więc musisz zdecydować, czy kompromis jest tego wart. W tej chwili masz zapach kodu i potencjalną przeszkodę dla przyszłych ulepszeń. Pozbycie się tego może oznaczać znacznie bardziej skomplikowaną architekturę. Bez dodatkowych informacji nie mogę powiedzieć, który z nich jest lepszy.

Sebastian Redl
źródło
1

Nie, wykorzystanie faktu, że dany argument nie tylko zapewnia interfejs A, ale także A2, nie narusza LSP.

Tylko upewnij się, że specjalna ścieżka nie ma żadnych silniejszych warunków wstępnych (oprócz tych przetestowanych przy podejmowaniu decyzji o jej przyjęciu), ani żadnych słabszych warunków wstępnych.

Szablony C ++ często to robią, aby zapewnić lepszą wydajność, na przykład wymagając InputIterators, ale dając dodatkowe gwarancje, jeśli są wywoływane z RandomAccessIterators.

Jeśli zamiast tego musisz zdecydować w czasie wykonywania (na przykład za pomocą dynamicznego rzutowania), strzeż się decyzji, którą ścieżkę wybrać, wykorzystując wszystkie potencjalne zyski lub nawet więcej.

Skorzystanie ze specjalnego przypadku często jest sprzeczne z OSUSZANIEM (nie powtarzaj się), ponieważ może być konieczne zduplikowanie kodu, oraz z KISS (Keep it Simple), ponieważ jest bardziej złożony.

Deduplikator
źródło
0

Istnieje kompromis między zasadą „nie rób sprawdzeń typów” i „segreguj swoje interfejsy”. Jeśli wiele klas zapewnia wykonalne, ale być może nieefektywne środki do wykonywania niektórych zadań, a kilka z nich może również oferować lepsze środki, i istnieje zapotrzebowanie na kod, który może zaakceptować dowolną szerszą kategorię elementów, które mogą wykonać zadanie (być może nieefektywnie), ale następnie wykonać zadanie tak efektywnie, jak to możliwe, konieczne będzie, aby wszystkie obiekty zaimplementowały interfejs zawierający element członkowski, aby powiedzieć, czy obsługiwana jest bardziej wydajna metoda, i inny, aby użyć go, jeśli jest, lub kod, który odbiera obiekt, sprawdza, czy obsługuje rozszerzony interfejs i rzutuje go, jeśli tak jest.

Osobiście popieram poprzednie podejście, choć chciałbym, aby frameworki zorientowane obiektowo, takie jak .NET, pozwoliłyby interfejsom na określenie domyślnych metod (dzięki czemu większe interfejsy byłyby mniej bolesne w pracy). Jeśli wspólny interfejs zawiera opcjonalne metody, wówczas pojedyncza klasa opakowania może obsługiwać obiekty o wielu różnych kombinacjach zdolności, jednocześnie obiecując konsumentom tylko te umiejętności, które występują w oryginalnym zawiniętym obiekcie. Jeśli wiele funkcji jest podzielonych na różne interfejsy, wówczas dla każdej innej kombinacji interfejsów, które mogą wymagać obsługi zawijanych obiektów, potrzebny będzie inny obiekt opakowania.

supercat
źródło
0

Zasada podstawienia Liskowa dotyczy podtypów działających zgodnie z umową ich nadtypu. Tak, jak napisał Ixrec, nie ma wystarczających informacji, aby odpowiedzieć, czy jest to naruszenie LSP.

Naruszono tu jednak zasadę otwartego zamkniętego. Jeśli masz nowy wymóg - wykonaj magię SpecialSomething - i musisz zmodyfikować istniejący kod, to zdecydowanie naruszasz OCP .

Zapadło
źródło