Czy można mieć obiekty, które się rzucają, nawet jeśli zanieczyszczają interfejs API ich podklas?

33

Mam klasy bazowej, Base. Ma dwie podklasy Sub1i Sub2. Każda podklasa ma kilka dodatkowych metod. Na przykład Sub1ma Sandwich makeASandwich(Ingredients... ingredients)i Sub2ma 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.

Basezapewnia większość funkcji, a ja mam dużą kolekcję Baseobiektów. Jednak wszystkie Baseobiekty są albo a Sub1albo 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. Baseteraz ma te metody:

boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();

I oczywiście Sub1implementuje te metody jako

boolean isSub1() { return true; }
Sub1 asSub1();   { return this; }
Sub2 asSub2();   { throw new IllegalStateException(); }

I Sub2wdraża je w odwrotny sposób.

Niestety, teraz Sub1i Sub2mają 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ż Sub1instancja 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?

łamacz kodów
źródło
12
Wszystkie 3 klasy muszą się teraz znać. Dodanie Sub3 wymagałoby wielu zmian w kodzie, a dodanie Sub10 byłoby wręcz bolesne
Dan Pichelman
15
Bardzo by pomógł, gdybyś dał nam prawdziwy kod. Są sytuacje, w których właściwe jest podejmowanie decyzji w oparciu o konkretną klasę czegoś, ale nie można stwierdzić, czy jesteś uzasadniony w tym, co robisz z tak wymyślonymi przykładami. Niezależnie od tego, co jest warte, czego chcesz, to Odwiedzający lub oznaczony związek .
Doval
1
Przepraszam, pomyślałem, że dzięki temu łatwiej będzie podać uproszczone przykłady. Może opublikuję nowe pytanie z szerszym zakresem tego, co chcę zrobić.
codebreaker
12
Właśnie reimplementujesz rzutowanie i instanceof, w sposób, który wymaga dużo pisania, jest podatny na błędy i utrudnia dodawanie kolejnych podklas.
user253751
5
Jeśli Sub1i Sub2nie 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”?
Pieter Witvoet,

Odpowiedzi:

27

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ć Animalklasę z CatiDog komponentami . Jeśli chcę móc je wydrukować lub pokazać w GUI, dodawanie renderToGUI(...)i sendToPrinter(...)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:

public abstract class Base {
  ...
  abstract void visit( BaseVisitor visitor );
}

public class Sub1 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub1(this); }
}

public class Sub2 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub2(this); }
}

public interface BaseVisitor {
   void onSub1(Sub1 that);
   void onSub2(Sub2 that);
}

Teraz twój kod staje się

public class ActOnBase implements BaseVisitor {
    void onSub1(Sub1 that) {
       that.makeASandwich(getRandomIngredients())
    }

    void onSub2(Sub2 that) {
       that.contactAliens(getFrequency());
    }
}

BaseVisitor visitor = new ActOnBase();
for (Base base : bases) {
    base.visit(visitor);
}

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() do ActOnBase.

Możesz także wyodrębnić logikę zapętlenia: na przykład powyższy fragment może się stać

BaseVisitor.applyToArray(bases, new ActOnBase() );

Jeszcze trochę masowania i korzystania z lambda i streamingu Java 8 pozwoli ci się dostać

bases.stream()
     .forEach( BaseVisitor.forEach(
       Sub1 that -> that.makeASandwich(getRandomIngredients()),
       Sub2 that -> that.contactAliens(getFrequency())
     ));

Który IMO jest tak schludny i zwięzły, jak tylko możesz.

Oto bardziej kompletny przykład Java 8:

public static abstract class Base {
    abstract void visit( BaseVisitor visitor );
}

public static class Sub1 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub1(this); }

    void makeASandwich() {
        System.out.println("making a sandwich");
    }
}

public static class Sub2 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub2(this); }

    void contactAliens() {
        System.out.println("contacting aliens");
    }
}

public interface BaseVisitor {
    void onSub1(Sub1 that);
    void onSub2(Sub2 that);

    static Consumer<Base> forEach(Consumer<Sub1> sub1, Consumer<Sub2> sub2) {

        return base -> {
            BaseVisitor baseVisitor = new BaseVisitor() {

                @Override
                public void onSub1(Sub1 that) {
                    sub1.accept(that);
                }

                @Override
                public void onSub2(Sub2 that) {
                    sub2.accept(that);
                }
            };
            base.visit(baseVisitor);
        };
    }
}

