Jestem aktualnie w procesie próbują opanować C #, więc czytam kod adaptacyjnego poprzez C # przez Gary McLean Hall .
Pisze o wzorach i anty-wzorach. W części Implementacje kontra interfejsy pisze:
Deweloperzy, którzy są nowi w koncepcji programowania interfejsów, często mają trudności z odpuszczeniem tego, co kryje się za interfejsem.
W czasie kompilacji każdy klient interfejsu nie powinien mieć pojęcia, jakiej implementacji interfejsu używa. Taka wiedza może prowadzić do błędnych założeń, które łączą klienta z konkretną implementacją interfejsu.
Wyobraź sobie powszechny przykład, w którym klasa musi zapisać rekord w trwałym magazynie. Aby to zrobić, słusznie deleguje do interfejsu, który ukrywa szczegóły stosowanego mechanizmu trwałego przechowywania. Jednak niewłaściwe byłoby przyjmowanie jakichkolwiek założeń dotyczących tego, która implementacja interfejsu jest używana w czasie wykonywania. Na przykład rzutowanie odwołania interfejsu do dowolnej implementacji jest zawsze złym pomysłem.
Może to być bariera językowa lub brak doświadczenia, ale nie do końca rozumiem, co to oznacza. Oto co rozumiem:
Mam projekt zabawy w wolnym czasie, aby ćwiczyć C #. Tam mam zajęcia:
public class SomeClass...
Ta klasa jest używana w wielu miejscach. Podczas nauki języka C # przeczytałem, że lepiej jest abstrakcji za pomocą interfejsu, więc zrobiłem następujące
public interface ISomeClass <- Here I made a "contract" of all the public methods and properties SomeClass needs to have.
public class SomeClass : ISomeClass <- Same as before. All implementation here.
Więc przejrzałem wszystkie odniesienia do klas i zastąpiłem je ISomeClass.
Z wyjątkiem konstrukcji, w której napisałem:
ISomeClass myClass = new SomeClass();
Czy dobrze rozumiem, że to źle? Jeśli tak, dlaczego tak i co powinienem zrobić zamiast tego?
źródło
ISomeClass myClass = new SomeClass();
? Jeśli tak naprawdę masz na myśli, to jest to rekurencja w konstruktorze, prawdopodobnie nie to, czego chcesz. Mam nadzieję, że masz na myśli w„ konstrukcji ”, to znaczy alokacji, ale nie w samym konstruktorze, prawda ?ISomeClass
), ale łatwo jest także tworzyć zbyt ogólne interfejsy, dla których nie można napisać przydatnego kodu, na którym to etapie jedyne opcje są na nowo przemyśleć interfejs i przepisać kod lub obniżyć.Odpowiedzi:
Przekształcenie twojej klasy w interfejs jest czymś, co powinieneś rozważyć tylko wtedy, gdy masz zamiar napisać inne implementacje tego interfejsu lub istnieje duża możliwość takiego uczynienia w przyszłości.
Może
SomeClass
iISomeClass
jest złym przykładem, ponieważ byłoby to jak posiadanieOracleObjectSerializer
klasy iIOracleObjectSerializer
interfejsu.Bardziej dokładnym przykładem byłoby coś takiego jak
OracleObjectSerializer
iIObjectSerializer
. Jedynym miejscem w twoim programie, w którym zależy Ci na implementacji, jest tworzenie instancji. Czasami jest to dalej oddzielane przy użyciu wzoru fabrycznego.Wszędzie w twoim programie nie powinno się
IObjectSerializer
troszczyć o to, jak to działa. Załóżmy przez chwilę, że oprócz tego masz takżeSQLServerObjectSerializer
implementacjęOracleObjectSerializer
. Załóżmy teraz, że musisz ustawić specjalną właściwość do ustawienia, a ta metoda jest obecna tylko w OracleObjectSerializer, a nie SQLServerObjectSerializer.Można to zrobić na dwa sposoby: niewłaściwy sposób i podejście oparte na zasadzie substytucji Liskowa .
Niepoprawny sposób
Niepoprawnym sposobem i samą instancją, o której mowa w książce, byłoby pobranie instancji
IObjectSerializer
i przesłanie jej,OracleObjectSerializer
a następnie wywołanie metodysetProperty
dostępnej tylko naOracleObjectSerializer
. Jest to złe, ponieważ nawet jeśli znasz instancjęOracleObjectSerializer
, wprowadzasz jeszcze jeden punkt w swoim programie, w którym chcesz wiedzieć, co to za implementacja. Kiedy ta implementacja ulegnie zmianie i przypuszczalnie stanie się to wcześniej czy później, jeśli masz wiele implementacji, najlepszym scenariuszem, będziesz musiał znaleźć wszystkie te miejsca i dokonać odpowiednich poprawek. W najgorszym scenariuszu rzutujeszIObjectSerializer
instancję na aOracleObjectSerializer
i pojawia się awaria środowiska wykonawczego podczas produkcji.Liskov Podstawowa zasada podejścia
Liskov powiedział, że nigdy nie powinieneś potrzebować metod takich jak
setProperty
w klasie implementacji, jak w przypadku mojej,OracleObjectSerializer
jeśli wykonano to poprawnie. Jeśli abstrakcyjne klasyOracleObjectSerializer
doIObjectSerializer
, należy objąć wszystkie metody niezbędne do korzystania z tej klasy, a jeśli nie, to coś jest nie tak z abstrakcją (próbuje zrobićDog
pracę klasy jakoIPerson
realizacji na przykład).Prawidłowym podejściem byłoby zapewnienie
setProperty
metodyIObjectSerializer
. Podobne metodySQLServerObjectSerializer
najlepiej działałyby dzięki tejsetProperty
metodzie. Co więcej, standaryzujesz nazwy właściwości poprzez miejsce, wEnum
którym każda implementacja tłumaczy ten wyliczenie na ekwiwalent dla własnej terminologii bazy danych.Mówiąc prosto, użycie a
ISomeClass
to tylko połowa tego. Nigdy nie powinieneś rzucać go poza metodę odpowiedzialną za jego tworzenie. Jest to prawie na pewno poważny błąd projektowy.źródło
IObjectSerializer
sięOracleObjectSerializer
dlatego, że „wie”, że jest to co to jest, to powinieneś być uczciwym wobec siebie (i co ważniejsze, z innymi, którzy mogą utrzymać ten kod, który może obejmować swoją przyszłość siebie) i używajOracleObjectSerializer
go od miejsca, w którym został utworzony, do miejsca, w którym jest używany. To sprawia, że jest to bardzo publiczne i jasne, że wprowadzasz zależność od konkretnej implementacji - a praca i brzydota związana z robieniem tego staje się sama w sobie silną wskazówką, że coś jest nie tak.Przyjęta odpowiedź jest poprawna i bardzo przydatna, ale chciałbym krótko omówić konkretnie wiersz kodu, o który pytałeś:
Ogólnie rzecz biorąc, to nie jest okropne. W miarę możliwości należy unikać:
Tam, gdzie twój kod ma interfejs zewnętrzny, ale wewnętrznie przekazuje go do konkretnej implementacji, „Ponieważ wiem, że będzie to tylko ta implementacja”. Nawet jeśli okazało się to prawdą, używając interfejsu i rzutując go na implementację, dobrowolnie rezygnujesz z bezpieczeństwa typu rzeczywistego tylko po to, abyś mógł udawać, że używasz abstrakcji. Jeśli ktoś miałby później popracować nad kodem i zobaczył metodę, która akceptuje parametr interfejsu, to założą, że jakakolwiek implementacja tego interfejsu jest prawidłową opcją do przekazania. wiersz po zapomnieniu, że określona metoda polega na tym, jakich parametrów potrzebuje. Jeśli kiedykolwiek poczujesz potrzebę przesyłania z interfejsu do konkretnej implementacji, wówczas interfejs, implementacja, lub kod, który się do nich odwołuje, jest niepoprawnie zaprojektowany i powinien ulec zmianie. Na przykład, jeśli metoda działa tylko wtedy, gdy przekazany argument jest określoną klasą, parametr powinien akceptować tylko tę klasę.
Teraz, patrząc wstecz na twoje wywołanie konstruktora
problemy z rzutowaniem tak naprawdę nie mają zastosowania. Nic z tego nie wydaje się być narażone na zewnątrz, więc nie wiąże się z tym żadne szczególne ryzyko. Zasadniczo ten wiersz kodu sam w sobie jest szczegółem implementacji, którego interfejsy zostały zaprojektowane tak, aby na początku były abstrakcyjne, więc zewnętrzny obserwator widziałby, że działa tak samo, niezależnie od tego, co robią. Jednak to również NIE ZYSKUJE z istnienia interfejsu. Twój
myClass
ma typISomeClass
, ale nie ma żadnego powodu, ponieważ zawsze ma przypisaną określoną implementację,SomeClass
. Istnieją pewne drobne potencjalne zalety, takie jak możliwość zamiany implementacji w kodzie przez zmianę tylko wywołania konstruktora lub ponowne przypisanie tej zmiennej później do innej implementacji, ale chyba że jest coś innego, co wymaga wpisania zmiennej do interfejsu zamiast implementacja tego wzorca sprawia, że Twój kod wygląda, jakby interfejsy były używane tylko przez rote, a nie z faktycznego zrozumienia korzyści płynących z interfejsów.źródło
Myślę, że łatwiej jest pokazać kod na złym przykładzie:
Problem polega na tym, że kiedy początkowo piszesz kod, prawdopodobnie istnieje tylko jedna implementacja tego interfejsu, więc rzutowanie będzie nadal działać, po prostu w przyszłości możesz zaimplementować inną klasę, a następnie (jak pokazuje mój przykład) spróbuj uzyskać dostęp do danych, które nie istnieją w używanym obiekcie.
źródło
Dla jasności zdefiniujmy casting.
Przesyłanie jest siłą przekształcające coś z jednego typu do drugiego. Typowym przykładem jest rzutowanie liczby zmiennoprzecinkowej na liczbę całkowitą. Podczas rzutowania można określić konkretną konwersję, ale domyślnie jest to po prostu reinterpretacja bitów.
Oto przykład przesyłania z tej strony dokumentacji Microsoft .
Ty mógłby zrobić to samo i oddać coś, implementuje interfejs do konkretnej implementacji tego interfejsu, ale nie powinno , bo to doprowadzi do błędu lub nieoczekiwanego zachowania, jeśli używana jest inna realizacja niż można się spodziewać.
źródło
Animal
/Giraffe
, jeśli miałbyś to zrobić,Animal a = (Animal)g;
bity zostałyby ponownie zinterpretowane (wszelkie dane specyficzne dla Giraffe byłyby interpretowane jako „nie będące częścią tego obiektu”).Moje 5 centów:
Wszystkie te przykłady są OK, ale nie są przykładami ze świata rzeczywistego i nie pokazały prawdziwych zamiarów świata.
Nie znam C #, więc dam abstrakcyjny przykład (mieszanka Java i C ++). Mam nadzieję, że to w porządku.
Załóżmy, że masz interfejs
iList
:Załóżmy teraz, że istnieje wiele implementacji:
Można pomyśleć o wielu różnych implementacjach.
Załóżmy teraz, że mamy następujący kod:
Wyraźnie pokazuje naszą intencję, z której chcemy skorzystać
iList
. Jasne, że nie jesteśmy już w stanie wykonywaćDynamicArrayList
określonych operacji, ale potrzebujemyiList
.Rozważ następujący kod:
Teraz nawet nie wiemy, czym jest wdrożenie. Ten ostatni przykład jest często używany w przetwarzaniu obrazu, gdy ładujesz jakiś plik z dysku i nie potrzebujesz jego typu (gif, jpeg, png, bmp ...), ale wszystko, czego potrzebujesz, to manipulacja obrazem (przerzucanie, skalę, zapisz jako png na końcu).
źródło
Masz interfejs ISomeClass i obiekt myObject, o którym nic nie wiesz ze swojego kodu poza tym, że zadeklarowano go do implementacji ISomeClass.
Masz klasę SomeClass, która, jak wiesz, implementuje interfejs ISomeClass. Wiesz o tym, ponieważ zadeklarowano wdrożenie ISomeClass lub sam zaimplementowałeś go, aby zaimplementować ISomeClass.
Co jest złego w przesyłaniu myClass do SomeClass? Dwie rzeczy są złe. Po pierwsze, naprawdę nie wiesz, że myClass to coś, co można przekonwertować na SomeClass (instancja SomeClass lub podklasa SomeClass), więc rzutowanie może się nie powieść. Po drugie, nie powinieneś tego robić. Powinieneś pracować z myClass zadeklarowaną jako iSomeClass i używać metod ISomeClass.
Punktem, w którym dostajesz obiekt SomeClass, jest wywołanie metody interfejsu. W pewnym momencie wywołujesz myClass.myMethod (), która jest zadeklarowana w interfejsie, ale ma implementację w SomeClass i oczywiście w wielu innych klasach implementujących ISomeClass. Jeśli wywołanie kończy się w kodzie SomeClass.myMethod, to wiesz, że self jest instancją SomeClass, i w tym momencie jest absolutnie w porządku i właściwie jest poprawne użycie go jako obiektu SomeClass. Oczywiście, jeśli faktycznie jest to instancja OtherClass, a nie SomeClass, to nie dojdziesz do kodu SomeClass.
źródło