Jak uniknąć downcastingu?

12

Moje pytanie dotyczy specjalnego przypadku superklasy Animal.

  1. Moja Animalpuszka moveForward()i eat().
  2. Sealrozszerza się Animal.
  3. Dogrozszerza się Animal.
  4. I jest specjalne stworzenie, które rozszerza także Animaltzw Human.
  5. Humanimplementuje również metodę speak()(nie zaimplementowaną przez Animal).

W implementacji metody abstrakcyjnej, która akceptuje Animal, chciałbym użyć tej speak()metody. To nie wydaje się możliwe bez zrujnowania. Jeremy Miller napisał w swoim artykule, że pachnie przygnębiony.

Jakie byłoby rozwiązanie, aby uniknąć downcastingu w tej sytuacji?

Bart Weber
źródło
6
Napraw swój model. Używanie istniejącej hierarchii taksonomicznej do modelowania hierarchii klas jest zwykle złym pomysłem. Powinieneś wyciągnąć swoje abstrakcje z istniejącego kodu, a nie tworzyć abstrakcje, a następnie spróbować dopasować do niego kod.
Euphoric
2
Jeśli chcesz, aby Zwierzęta mówiły, to umiejętność mówienia jest częścią tego, co czyni zwierzę: artima.com/interfacedesign/PreferPoly.html
JeffO
8
pójść naprzód? co z krabami?
Fabio Marcolini,
Który element # to jest w Effective Java, BTW?
goldilocks
1
Niektórzy ludzie nie mogą mówić, więc jeśli spróbujesz, zostanie zgłoszony wyjątek.
Tulains Córdova

Odpowiedzi:

12

Jeśli masz metodę, która musi wiedzieć, czy konkretna klasa jest typu Human, aby coś zrobić, to łamiesz niektóre zasady SOLID , w szczególności:

  • Zasada otwarta / zamknięta - jeśli w przyszłości musisz dodać nowy typ zwierzęcia, który może mówić (na przykład papuga), lub zrobić coś specyficznego dla tego typu, Twój istniejący kod będzie musiał się zmienić
  • Zasada segregacji interfejsu - wygląda na to, że generalizujesz za dużo. Zwierzę może obejmować szerokie spektrum gatunków.

Moim zdaniem, jeśli twoja metoda oczekuje określonego typu klasy, aby wywołać tę metodę, zmień tę metodę, aby akceptowała tylko tę klasę, a nie interfejs.

Coś takiego :

public void MakeItSpeak( Human obj );

i nie tak:

public void SpeakIfHuman( Animal obj );
BЈовић
źródło
Można również tworzyć abstrakcyjne metody na Animalnazwał canSpeaka każda realizacja beton musi określić, czy może „mówić”.
Brandon
Ale bardziej podoba mi się twoja odpowiedź. Duża złożoność wynika z próby traktowania czegoś takiego, czym nie jest.
Brandon
@Brandon Chyba w takim przypadku, jeśli nie można „mówić”, to rzuć wyjątek. Lub po prostu nic nie rób.
BЈовић
Więc dostajesz przeciążenie w ten sposób: public void makeAnimalDoDailyThing(Animal animal) {animal.moveForward(); animal.eat()}ipublic void makeAnimalDoDailyThing(Human human) {human.moveForward(); human.eat(); human.speak();}
Bart Weber,
1
Idź na całość - makeItSpeak (ISpeakingAnimal animal) - możesz wtedy mieć ISpeakingAnimal implementuje Animal. Możesz także mieć makeItSpeak (Animal animal) {mów, jeśli wystąpienie ISpeakingAnimal}, ale ma słaby zapach.
ptyx
6

Problemem nie jest to, że jesteś downcastingiem - to, że jesteś downcastingiem Human. Zamiast tego utwórz interfejs:

public interface CanSpeak{
    void speak();
}

public abstract class Animal{
    //....
}

public class Human extends Animal implements CanSpeak{
    public void speak(){
        //....
    }
}

public void mysteriousMethod(Animal animal){
    //....
    if(animal instanceof CanSpeak){
        ((CanSpeak)animal).speak();
    }else{
        //Throw exception or something
    }
    //....
}

W ten sposób warunkiem nie jest to, że zwierzę jest Human- warunkiem jest to, że może mówić. Oznacza to, że mysteriousMethodmogą współpracować z innymi niekluczowymi podklasami, o Animalile tylko się zaimplementują CanSpeak.