Collection<Base> bases = Arrays.asList(new Sub1(), new Sub2());

bases.stream()
     .forEach(BaseVisitor.forEach(
             Sub1::makeASandwich,
             Sub2::contactAliens));
Michael Anderson
źródło
+1: Tego rodzaju rzeczy są powszechnie używane w językach, które mają pierwszorzędne wsparcie dla typów sum i dopasowywania wzorców, i jest to poprawny projekt w Javie, nawet jeśli jest bardzo brzydki pod względem składniowym.
Mankarse
@Mankarse Trochę cukru syntaktycznego może uczynić go bardzo dalekim od brzydkiego - zaktualizowałem go przykładem.
Michael Anderson
Powiedzmy hipotetycznie, że OP zmienia zdanie i postanawia dodać Sub3. Czy gość nie ma dokładnie takiego samego problemu jak instanceof? Teraz musisz dodać onSub3do odwiedzającego, a jeśli zapomnisz, pęka.
Radiodef
1
@Radiodef Zaletą używania wzorca odwiedzającego nad bezpośrednim włączeniem typu jest to, że po dodaniu onSub3do 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.
Michael Anderson
1
@FiveNine, jeśli z przyjemnością dodasz ten pełny przykład na końcu odpowiedzi, która może pomóc innym.
Michael Anderson
83

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ą catsi fish. animalsmają właściwości, które są wspólne dla catsi fish. Ale to nie wystarczy: istnieją pewne właściwości, które różnią się catod nich fish, dlatego należy podklasę.

Teraz masz problem polegający na tym, że zapomniałeś modelować ruch . W porządku. To stosunkowo łatwe:

for(Animal a : animals){
   if (a instanceof Fish) swim();
   if (a instanceof Cat) walk();
}

Ale to zły projekt. Prawidłowy sposób to:

for(Animal a : animals){
    animal.move()
}

Gdzie movebyłoby wspólne zachowanie wdrażane inaczej przez każde zwierzę.

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.

Oznacza to: twój projekt jest zepsuty.

Moja rekomendacja: Refactor Base, Sub1i Sub2.

Thomas Junk
źródło
8
Jeśli zaoferowałbyś rzeczywisty kod, mógłbym przedstawić zalecenia.
Thomas Junk
9
@codebreaker Jeśli zgadzasz się, że twój przykład nie był dobry, zalecamy zaakceptowanie tej odpowiedzi, a następnie napisanie nowego pytania. Nie zmieniaj pytania, aby mieć inny przykład, ponieważ istniejące odpowiedzi nie będą miały sensu.
Moby Disk
16
@codebreaker Myślę, że to „rozwiązuje” problem, odpowiadając na prawdziwe pytanie. Prawdziwe pytanie brzmi: jak rozwiązać problem? Popraw kod poprawnie. Refactor tak, że .move()albo .attack()a właściwie streszczenie thatczęść - 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.
WernerCD
11
@codebreaker Jeśli hierarchia klas prowadzi Cię do takich sytuacji, to z definicji nie ma sensu
Patrick Collins
7
@codebreaker W związku z ostatnim komentarzem Crisfole'a istnieje inny sposób patrzenia na to, który może pomóc. Na przykład za pomocą movevs fly/ 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.
Izkata
9

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 Basepracuj nad listą akcji

for (RandomAction action : actions)
   action.act(context);

gdzie

interface RandomAction {
    void act(Context context);
} 

interface Context {
    Ingredients getRandomIngredients();
    double getFrequency();
}

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)

