Mam klasy bazowej, Base
. Ma dwie podklasy Sub1
i Sub2
. Każda podklasa ma kilka dodatkowych metod. Na przykład Sub1
ma Sandwich makeASandwich(Ingredients... ingredients)
i Sub2
ma boolean contactAliens(Frequency onFrequency)
.
Ponieważ metody te przyjmują różne parametry i robią zupełnie różne rzeczy, są całkowicie niezgodne i nie mogę po prostu użyć polimorfizmu do rozwiązania tego problemu.
Base
zapewnia większość funkcji, a ja mam dużą kolekcję Base
obiektów. Jednak wszystkie Base
obiekty są albo a Sub1
albo a Sub2
, i czasami muszę wiedzieć, które to są.
Wydaje się, że złym pomysłem jest wykonanie następujących czynności:
for (Base base : bases) {
if (base instanceof Sub1) {
((Sub1) base).makeASandwich(getRandomIngredients());
// ... etc.
} else { // must be Sub2
((Sub2) base).contactAliens(getFrequency());
// ... etc.
}
}
Więc wymyśliłem strategię, aby tego uniknąć bez rzucania. Base
teraz ma te metody:
boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();
I oczywiście Sub1
implementuje te metody jako
boolean isSub1() { return true; }
Sub1 asSub1(); { return this; }
Sub2 asSub2(); { throw new IllegalStateException(); }
I Sub2
wdraża je w odwrotny sposób.
Niestety, teraz Sub1
i Sub2
mają te metody we własnym API. Mogę to zrobić na przykład na Sub1
.
/** no need to use this if object is known to be Sub1 */
@Deprecated
boolean isSub1() { return true; }
/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub1 asSub1(); { return this; }
/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub2 asSub2(); { throw new IllegalStateException(); }
W ten sposób, jeśli wiadomo, że obiekt jest tylko a Base
, metody te są nieaktualne i można ich użyć do „rzutowania” na inny typ, aby móc wywołać na nim metody podklasy. W pewnym sensie wydaje mi się to eleganckie, ale z drugiej strony nadużywam przestarzałych adnotacji jako sposobu na „usunięcie” metod z klasy.
Ponieważ Sub1
instancja tak naprawdę jest bazą, sensownym jest użycie dziedziczenia zamiast enkapsulacji. Czy to, co robię dobrze? Czy istnieje lepszy sposób na rozwiązanie tego problemu?
źródło
instanceof
, w sposób, który wymaga dużo pisania, jest podatny na błędy i utrudnia dodawanie kolejnych podklas.Sub1
iSub2
nie można ich używać zamiennie, to dlaczego traktujesz je jako takie? Dlaczego nie śledzić osobno swoich „producentów kanapek” i „osób kontaktujących się z kosmitami”?Odpowiedzi:
Dodawanie funkcji do klasy podstawowej nie zawsze ma sens, jak sugerowano w niektórych innych odpowiedziach. Dodanie zbyt wielu funkcji specjalnych przypadków może spowodować powiązanie ze sobą niezwiązanych ze sobą komponentów.
Na przykład mogę mieć
Animal
klasę zCat
iDog
komponentami . Jeśli chcę móc je wydrukować lub pokazać w GUI, dodawanierenderToGUI(...)
isendToPrinter(...)
do klasy podstawowej może być dla mnie przesadą .Podejście, które stosujesz, używając sprawdzania typu i rzutowania, jest kruche - ale przynajmniej rozdziela obawy.
Jeśli jednak często przeprowadzasz tego rodzaju kontrole / rzuty, jedną z opcji jest zaimplementowanie wzorca odwiedzającego / podwójnej wysyłki. Wygląda to mniej więcej tak:
Teraz twój kod staje się
Główną zaletą jest to, że jeśli dodasz podklasę, otrzymasz błędy kompilacji zamiast cichych braków spraw. Nowa klasa odwiedzających staje się również przyjemnym celem przyciągania funkcji. Na przykład sensowne może być poruszanie się
getRandomIngredients()
doActOnBase
.Możesz także wyodrębnić logikę zapętlenia: na przykład powyższy fragment może się stać
Jeszcze trochę masowania i korzystania z lambda i streamingu Java 8 pozwoli ci się dostać
Który IMO jest tak schludny i zwięzły, jak tylko możesz.
Oto bardziej kompletny przykład Java 8:
źródło
Sub3
. Czy gość nie ma dokładnie takiego samego problemu jakinstanceof
? Teraz musisz dodaćonSub3
do odwiedzającego, a jeśli zapomnisz, pęka.onSub3
do interfejsu gościa pojawia się błąd kompilacji w każdej lokalizacji, w której tworzysz nowego Odwiedzającego, który nie został jeszcze zaktualizowany. W przeciwieństwie do tego, włączenie typu w najlepszym wypadku wygeneruje błąd w czasie wykonywania - który może być trudniejszy do uruchomienia.Z mojej perspektywy: twój projekt jest zły .
Przetłumaczone na język naturalny, mówisz, co następuje:
Biorąc pod uwagę, że mamy
animals
, sącats
ifish
.animals
mają właściwości, które są wspólne dlacats
ifish
. Ale to nie wystarczy: istnieją pewne właściwości, które różnią sięcat
od nichfish
, dlatego należy podklasę.Teraz masz problem polegający na tym, że zapomniałeś modelować ruch . W porządku. To stosunkowo łatwe:
Ale to zły projekt. Prawidłowy sposób to:
Gdzie
move
byłoby wspólne zachowanie wdrażane inaczej przez każde zwierzę.Oznacza to: twój projekt jest zepsuty.
Moja rekomendacja: Refactor
Base
,Sub1
iSub2
.źródło
.move()
albo.attack()
a właściwie streszczeniethat
część - zamiast.swim()
,.fly()
,.slide()
,.hop()
,.jump()
,.squirm()
,.phone_home()
, itd. Jak byłaby prawidłowo? Nieznany bez lepszych przykładów ... ale ogólnie poprawna odpowiedź - chyba że przykładowy kod i więcej szczegółów sugerują inaczej.move
vsfly
/run
/ etc, które można wyjaśnić w jednym zdaniu jako: Powinieneś powiedzieć obiektowi, co ma robić, a nie jak to zrobić. Jeśli chodzi o akcesoria, jak wspominasz, bardziej przypomina to prawdziwy przypadek w komentarzu tutaj, powinieneś zadawać pytania obiektowe, a nie sprawdzać jego stan.Trochę trudno jest wyobrazić sobie sytuację, w której masz grupę rzeczy i chcesz, aby zrobili kanapkę lub skontaktowali się z kosmitami. W większości przypadków, w których znajdziesz taki rzut, będziesz działał z jednym typem - np. W clang filtrujesz zestaw węzłów dla deklaracji, w których getAsFunction zwraca wartość niż null, zamiast robić coś innego dla każdego węzła na liście.
Może się zdarzyć, że potrzebujesz sekwencji działań i tak naprawdę nie jest istotne, że obiekty wykonujące akcję są powiązane.
Zamiast listy
Base
pracuj nad listą akcjigdzie
Możesz, jeśli to konieczne, Base zaimplementować metodę zwracającą akcję, lub cokolwiek innego, co musisz wybrać akcję z instancji na liście podstawowej (ponieważ mówisz, że nie możesz użyć polimorfizmu, więc prawdopodobnie akcja, którą musisz podjąć nie jest funkcją klasy, ale jakąś inną właściwością baz; w przeciwnym razie po prostu podasz Base metodzie act (Context)
źródło
Context
obiektu tylko pogarsza sytuację. Równie dobrze mógłbym przekazywaćObject
metody, rzucać je i mieć nadzieję, że są odpowiedniego typu.Context
nie musi to być nowa klasa ani nawet interfejs. Zwykle kontekstem jest tylko osoba dzwoniąca, więc połączenia są w formieaction.act(this)
. JeśligetFrequency()
igetRandomIngredients()
są metodami statycznymi, możesz nawet nie potrzebować kontekstu. Tak chciałbym wdrożyć powiedzmy, kolejkę zadań, której twój problem brzmi okropnie.A jeśli masz podklasy, które implementują jeden lub więcej interfejsów, które określają, co mogą zrobić? Coś takiego:
Twoje zajęcia będą wyglądać następująco:
Twoja pętla for stanie się:
Dwie główne zalety tego podejścia:
PS: Przepraszam za nazwy interfejsów, nie mogłem wymyślić nic fajniejszego w tym konkretnym momencie: D.
źródło
Podejście to może być dobre w przypadkach, w których prawie każdy typ w rodzinie będzie albo bezpośrednio użyteczny jako implementacja jakiegoś interfejsu, który spełnia pewne kryterium, albo może zostać wykorzystany do stworzenia implementacji tego interfejsu. Wbudowane typy kolekcji skorzystałyby z tego wzorca IMHO, ale ponieważ nie robią tego na przykład, wymyślę interfejs kolekcji
BunchOfThings<T>
.Niektóre implementacje
BunchOfThings
są zmienne; niektóre nie są. W wielu przypadkach obiekt Fred może chcieć trzymać coś, co może wykorzystać jakoBunchOfThings
i wie, że nic innego niż Fred nie będzie w stanie go zmodyfikować. Wymóg ten można spełnić na dwa sposoby:Fred wie, że zawiera jedyne odniesienia do tego
BunchOfThings
, i że żadneBunchOfThings
wewnętrzne odniesienie nie istnieje w całym wszechświecie. Jeśli nikt inny nie ma odniesienia doBunchOfThings
wewnętrznych elementów, nikt inny nie będzie mógł go zmodyfikować, więc ograniczenie zostanie spełnione.Ani
BunchOfThings
, ani żaden z jego elementów wewnętrznych, do których istnieją odniesienia zewnętrzne, nie może być modyfikowany w jakikolwiek sposób. Jeśli absolutnie nikt nie może zmodyfikować aBunchOfThings
, ograniczenie zostanie spełnione.Jednym ze sposobów spełnienia tego ograniczenia byłoby bezwarunkowe skopiowanie dowolnego otrzymanego obiektu (rekurencyjne przetwarzanie zagnieżdżonych komponentów). Innym byłoby sprawdzenie, czy otrzymany obiekt obiecuje niezmienność, a jeśli nie, wykonaj jego kopię i postępuj podobnie z dowolnymi zagnieżdżonymi komponentami. Alternatywą, która może być czystsza od drugiej i szybsza od pierwszej, jest zaoferowanie
AsImmutable
metody, która prosi obiekt o wykonanie niezmiennej kopii samego siebie (używającAsImmutable
wszystkich zagnieżdżonych komponentów, które go obsługują).Można również zapewnić pokrewne metody
asDetached
(do użycia, gdy kod odbiera obiekt i nie wie, czy będzie chciał go mutować, w którym to przypadku obiekt zmienny powinien zostać zastąpiony nowym obiektem zmiennym, ale obiekt niezmienny może zostać zachowany jak jest),asMutable
(w przypadkach, gdy obiekt wie, że będzie przechowywał obiekt wcześniej zwróconyasDetached
, tj. albo nieudostępnione odwołanie do obiektu zmiennego lub współdzielone odniesienie do obiektu zmiennego), orazasNewMutable
(w przypadkach, gdy kod odbiera z zewnątrz i wie, że będzie chciał zmutować kopię danych w nim zawartych - jeśli przychodzące dane są modyfikowalne, nie ma powodu, aby zacząć od stworzenia niezmiennej kopii, która zostanie natychmiast wykorzystana do stworzenia mutowalnej kopii, a następnie porzucona).Zauważ, że chociaż
asXX
metody mogą zwracać nieco inne typy, ich prawdziwą rolą jest zapewnienie, że zwracane obiekty będą spełniać potrzeby programu.źródło
Ignorując kwestię tego, czy masz dobry projekt, czy nie, i zakładając, że jest dobry lub przynajmniej do zaakceptowania, chciałbym rozważyć możliwości podklas, a nie typ.
Dlatego albo:
Przenieś trochę wiedzy o istnieniu kanapek i kosmitów do klasy podstawowej, nawet jeśli wiesz, że niektóre przypadki klasy podstawowej nie mogą tego zrobić. Zaimplementuj go w klasie podstawowej, aby zgłaszać wyjątki i zmień kod na:
Następnie jedna lub obie podklasy zastępują
canMakeASandwich()
i tylko jedna implementuje każdą zmakeASandwich()
icontactAliens()
.Użyj interfejsów, a nie konkretnych podklas, aby wykryć możliwości danego typu. Pozostaw klasę podstawową w spokoju i zmień kod na:
lub ewentualnie (i możesz zignorować tę opcję, jeśli nie pasuje ona do Twojego stylu lub jeśli chodzi o styl Java, który uważasz za rozsądny):
Osobiście zazwyczaj nie lubię tego drugiego sposobu wychwytywania połowicznie oczekiwanego wyjątku, ze względu na ryzyko niewłaściwego wyłapania
ClassCastException
wychodzenia zgetRandomIngredients
lubmakeASandwich
, ale YMMV.źródło
<
jest unikanie łapaniaArrayIndexOutOfBoundsException
, a niektórzy decydują się to zrobić. Brak rozliczania się ze smaku ;-) Zasadniczo moim „problemem” jest to, że pracuję w Pythonie, gdzie preferowanym stylem jest zwykle to, że złapanie dowolnego wyjątku jest lepsze niż wcześniejsze testowanie, czy wystąpi wyjątek, bez względu na to, jak prosty byłby test z wyprzedzeniem . Być może nie warto o tym wspominać w Javie.Tutaj mamy interesujący przypadek klasy podstawowej, która sprowadza się do własnych klas pochodnych. Wiemy dobrze, że zwykle jest to złe, ale jeśli chcemy powiedzieć, że znaleźliśmy dobry powód, spójrzmy i zobaczmy, jakie mogą być ograniczenia:
Jeśli 4, to mamy: 5. Polityka klasy pochodnej jest zawsze pod taką samą kontrolą polityczną jak polityka klasy bazowej.
Oba 2 i 5 bezpośrednio oznaczają, że możemy wyliczyć wszystkie klasy pochodne, co oznacza, że nie powinny istnieć żadne zewnętrzne klasy pochodne.
Ale o to chodzi. Jeśli wszystkie są twoje, możesz zastąpić if abstrakcją, która jest wirtualnym wywołaniem metody (nawet jeśli jest to nonsensowne) i pozbyć się if i samozastosowania. Dlatego nie rób tego. Dostępny jest lepszy projekt.
źródło
Umieść metodę abstrakcyjną w klasie bazowej, która robi jedną rzecz w Sub1, a drugą w Sub2, i wywołaj tę metodę abstrakcyjną w swoich mieszanych pętlach.
Zauważ, że może to wymagać innych zmian w zależności od sposobu zdefiniowania getRandomIngredients i getFrequency. Ale szczerze mówiąc, wewnątrz klasy, która kontaktuje się z kosmitami, jest prawdopodobnie lepsze miejsce do zdefiniowania getFrequency.
Nawiasem mówiąc, twoje metody asSub1 i asSub2 są zdecydowanie złą praktyką. Jeśli masz zamiar to zrobić, zrób to za pomocą castingu. Te metody nie dają nic, czego nie da casting.
źródło
Można wcisnąć oba
makeASandwich
icontactAliens
do klasy bazowej, a następnie wdrożyć je z manekina wdrożeń. Ponieważ zwracane typy / argumenty nie mogą zostać rozstrzygnięte na wspólną klasę podstawową.Istnieją oczywiste wady tego rodzaju rzeczy, takie jak to, co to oznacza dla kontraktu metody i co można, a czego nie można wnioskować na temat kontekstu wyniku. Nie możesz myśleć, ponieważ nie udało mi się zrobić kanapki, składniki zostały zużyte podczas próby itp.
źródło
To, co robisz, jest całkowicie uzasadnione. Nie zwracajcie uwagi na naysayers, którzy jedynie powtarzają dogmat, ponieważ czytają je w niektórych książkach. Dogma nie ma miejsca w inżynierii.
Kilka razy korzystałem z tego samego mechanizmu i mogę śmiało powiedzieć, że środowisko wykonawcze java mogło zrobić to samo w co najmniej jednym miejscu, o którym mogę myśleć, zwiększając w ten sposób wydajność, użyteczność i czytelność kodu, który używa tego.
Weźmy na przykład
java.lang.reflect.Member
, która jest podstawąjava.lang.reflect.Field
ijava.lang.reflect.Method
. (Rzeczywista hierarchia jest nieco bardziej skomplikowana, ale to nie ma znaczenia.) Pola i metody to bardzo różne zwierzęta: jedna ma wartość, którą można uzyskać lub ustawić, a druga nie ma takiej rzeczy, ale można ją wywołać za pomocą wiele parametrów i może zwrócić wartość. Zatem pola i metody są członkami, ale rzeczy, które możesz z nimi zrobić, różnią się od siebie tak samo, jak robienie kanapek kontra kontaktowanie się z kosmitami.Teraz, pisząc kod wykorzystujący refleksję, bardzo często mamy
Member
w rękach i wiemy, że jest toMethod
albo aField
, albo (rzadko, coś innego), a jednak musimy robić wszystko, co żmudne,instanceof
aby dokładnie wymyślić co to jest, a następnie musimy go rzucić, aby uzyskać odpowiednie odniesienie do niego. (I to jest nie tylko żmudne, ale również nie działa zbyt dobrze.)Method
Klasa mogłaby bardzo łatwo zaimplementować opisany przez Ciebie wzorzec, ułatwiając w ten sposób życie tysiącom programistów.Oczywiście ta technika jest wykonalna tylko w małych, dobrze zdefiniowanych hierarchiach ściśle powiązanych klas, nad którymi masz (i zawsze będziesz mieć) kontrolę na poziomie źródła: nie chcesz robić takich rzeczy, jeśli twoja hierarchia klas jest mogą zostać przedłużone przez osoby, które nie mają wolności, aby refaktoryzować klasę podstawową.
Oto, co zrobiłem, różni się od tego, co zrobiłeś:
Klasa podstawowa zapewnia domyślną implementację dla całej
asDerivedClass()
rodziny metod, zwracając każdą z nichnull
.Każda klasa pochodna zastępuje tylko jedną z
asDerivedClass()
metod, zwracającthis
zamiastnull
. Nie zastępuje żadnej z pozostałych, ani nie chce nic o nich wiedzieć. Więc nieIllegalStateException
są wyrzucane.Klasa podstawowa zapewnia również
final
implementacje dla całejisDerivedClass()
rodziny metod, kodowanych w następujący sposób: Wreturn asDerivedClass() != null;
ten sposób liczba metod, które muszą zostać zastąpione przez klasy pochodne, jest zminimalizowana.Nie korzystałem
@Deprecated
z tego mechanizmu, ponieważ o tym nie myślałem. Teraz, kiedy mi podsunąłeś pomysł, wykorzystam go, dzięki!C # ma powiązany mechanizm wbudowany za pomocą
as
słowa kluczowego. W języku C # można powiedziećDerivedClass derivedInstance = baseInstance as DerivedClass
, a dostaniesz odniesienie doDerivedClass
jeślibaseInstance
był z tej klasy, lubnull
jeśli nie było. To (teoretycznie) działa lepiej niżis
po nim rzutowanie (is
jest to oczywiście lepiej nazwane słowo kluczowe C # dlainstanceof
,), ale mechanizm niestandardowy, który tworzymy ręcznie, działa jeszcze lepiej: parainstanceof
operacji Java i rzutowania również w Javie ponieważas
operator C # nie działa tak szybko, jak pojedyncze wirtualne wywołanie metody naszego niestandardowego podejścia.Niniejszym proponuję, aby tę technikę uznać za wzór i znaleźć dla niej ładne imię.
Rany, dzięki za opinie!
Podsumowanie kontrowersji, aby uchronić Cię przed trudem czytania komentarzy:
Ludzie sprzeciwiają się temu, że oryginalny projekt był błędny, co oznacza, że nigdy nie powinieneś mieć znacznie różnych klas wywodzących się ze wspólnej klasy bazowej, lub nawet jeśli tak, kod korzystający z takiej hierarchii nigdy nie powinien mieć możliwości referencja podstawowa i potrzeba ustalenia klasy pochodnej. Dlatego twierdzą, że mechanizm samonastawny zaproponowany przez to pytanie i moją odpowiedź, który poprawia wykorzystanie oryginalnego projektu, nigdy nie powinien był być konieczny. (Tak naprawdę nie mówią nic o samym mechanizmie samonastawnym, narzekają tylko na naturę projektów, do których mechanizm ma być zastosowany.)
Jednak w powyższym przykładzie mam już pokazały, że twórcy Javy rzeczywiście wybrać dokładnie taki projekt dla
java.lang.reflect.Member
,Field
,Method
hierarchii, aw komentarzach poniżej ja również pokazać, że twórcy # starcie C niezależnie przybył na równoważny projekt dlaSystem.Reflection.MemberInfo
,FieldInfo
,MethodInfo
hierarchii. Są to więc dwa różne scenariusze ze świata rzeczywistego, które siedzą tuż pod nosem wszystkich i które mają możliwe do udowodnienia wykonalne rozwiązania wykorzystujące dokładnie takie projekty.Do tego sprowadzają się wszystkie poniższe komentarze. Trudno wspomnieć o mechanizmie samonastawnym.
źródło
java.lang.reflection.Member
hierarchii. Twórcy środowiska wykonawczego java dostali się w tego typu sytuacje. Nie sądzę, że powiedziałbyś, że zaprojektowali się w pudełku, ani nie obwiniasz ich za nieprzestrzeganie najlepszych praktyk? Chodzi mi o to, że gdy już znajdziesz się w takiej sytuacji, ten mechanizm jest dobrym sposobem na poprawę sytuacji.Member
interfejs, który służy jedynie do sprawdzenia kwalifikatorów dostępu. Zazwyczaj dostajesz listę metod lub właściwości i używasz ich bezpośrednio (bez ich mieszania, więc nieList<Member>
pojawia się), więc nie musisz rzucać. Uwaga:Class
nie zapewniagetMembers()
metody. Oczywiście, nieświadomy programista mógłby stworzyćList<Member>
, ale nie miałoby to większego sensu, jak zrobienie goList<Object>
i dodanie częściInteger
lubString
do niego.null
zamiast wyjątku nie poprawia sytuacji. Wszystkie pozostałe są równe, odwiedzający jest bezpieczniejszy podczas wykonywania tej samej pracy.