Idan Arye
źródło
w takim przypadku jako argument powinien przyjąć instancję CanSpeak zamiast Animal
Newtopian
1
@Newtopian Zakładając, że ta metoda niczego nie potrzebuje Animali że wszyscy użytkownicy tej metody będą przechowywać obiekt, który chcą do niej wysłać za pośrednictwem CanSpeakodwołania do typu (lub nawet Humanodwołania do typu). Gdyby tak było, metoda ta mogłaby zostać zastosowana Humanw pierwszej kolejności i nie musielibyśmy jej przedstawiać CanSpeak.
Idan Arye
Pozbycie się interfejsu nie jest powiązane z byciem konkretnym w sygnaturze metody, możesz pozbyć się interfejsu wtedy i tylko wtedy, gdy są tylko ludzie i zawsze będą ludzie, którzy „CanSpeak”.
Newtopian
1
@Newtopian Powodem, dla którego wprowadziliśmy CanSpeakprzede wszystkim, nie jest to, że mamy coś, co ją implementuje ( Human), ale że mamy coś, co z niej korzysta (metoda). Chodzi o CanSpeakto, aby oddzielić tę metodę od konkretnej klasy Human. Gdybyśmy nie mieli metod, które traktowałyby te rzeczy CanSpeakinaczej, nie byłoby sensu rozróżniać tych rzeczy CanSpeak. Nie tworzymy interfejsu tylko dlatego, że mamy metodę ...
Idan Arye
2

Możesz dodać Communicate to Animal. Pies szczeka, Człowiek mówi, Pieczęć ... Uch ... Nie wiem, co robi pieczęć.

Ale wygląda na to, że twoja metoda została zaprojektowana, jeśli (Animal is Human) Speak ();

Pytanie, które możesz zadać, brzmi: jaka jest alternatywa? Trudno podać sugestię, ponieważ nie wiem dokładnie, co chcesz osiągnąć. Istnieją teoretyczne sytuacje, w których downcasting / upcasting jest najlepszym podejściem.

Andrew Hoffman
źródło
15
Pieczęć się powtarza , ale lis jest niezdefiniowany.
2

W takim przypadku domyślną implementacją speak()w AbstractAnimalklasie będzie:

void speak() throws CantSpeakException {
  throw new CantSpeakException();
}

W tym momencie masz domyślną implementację w klasie Abstract - i działa ona poprawnie.

try {
  thingy.speak();
} catch (CantSeakException e) {
  System.out.println("You can't talk to the " + thingy.name());
}

Tak, oznacza to, że masz rozrzuty rozrzucone po kodzie, aby obsłużyć każdy speak, ale alternatywą jest if(thingy is Human)owijanie wszystkich wypowiedzi.

Zaletą tego wyjątku jest to, że jeśli masz coś innego, co mówi (papuga), nie będziesz musiał zaimplementować wszystkich swoich testów.


źródło
1
Nie sądzę, że jest to dobry powód, aby używać wyjątku (chyba że jest to kod python).
Bryan Chen
1
Nie oddam głosu, ponieważ technicznie by to działało, ale naprawdę nie lubię tego rodzaju projektów. Po co definiować metodę na poziomie nadrzędnym, której rodzic nie może wdrożyć? O ile nie wiesz, jaki jest typ obiektu (a jeśli tak - nie miałbyś tego problemu), zawsze musisz użyć try / catch, aby sprawdzić wyjątek, którego można całkowicie uniknąć. Możesz nawet użyć canSpeak()metody, aby lepiej sobie z tym poradzić.
Brandon
2
Pracowałem nad projektem, który miał mnóstwo wad powstałych w wyniku czyjejś decyzji o przepisaniu wielu metod pracy w celu wyrzucenia wyjątku, ponieważ używają one przestarzałej implementacji. Mógł naprawić implementację, ale zamiast tego wybrał wszędzie wyjątki. Oczywiście wprowadziliśmy kontrolę jakości z setkami błędów. Jestem więc stronniczy w rzucaniu wyjątków w metody, które nie powinny być wywoływane. Jeśli nie mają być wywoływane, usuń je .
Brandon
2
Alternatywą dla zgłoszenia wyjątku w tym przypadku może być brak działania. W końcu nie mogą mówić :)
BЈовић
@Brandon istnieje różnica między projektowaniem go od samego początku z wyjątkiem a jego późniejszym doposażeniem. Można również spojrzeć na interfejs z domyślną metodą w Javie8 lub nie zgłaszać wyjątku i po prostu milczeć. Kluczową kwestią jest jednak to, że aby uniknąć konieczności downcastu, funkcja musi być zdefiniowana w przekazywanym typie.
1