Pete Kirkham
źródło
2
Rozumiecie, że moje absurdalne metody mają na celu pokazanie różnic w tym, co klasy mogą zrobić, gdy ich podklasa jest znana (i że nie mają ze sobą nic wspólnego), ale myślę, że posiadanie Contextobiektu tylko pogarsza sytuację. Równie dobrze mógłbym przekazywać Objectmetody, rzucać je i mieć nadzieję, że są odpowiedniego typu.
codebreaker
1
@ kodebreaker naprawdę musisz wyjaśnić swoje pytanie - w większości systemów istnieje uzasadniony powód, aby wywołać wiele funkcji niezależnych od tego, co robią; na przykład, aby przetworzyć reguły dotyczące zdarzenia lub wykonać krok w symulacji. Zazwyczaj takie systemy mają kontekst, w którym zachodzą działania. Jeśli „getRandomIngredients” i „getFrequency” nie są powiązane, to nie powinno znajdować się w tym samym obiekcie, a potrzebujesz jeszcze innego podejścia, na przykład polegającego na przechwytywaniu źródła składników lub częstotliwości.
Pete Kirkham
1
Masz rację i nie sądzę, żebym mógł ocalić to pytanie. W końcu wybrałem zły przykład, więc prawdopodobnie opublikuję inny. GetFrequency () i getIngredients () były tylko symbolami zastępczymi. Prawdopodobnie powinienem był po prostu wstawić „...” jako argumenty, aby uczynić bardziej oczywistym, że nie wiedziałbym, czym one są.
codebreaker
To jest właściwa odpowiedź IMO. Zrozum, że Contextnie musi to być nowa klasa ani nawet interfejs. Zwykle kontekstem jest tylko osoba dzwoniąca, więc połączenia są w formie action.act(this). Jeśli getFrequency()i getRandomIngredients()są metodami statycznymi, możesz nawet nie potrzebować kontekstu. Tak chciałbym wdrożyć powiedzmy, kolejkę zadań, której twój problem brzmi okropnie.
Pytanie C
Jest to również w większości identyczne z rozwiązaniem wzorców użytkowników. Różnica polega na tym, że implementujesz metody Visitor :: OnSub1 () / Visitor :: OnSub2 () jako Sub1 :: act () / Sub2 :: act ()
Pytanie C
4

A jeśli masz podklasy, które implementują jeden lub więcej interfejsów, które określają, co mogą zrobić? Coś takiego:

interface SandwichCook
{
    public void makeASandwich(String[] ingredients);
}

interface AlienRadioSignalAwarable
{
    public void contactAliens(int frequency);

}

Twoje zajęcia będą wyglądać następująco:

class Sub1 extends Base implements SandwichCook
{
    public void makeASandwich(String[] ingredients)
    {
        //some code here
    }
}

class Sub2 extends Base implements AlienRadioSignalAwarable
{
    public void contactAliens(int frequency)
    {
        //some code here
    }
}

Twoja pętla for stanie się:

for (Base base : bases) {
    if (base instanceof SandwichCook) {
        base.makeASandwich(getRandomIngredients());
    } else if (base instanceof AlienRadioSignalAwarable) {
        base.contactAliens(getFrequency());
    }
}

Dwie główne zalety tego podejścia:

  • bez udziału castingu
  • możesz sprawić, by każda podklasa zaimplementowała tyle interfejsów, ile chcesz, co zapewnia pewną elastyczność dla przyszłych zmian.

PS: Przepraszam za nazwy interfejsów, nie mogłem wymyślić nic fajniejszego w tym konkretnym momencie: D.

Radu Murzea
źródło
3
To nie rozwiązuje problemu. Nadal potrzebujesz obsady w pętli for. (Jeśli nie, to też nie potrzebujesz obsady w przykładzie OP).
Taemyr
2

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 kolekcjiBunchOfThings<T> .

Niektóre implementacje BunchOfThingssą zmienne; niektóre nie są. W wielu przypadkach obiekt Fred może chcieć trzymać coś, co może wykorzystać jako BunchOfThingsi wie, że nic innego niż Fred nie będzie w stanie go zmodyfikować. Wymóg ten można spełnić na dwa sposoby:

  1. Fred wie, że zawiera jedyne odniesienia do tego BunchOfThings , i że żadne BunchOfThingswewnętrzne odniesienie nie istnieje w całym wszechświecie. Jeśli nikt inny nie ma odniesienia do BunchOfThingswewnętrznych elementów, nikt inny nie będzie mógł go zmodyfikować, więc ograniczenie zostanie spełnione.

  2. 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ć a BunchOfThings, 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 AsImmutablemetody, 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ócony asDetached, tj. albo nieudostępnione odwołanie do obiektu zmiennego lub współdzielone odniesienie do obiektu zmiennego), oraz asNewMutable(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ż asXXmetody mogą zwracać nieco inne typy, ich prawdziwą rolą jest zapewnienie, że zwracane obiekty będą spełniać potrzeby programu.

supercat
źródło
0

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:

if (base.canMakeASandwich()) {
    base.makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    base.contactAliens(getFrequency());
    // ... etc.
}

Następnie jedna lub obie podklasy zastępują canMakeASandwich()i tylko jedna implementuje każdą z makeASandwich()i contactAliens().


Użyj interfejsów, a nie konkretnych podklas, aby wykryć możliwości danego typu. Pozostaw klasę podstawową w spokoju i zmień kod na:

if (base instanceof SandwichMaker) {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    ((AlienContacter)base).contactAliens(getFrequency());
    // ... etc.
}

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):

