is
Operator C # i operator Java instanceof
pozwalają na rozgałęzienie w interfejsie (lub, co bardziej nieokiełznane, jego klasie bazowej) zaimplementowanej instancji obiektu.
Czy właściwe jest używanie tej funkcji do rozgałęzień wysokiego poziomu w oparciu o możliwości interfejsu?
A może klasa podstawowa powinna zapewniać zmienne boolowskie, aby zapewnić interfejs opisujący możliwości obiektu?
Przykład:
if (account is IResetsPassword)
((IResetsPassword)account).ResetPassword();
else
Print("Not allowed to reset password with this account type!");
vs.
if (account.CanResetPassword)
((IResetsPassword)account).ResetPassword();
else
Print("Not allowed to reset password with this account type!");
Czy są jakieś pułapki związane z wykorzystaniem implementacji interfejsu do identyfikacji zdolności?
Ten przykład był właśnie taki, przykład. Zastanawiam się nad bardziej ogólnym zastosowaniem.
object-oriented
TheCatWhisperer
źródło
źródło
CanResetPassword
będzie to prawdą tylko wtedy, gdy coś się zaimplementujeIResetsPassword
. Teraz tworzysz właściwość określającą coś, co system typów powinien kontrolować (i ostatecznie zrobi to, gdy wykonasz rzut).Odpowiedzi:
Z zastrzeżeniem, że najlepiej jest unikać downcastingu, jeśli to możliwe, pierwsza forma jest preferowana z dwóch powodów:
is
pożary, wtedy spuszczony na pewno zadziała. W drugim formularzu musisz ręcznie upewnić się, że tylko obiekty, które implementują,IResetsPassword
zwracają wartość true dla tej właściwościNie oznacza to, że nie można nieco poprawić tych problemów. Możesz na przykład mieć klasę bazową z zestawem opublikowanych interfejsów, w których możesz sprawdzić włączenie. Ale tak naprawdę po prostu ręcznie wdrażasz część istniejącego systemu typów, który jest zbędny.
BTW, moją naturalną preferencją jest kompozycja nad dziedziczeniem, ale głównie w odniesieniu do stanu. Dziedziczenie interfejsu zwykle nie jest takie złe. Ponadto, jeśli zauważysz, że wdrażasz hierarchię typów biedaka, lepiej skorzystaj z już istniejącej, ponieważ kompilator ci pomoże.
źródło
(account as IResettable)?.ResetPassword();
lubvar resettable = account as IResettable; if(resettable != null) {resettable.ResetPassword()} else {....}
Możliwości obiektów nie powinny być w ogóle identyfikowane.
Od klienta korzystającego z obiektu nie powinno się wymagać żadnej wiedzy na temat jego działania. Klient powinien wiedzieć tylko, co może powiedzieć obiektowi. To, co robi obiekt, gdy zostanie mu powiedziane, nie stanowi problemu dla klientów.
Więc raczej niż
lub
rozważać
lub
ponieważ
account
już wie, czy to zadziała. Po co to pytać? Po prostu powiedz mu, co chcesz i pozwól mu zrobić wszystko, co chcesz.Wyobraź sobie, że zrobiono to w taki sposób, że klient nie dba o to, czy zadziałało, czy nie, ponieważ to jest inny problem. Zadaniem klientów była tylko próba. Zrobiło to już teraz i trzeba poradzić sobie z innymi sprawami. Ten styl ma wiele nazw, ale imię, które lubię najbardziej, to powiedzieć, nie pytaj .
To bardzo kuszące, gdy piszesz do klienta, że myślisz, że musisz wszystko śledzić, a więc przyciągnij do siebie wszystko, co myślisz, że musisz wiedzieć. Kiedy to robisz, obracasz przedmioty na lewą stronę. Doceń swoją ignorancję. Odepchnij szczegóły i pozwól obiektom sobie z nimi poradzić. Im mniej wiesz, tym lepiej.
źródło
if (!account.AttemptPasswordReset()) /* attempt failed */;
W ten sposób klient zna wynik, ale unika niektórych problemów z logiką decyzyjną w pytaniu. Jeśli próba jest asynchroniczna, można przekazać wywołanie zwrotne.account
podczas jej budowy. W tym momencie mogą pojawiać się wyskakujące okienka, rejestrowanie, wydruki i alarmy dźwiękowe. Zadaniem klienta jest powiedzenie „zrób to teraz”. Nie musi robić nic więcej. Nie musisz robić wszystkiego w jednym miejscu.tło
Dziedziczenie to potężne narzędzie, które służy celowi w programowaniu obiektowym. Jednak nie rozwiązuje każdego problemu elegancko: czasem inne rozwiązania są lepsze.
Jeśli przypomnisz sobie swoje wczesne zajęcia z informatyki (zakładając, że masz dyplom CS), możesz przypomnieć sobie profesora, który dał ci akapit z informacją o tym, co klient chce zrobić z oprogramowaniem. Twoim zadaniem jest przeczytanie akapitu, zidentyfikowanie aktorów i działań oraz przedstawienie przybliżonego szkicu klas i metod. Pojawią się tam nieprzyzwoite tropy, które wyglądają, jakby były ważne, ale nie są. Istnieje również bardzo realna możliwość błędnej interpretacji wymagań.
Jest to ważna umiejętność, którą popełniają nawet najbardziej doświadczeni z nas: prawidłowe identyfikowanie wymagań i tłumaczenie ich na języki maszynowe.
Twoje pytanie
Na podstawie tego, co napisałeś, myślę, że możesz nie rozumieć ogólnego przypadku opcjonalnych akcji, które mogą wykonywać klasy. Tak, wiem, że Twój kod jest tylko przykładem i jesteś zainteresowany ogólną sprawą. Wydaje się jednak, że chcesz wiedzieć, jak poradzić sobie z sytuacją, w której pewne podtypy obiektu mogą wykonać akcję, ale inne podtypy nie.
To, że obiekt taki jak
account
ma typ konta, nie oznacza, że przekłada się na typ w języku OO. „Pisanie” w ludzkim języku nie zawsze oznacza „klasę”. W kontekście konta „typ” może ściślej korelować z „zestawem uprawnień”. Chcesz użyć konta użytkownika do wykonania akcji, ale ta akcja może lub nie może być wykonana przez to konto. Zamiast korzystać z dziedziczenia, używałbym delegata lub tokena zabezpieczającego.Moje rozwiązanie
Rozważ klasę kont, która ma kilka opcjonalnych akcji, które może wykonać. Zamiast definiować „może wykonać akcję X” poprzez dziedziczenie, dlaczego konto nie zwróci obiektu delegowanego (resetowania hasła, osoby wysyłającej formularz itp.) Lub tokena dostępu?
Korzyść dla ostatniej opcji nie ma co, jeśli chcę wykorzystać moje poświadczenia konta, aby zresetować ktoś innego hasła?
System jest nie tylko bardziej elastyczny, nie tylko usunąłem odlewy typu, ale projekt jest bardziej wyrazisty . Mogę to przeczytać i łatwo zrozumieć, co robi i jak można z niej korzystać.
TL; DR: brzmi to jak problem XY . Zasadniczo w obliczu dwóch opcji, które nie są optymalne, warto cofnąć się o krok i zastanowić się: „co tak naprawdę chcę tutaj osiągnąć? Czy naprawdę powinienem zastanowić się, jak sprawić, by rzutowanie tekstu było mniej brzydkie, czy też powinienem poszukać sposobów usunąć całkowicie typecast? ”
źródło
account.getPasswordResetter().resetPassword();
.The benefit to the last option there is what if I want to use my account credentials to reset someone else's password?
.. łatwy. Tworzysz obiekt domeny Konto dla drugiego konta i używasz go. Klasy statyczne są problematyczne przy testowaniu jednostkowym (ta metoda z definicji będzie wymagała dostępu do pewnej pamięci, więc teraz nie można łatwo testować jednostkowo kodu, który dotyka tej klasy) i najlepiej tego unikać. Opcja zwrotu jakiegoś innego interfejsu jest często dobrym pomysłem, ale tak naprawdę nie rozwiązuje podstawowego problemu (właśnie przeniosłeś problem na inny interfejs).Bill Venner mówi, że twoje podejście jest absolutnie w porządku ; przejdź do sekcji zatytułowanej „Kiedy korzystać z instanceof”. Sprowadzasz się do określonego typu / interfejsu, który implementuje tylko to konkretne zachowanie, więc masz absolutną rację, jeśli chodzi o selektywność.
Jeśli jednak chcesz zastosować inne podejście, istnieje wiele sposobów na ogolenie jak.
Istnieje podejście polimorficzne; możesz argumentować, że wszystkie konta mają hasło, dlatego wszystkie konta powinny przynajmniej móc spróbować zresetować hasło. Oznacza to, że w podstawowej klasie konta powinniśmy mieć
resetPassword()
metodę, którą wszystkie konta wdrażają, ale na swój własny sposób. Pytanie brzmi, jak ta metoda powinna się zachowywać, gdy nie ma takiej możliwości.Może zwrócić pustkę i po cichu dokończyć, czy zresetuje hasło, czy nie, być może bierze odpowiedzialność za wewnętrzne wydrukowanie wiadomości, jeśli nie zresetuje hasła. Nie najlepszy pomysł.
Może zwrócić wartość logiczną wskazującą, czy reset się powiódł. Włączając tę wartość logiczną, możemy stwierdzić, że resetowanie hasła nie powiodło się.
Może zwrócić ciąg znaków wskazujący wynik próby zresetowania hasła. Ciąg może podawać więcej szczegółów na temat przyczyny niepowodzenia resetowania i może być wyprowadzany.
Może zwrócić obiekt ResetResult, który przekazuje więcej szczegółów i łączy wszystkie poprzednie elementy zwracane.
Może zwrócić pustkę i zamiast tego rzucić wyjątek, jeśli spróbujesz zresetować konto, które nie ma takiej możliwości (nie rób tego, ponieważ używanie wyjątków do normalnej kontroli przepływu jest złą praktyką z różnych powodów).
Posiadanie
canResetPassword()
metody dopasowania może nie wydawać się najgorszą rzeczą na świecie, ponieważ teoretycznie jest to funkcja statyczna wbudowana w klasę, gdy została napisana. Jest to wskazówka, dlaczego podejście metodyczne jest złym pomysłem, ponieważ sugeruje, że zdolność jest dynamiczna i żecanResetPassword()
może się zmienić, co stwarza dodatkową możliwość, że może się zmienić między pytaniem o pozwolenie a wykonaniem połączenia. Jak wspomniano w innym miejscu, powiedz zamiast pytać o pozwolenie.Kompozycja nad dziedziczeniem może być opcją: możesz mieć końcowe
passwordResetter
pole (lub równoważny getter) i równoważne klasy, dla których możesz sprawdzić wartość null przed wywołaniem jej. Chociaż działa to trochę jak prośba o pozwolenie, ostateczność może uniknąć jakiejkolwiek wywnioskowanej natury dynamicznej.Możesz pomyśleć o przekazaniu funkcjonalności na własną klasę, która może wziąć pod uwagę parametr jako parametr i działać zgodnie z nim (np.
resetter.reset(account)
), Chociaż często jest to również zła praktyka (powszechną analogią jest sklepikarz sięgający do portfela w celu uzyskania gotówki).W językach, w których jest to możliwe, możesz używać miksów lub cech, ale ostatecznie kończysz w miejscu, w którym zacząłeś sprawdzać istnienie tych możliwości.
źródło
W tym konkretnym przykładzie „ można zresetować hasło ” zalecam użycie kompozycji zamiast dziedziczenia (w tym przypadku dziedziczenie interfejsu / kontraktu). Ponieważ robiąc to:
Natychmiast określasz (w czasie kompilacji), że twoja klasa „ może zresetować hasło ”. Ale jeśli w twoim scenariuszu obecność zdolności jest warunkowa i zależy od innych rzeczy, nie możesz już więcej określać rzeczy w czasie kompilacji. Następnie sugeruję zrobienie tego:
Teraz w czasie wykonywania możesz sprawdzić, czy
myFoo.passwordResetter != null
przed wykonaniem tej operacji. Jeśli chcesz oddzielić rzeczy jeszcze bardziej (i planujesz dodać o wiele więcej możliwości), możesz:AKTUALIZACJA
Po przeczytaniu kilku innych odpowiedzi i komentarzy z OP zrozumiałem, że pytanie nie dotyczy rozwiązania kompozycyjnego. Myślę więc, że pytanie dotyczy tego, jak lepiej zidentyfikować możliwości obiektów w ogóle, w scenariuszu takim jak poniżej:
Teraz spróbuję przeanalizować wszystkie wymienione opcje:
Drugi przykład PO:
Powiedzmy, że ten kod otrzymuje podstawową klasę konta (np .:
BaseAccount
w moim przykładzie); jest to złe, ponieważ wstawia booleany do klasy podstawowej, zanieczyszczając go kodem, który w ogóle nie ma sensu.Pierwszy przykład PO:
Aby odpowiedzieć na to pytanie, jest to bardziej odpowiednie niż poprzednia opcja, ale w zależności od implementacji złamie zasadę L bryły i prawdopodobnie takie kontrole zostaną rozłożone w kodzie i utrudnią dalszą konserwację.
Anser CandiedOrange:
Jeśli ta
ResetPassword
metoda jest wstawiona doBaseAccount
klasy, wówczas również zanieczyszcza klasę podstawową nieodpowiednim kodem, jak w drugim przykładzie OPOdpowiedź bałwana:
To dobre rozwiązanie, ale uważa, że możliwości są dynamiczne (i mogą się zmieniać z czasem). Jednak po przeczytaniu kilku komentarzy z OP myślę, że tutaj mowa jest o polimorfizmie i klasach zdefiniowanych statycznie (chociaż przykład kont intuicyjnie wskazuje na scenariusz dynamiczny). EG: w tym
AccountManager
przykładzie sprawdzeniem uprawnień byłyby zapytania do DB; w pytaniu PO kontrole są próbami rzucenia obiektów.Kolejna sugestia ode mnie:
Użyj wzorca metody szablonu dla rozgałęzień wysokiego poziomu. Wspomniana hierarchia klas jest zachowana bez zmian; tworzymy tylko bardziej odpowiednie procedury obsługi obiektów, aby uniknąć rzutowania i nieodpowiednich właściwości / metod zanieczyszczających klasę podstawową.
Można powiązać operację z odpowiednią klasą konta, używając słownika / tablicy hashtable, lub wykonać operacje w czasie wykonywania przy użyciu metod rozszerzenia, użyć
dynamic
słowa kluczowego lub jako ostatnią opcję użyć tylko jednej rzutowania , aby przekazać obiekt konta do operacji (w w tym przypadku liczba rzutów jest tylko jedna, na początku operacji).źródło
Żadna z przedstawionych opcji nie jest dobra. Jeśli piszesz, jeśli oświadczenia dotyczące typu obiektu, najprawdopodobniej źle robisz OO (są wyjątki, to nie jest jeden). Oto prosta odpowiedź OO na twoje pytanie (może nie być poprawna C #):
Zmodyfikowałem ten przykład, aby pasował do tego, co robił oryginalny kod. Na podstawie niektórych komentarzy wydaje się, że ludzie rozłączają się, czy jest to „właściwy” konkretny kod tutaj. Nie o to chodzi w tym przykładzie. Całe przeciążenie polimorficzne polega przede wszystkim na warunkowym wykonaniu różnych implementacji logiki w zależności od typu obiektu. W obu przykładach robisz ręczne zacinanie się w tym, co oferuje Twój język. W skrócie możesz pozbyć się podtypów i ustawić możliwość resetowania jako właściwość logiczną typu Konta (ignorując inne funkcje podtypów).
Bez szerszego spojrzenia na projekt nie można stwierdzić, czy jest to dobre rozwiązanie dla konkretnego systemu. Jest to proste i jeśli zadziała to, co robisz, prawdopodobnie nigdy nie będziesz musiał więcej o tym myśleć, chyba że ktoś nie sprawdzi CanResetPassword () przed wywołaniem ResetPassword (). Możesz także zwrócić wartość logiczną lub zakończyć się niepowodzeniem w trybie cichym (niezalecane). To naprawdę zależy od specyfiki projektu.
źródło
NotResetable
. „może konto ma więcej funkcji niż możliwość resetowania” Spodziewałbym się tego, ale wydaje się, że nie jest to ważne dla tego pytania.Odpowiedź
Moja lekko uparta opinia na twoje pytanie brzmi:
Uzupełnienie:
Pnący
Widzę, że nie dodałeś
c#
tagu, ale pytasz w ogólnymobject-oriented
kontekście.To, co opisujesz, to technika statycznych / silnych języków pisanych, a nie specyfika „OO” w ogóle. Twój problem wynika między innymi z faktu, że nie możesz wywoływać metod w tych językach bez posiadania wystarczająco wąskiego typu w czasie kompilacji (poprzez definicję zmiennej, definicję parametru metody / wartości zwracanej lub jawną obsadę). W przeszłości robiłem oba wasze warianty, w tego rodzaju językach, i oba wydają mi się teraz brzydkie.
W dynamicznie wpisywanych językach OO ten problem nie występuje z powodu koncepcji zwanej „pisaniem kaczych”. Krótko mówiąc, oznacza to, że zasadniczo nie deklarujesz nigdzie typu obiektu (z wyjątkiem tworzenia). Wysyłasz wiadomości do obiektów, a obiekty obsługują te wiadomości. Nie obchodzi Cię, czy obiekt rzeczywiście ma metodę o tej nazwie. Niektóre języki mają nawet ogólną
method_missing
metodę „ catch-all” , co czyni ją jeszcze bardziej elastyczną.W takim języku (na przykład Ruby) kod staje się:
Kropka.
account
Będzie albo zresetować hasło, rzucać „odrzuć” wyjątek, albo rzucać „nie wiem, jak obsługiwać«reset_password»” wyjątek.Jeśli potrzebujesz jawnie rozgałęzić się z jakiegokolwiek powodu, nadal robiłbyś to dla możliwości, a nie dla klasy:
(Gdzie
can?
jest tylko metoda sprawdzająca, czy obiekt może wykonać jakąś metodę, bez jej wywoływania).Zwróć uwagę, że chociaż moje osobiste preferencje dotyczą obecnie dynamicznie pisanych języków, są to tylko osobiste preferencje. Języki o typie statycznym również się do nich odnoszą, więc proszę, nie przejmuj się tym bełkotem przeciwko językom o typie statycznym ...
źródło
Wygląda na to, że twoje opcje pochodzą od różnych sił motywacyjnych. Pierwszy wydaje się być włamaniem stosowanym z powodu złego modelowania obiektów. Wykazało, że twoje abstrakcje są błędne, ponieważ łamią polimorfizm. Bardziej prawdopodobne niż to, że łamie zasadę otwartego zamknięcia , co skutkuje ściślejszym sprzężeniem.
Twoja druga opcja może być dobra z punktu widzenia OOP, ale rzutowanie tekstu myli mnie. Jeśli twoje konto pozwala zresetować hasło, dlaczego więc potrzebujesz rzutowania? Więc zakładając, że twoja
CanResetPassword
klauzula reprezentuje pewne podstawowe wymagania biznesowe, które są częścią abstrakcji konta, twoja druga opcja jest OK.źródło