Downcasting jest czasem konieczny i odpowiedni. W szczególności jest to często odpowiednie w przypadkach, w których ktoś posiada przedmioty, które mogą, ale nie muszą, mieć jakąś zdolność, i chce skorzystać z tej zdolności, gdy istnieje, podczas obsługi obiektów bez tej zdolności w pewien domyślny sposób. Jako prosty przykład, załóżmy, że a Stringjest pytane, czy jest ono równe jakimś innemu dowolnemu obiektowi. Aby jeden Stringbył równy String, musi zbadać długość i tylną tablicę znaków drugiego łańcucha. Jeśli a Stringzostanie zapytane, czy jest równe a Dog, nie może uzyskać dostępu do długości Dog, ale nie powinno tak być; zamiast tego, jeśli obiekt, do którego Stringma się porównać a, nie jestString, porównanie powinno wykorzystywać zachowanie domyślne (zgłaszanie, że drugi obiekt nie jest równy).

Moment, w którym spuszczanie wody należy uznać za najbardziej wątpliwe, to moment, w którym rzucany przedmiot jest „znany” z właściwego rodzaju. Ogólnie rzecz biorąc, jeśli obiekt jest znany jako Cat, należy do niego użyć zmiennej typu Cat, a nie zmiennej typu Animal. Są jednak chwile, kiedy to nie zawsze działa. Na przykład Zookolekcja może przechowywać pary obiektów w parzystych / nieparzystych gniazdach tablicowych, oczekując, że obiekty w każdej parze będą mogły na siebie oddziaływać, nawet jeśli nie będą mogły oddziaływać na obiekty w innych parach. W takim przypadku obiekty w każdej parze nadal będą musiały zaakceptować nieokreślony typ parametru, tak że mogłyby składniowo przekazać obiekty z dowolnej innej pary. Tak więc, nawet jeśli Cat„splayWith(Animal other)metoda działałaby tylko wtedy, gdy otherbyła a Cat, Zoomusiałaby móc przekazać jej element an Animal[], więc typem parametru musiałaby być Animalzamiast Cat.

W przypadkach, gdy downcasting jest prawnie nieunikniony, należy go używać bez żadnych skrupułów. Kluczowym pytaniem jest ustalenie, kiedy można rozsądnie uniknąć upuszczenia i unikanie go, gdy jest to rozsądnie możliwe.

supercat
źródło
W przypadku ciągu powinieneś raczej mieć metodę Object.equalToString(String string). Więc masz boolean String.equal(Object object) { return object.equalStoString(this); }Tak, nie jest potrzebny downcast: możesz użyć dynamicznej wysyłki.
Giorgio
@Giorgio: Dynamiczne wysyłanie ma swoje zastosowania, ale ogólnie jest nawet gorsze niż downcasting.
supercat
jak dynamiczne wysyłanie jest na ogół jeszcze gorsze niż downcasting? Myślę, że jest na odwrót
Bryan Chen,
@BryanChen: To zależy od twojej terminologii. Nie sądzę Object, aby equalStoStringistniała żadna metoda wirtualna i przyznaję, że nie wiem, jak cytowany przykład mógłby nawet działać w Javie, ale w języku C # dynamiczne wysyłanie (w odróżnieniu od wirtualnego wysyłania) oznaczałoby, że kompilator zasadniczo ma aby wykonać wyszukiwanie nazw oparte na odbiciu przy pierwszym użyciu metody w klasie, która różni się od wirtualnej wysyłki (która po prostu wykonuje wywołanie przez szczelinę w tabeli metod wirtualnych, która musi zawierać prawidłowy adres metody).
supercat
Z punktu widzenia modelowania wolałbym ogólnie dynamiczne wysyłanie. Jest to również obiektowy sposób wyboru procedury na podstawie typu jej parametrów wejściowych.
Giorgio
1

W realizacji abstrakcyjnej metody, która akceptuje Zwierzęta, chciałbym użyć metody speak ().

Masz kilka możliwości:

  • Użyj odbicia, aby zadzwonić, speakjeśli istnieje. Zaleta: brak zależności od Human. Wada: teraz istnieje ukryta zależność od nazwy „mówić”.

  • Wprowadź nowy interfejs Speakeri spuść do interfejsu. Jest to bardziej elastyczne niż w zależności od konkretnego rodzaju betonu. Ma tę wadę, że trzeba ją zmodyfikować, Humanaby zaimplementować Speaker. To nie zadziała, jeśli nie możesz modyfikowaćHuman

  • Downcast to Human. Ma to tę wadę, że będziesz musiał zmodyfikować kod, ilekroć chcesz mówić innej podklasie. Idealnie chcesz rozszerzyć aplikacje, dodając kod bez ciągłego cofania się i zmieniania starego kodu.

Kevin Cline
źródło