try {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
} catch (ClassCastException e) {
    ((AlienContacter)base).contactAliens(getFrequency());
}

Osobiście zazwyczaj nie lubię tego drugiego sposobu wychwytywania połowicznie oczekiwanego wyjątku, ze względu na ryzyko niewłaściwego wyłapania ClassCastExceptionwychodzenia z getRandomIngredientslub makeASandwich, ale YMMV.

Steve Jessop
źródło
2
Łapanie ClassCastException jest po prostu ew. To jest cały cel instanceof.
Radiodef
@Radiodef: prawda, ale jednym z celów <jest unikanie łapania ArrayIndexOutOfBoundsException, 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.
Steve Jessop,
@SteveJessop »Łatwiej prosić o wybaczenie niż o pozwolenie« to hasło;)
Thomas Junk
0

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:

  1. Możemy objąć wszystkie przypadki w klasie podstawowej.
  2. Żadna obca klasa pochodna nie musiałaby nigdy dodawać nowego przypadku.
  3. Możemy żyć z polityką, dla której wezwanie do bycia pod kontrolą klasy podstawowej.
  4. Ale wiemy z naszej nauki, że polityka tego, co robić w klasie pochodnej dla metody innej niż klasa podstawowa, jest polityką klasy pochodnej, a nie klasy podstawowej.

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.

Jozuego
źródło
-1

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.

class Sub1 : Base {
    @Override void doAThing() {
        this.makeASandwich(getRandomIngredients());
    }
}

class Sub2 : Base {
    @Override void doAThing() {
        this.contactAliens(getFrequency());
    }
}

for (Base base : bases) {
    base.doAThing();
}

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.

Losowo 832
źródło
2
Twoja odpowiedź sugeruje, że nie przeczytałeś do końca pytania: polimorfizm po prostu go tutaj nie wycina. Istnieje kilka metod dla Sub1 i Sub2, są to tylko przykłady, a „doAThing” tak naprawdę nic nie znaczy. To nie odpowiada nic, co bym zrobił. Dodatkowo, jak w ostatnim zdaniu: co powiesz na gwarantowane bezpieczeństwo typu?
codebreaker
@codebreaker - odpowiada dokładnie temu, co robisz w pętli w przykładzie. Jeśli to nie ma znaczenia, nie pisz pętli. Jeśli nie ma to sensu jako metoda, nie ma sensu jako ciało pętli.
Random832
1
A twoje metody „gwarantują” bezpieczeństwo w taki sam sposób, jak robi to rzut: rzucając wyjątek, jeśli obiekt jest niewłaściwego typu.
Random832
Zdaję sobie sprawę, że wybrałem zły przykład. Problem polega na tym, że większość metod w podklasach bardziej przypomina metody pobierające niż metody mutacyjne. Te klasy nie robią rzeczy same, głównie dostarczają dane. Polimorfizm mi tam nie pomoże. Mam do czynienia z producentami, a nie konsumentami. Ponadto: zgłoszenie wyjątku jest gwarancją czasu wykonywania, który nie jest tak dobry jak czas kompilacji.
codebreaker
1
Chodzi mi o to, że twój kod zapewnia jedynie gwarancję czasu działania. Nic nie stoi na przeszkodzie, aby ktoś nie sprawdził isSub2 przed wywołaniem asSub2.
Random832
-2

Można wcisnąć oba makeASandwichi contactAliensdo 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ą.

class Sub1 extends Base{
    Sandwich makeASandwich(Ingredients i){
        //Normal implementation
    }
    boolean contactAliens(Frequency onFrequency){
        return false;
    }
 }
class Sub2 extends Base{
    Sandwich makeASandwich(Ingredients i){
        return null;
    }
    boolean contactAliens(Frequency onFrequency){
       // normal implementation
    }
 }

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.

Znak
źródło
1
Pomyślałem o zrobieniu tego, ale narusza to zasadę substytucji Liskowa i nie jest tak miło pracować, np. Po wpisaniu „sub1”. pojawia się autouzupełnianie, widzisz makeASandwich, ale nie kontaktujesz się z kosmitami.
codebreaker
-5

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.Fieldijava.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 Memberw rękach i wiemy, że jest to Methodalbo a Field, albo (rzadko, coś innego), a jednak musimy robić wszystko, co żmudne, instanceofaby 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.) MethodKlasa 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 nich null.

  • Każda klasa pochodna zastępuje tylko jedną z asDerivedClass()metod, zwracając thiszamiast null. Nie zastępuje żadnej z pozostałych, ani nie chce nic o nich wiedzieć. Więc nie IllegalStateExceptionsą wyrzucane.

  • Klasa podstawowa zapewnia również finalimplementacje dla całej isDerivedClass()rodziny metod, kodowanych w następujący sposób: W return asDerivedClass() != null; ten sposób liczba metod, które muszą zostać zastąpione przez klasy pochodne, jest zminimalizowana.

  • Nie korzystałem @Deprecatedz 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ą assłowa kluczowego. W języku C # można powiedzieć DerivedClass derivedInstance = baseInstance as DerivedClass, a dostaniesz odniesienie do DerivedClassjeśli baseInstancebył z tej klasy, lub nulljeśli nie było. To (teoretycznie) działa lepiej niż ispo nim rzutowanie ( isjest to oczywiście lepiej nazwane słowo kluczowe C # dla instanceof,), ale mechanizm niestandardowy, który tworzymy ręcznie, działa jeszcze lepiej: para instanceofoperacji Java i rzutowania również w Javie ponieważ asoperator 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, Methodhierarchii, aw komentarzach poniżej ja również pokazać, że twórcy # starcie C niezależnie przybył na równoważny projekt dla System.Reflection.MemberInfo, FieldInfo, MethodInfohierarchii. 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.

Mike Nakis
źródło
13
To prawda, jeśli zaprojektowałeś się w pudełku. Taki jest problem z wieloma tego typu pytaniami. Ludzie podejmują decyzje w innych obszarach projektu, które wymuszają tego typu hackerskie rozwiązania, gdy prawdziwym rozwiązaniem jest ustalenie, dlaczego w ogóle znalazłeś się w takiej sytuacji. Przyzwoity projekt cię tu nie doprowadzi. Obecnie patrzę na aplikację o wartości 200 000 SLOC i nigdzie nie widzę potrzeby, aby to zrobić. Więc to nie jest po prostu coś, co ludzie czytają w książce. Nie dostanie się w tego typu sytuacje jest po prostu końcowym rezultatem przestrzegania dobrych praktyk.
Dunk
3
@Dunk Właśnie pokazałem, że potrzeba takiego mechanizmu istnieje na przykład w java.lang.reflection.Memberhierarchii. 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.
Mike Nakis,
6
@MikeNakis Twórcy Javy podjęli wiele złych decyzji, więc same w sobie niewiele mówią. W każdym razie lepsze byłoby skorzystanie z gościa niż wykonanie ad-hoc testu-wtedy-obsady.
Doval
3
Ponadto przykład jest wadliwy. Istnieje Memberinterfejs, 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 nie List<Member>pojawia się), więc nie musisz rzucać. Uwaga: Classnie zapewnia getMembers()metody. Oczywiście, nieświadomy programista mógłby stworzyć List<Member>, ale nie miałoby to większego sensu, jak zrobienie go List<Object>i dodanie części Integerlub Stringdo niego.
SJuan76
5
@MikeNakis „Wiele osób to robi” i „Znani ludzie to robią” nie są prawdziwymi argumentami. Antypatterny są zarówno złymi pomysłami, jak i szeroko stosowanymi z definicji, ale te wymówki uzasadniałyby ich użycie. Podaj rzeczywiste powody zastosowania takiego lub innego projektu. Mój problem z przesyłaniem ad-hoc polega na tym, że każdy użytkownik Twojego interfejsu API musi poprawnie przesyłać za każdym razem, gdy to robi. Odwiedzający musi być poprawny tylko raz. Ukrywanie rzutowania za niektórymi metodami i zwracanie nullzamiast wyjątku nie poprawia sytuacji. Wszystkie pozostałe są równe, odwiedzający jest bezpieczniejszy podczas wykonywania tej samej pracy.
Doval