Como evitar o downcasting?

12

Minha pergunta é sobre um caso especial da super classe Animal.

  1. Minha Animallata moveForward()e eat().
  2. Sealse estende Animal.
  3. Dogse estende Animal.
  4. E há uma criatura especial que também se estende Animalchamada Human.
  5. Humanimplementa também um método speak()(não implementado por Animal).

Em uma implementação de um método abstrato que aceite, Animaleu gostaria de usar o speak()método. Isso parece impossível sem fazer um downcast. Jeremy Miller escreveu em seu artigo que um downcast cheira.

Qual seria uma solução para evitar o downcasting nessa situação?

Bart Weber
fonte
6
Corrija seu modelo. Usar a hierarquia taxonômica existente para modelar uma hierarquia de classes geralmente é uma idéia errada. Você deve extrair suas abstrações do código existente, não criar abstrações e tentar ajustar o código a ele.
Euphoric
2
Se você quiser Animais de falar, então a capacidade de falar é parte do que faz algo um animal: artima.com/interfacedesign/PreferPoly.html
Jeffo
8
Siga em frente? e os caranguejos?
Fabio Marcolini
Qual item # é aquele em Java Efetivo, BTW?
Goldilocks
1
Algumas pessoas não podem falar, então uma exceção é lançada se você tentar.
Tulains Córdova

Respostas:

12

Se você possui um método que precisa saber se a classe específica é do tipo Humanpara fazer algo, está quebrando alguns princípios do SOLID , particularmente:

  • Princípio aberto / fechado - se, no futuro, você precisar adicionar um novo tipo de animal que possa falar (por exemplo, um papagaio) ou fazer algo específico para esse tipo, seu código existente precisará ser alterado
  • Princípio de segregação de interface - parece que você está generalizando demais. Um animal pode cobrir um amplo espectro de espécies.

Na minha opinião, se seu método espera um tipo de classe específico, para chamá-lo de método específico, altere esse método para aceitar apenas essa classe, e não sua interface.

Algo assim :

public void MakeItSpeak( Human obj );

e não assim:

public void SpeakIfHuman( Animal obj );
BЈовић
fonte
Pode-se também criar um método abstrato em Animalchamado canSpeake cada implementação concreta deve definir se pode ou não "falar".
Brandon
Mas eu gosto mais da sua resposta. Muita complexidade vem da tentativa de tratar algo como algo que não é.
Brandon
@Brandon Acho que, nesse caso, se não puder "falar", lance uma exceção. Ou apenas não faça nada.
BЈовић
Então você fica sobrecarregado assim: public void makeAnimalDoDailyThing(Animal animal) {animal.moveForward(); animal.eat()}epublic void makeAnimalDoDailyThing(Human human) {human.moveForward(); human.eat(); human.speak();}
Bart Weber
1
Vá até o fim - makeItSpeak (ISpeakingAnimal animal) - você poderá ter o ISpeakingAnimal implementando Animal. Você também pode fazer o itSpeak (Animal animal) {falar se for exemplo de ISpeakingAnimal}, mas isso tem um cheiro fraco.
Ptyx
6

O problema não é que você está fazendo downcast - é para onde está fazendo downcast Human. Em vez disso, crie uma interface:

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
    }
    //....
}

Dessa forma, a condição não é que o animal seja Human- a condição é que ele possa falar. Isso significa que mysteriousMethodpode trabalhar com outras subclasses não humanas Animal, desde que sejam implementadas CanSpeak.

Idan Arye
fonte
caso em que deve tomar como argumento uma instância de CanSpeak em vez de animal
Newtopian
1
@ Newtopian Supondo que esse método não precise de nada Animale que todos os usuários desse método retenham o objeto que desejam enviar a ele por meio de uma CanSpeakreferência de tipo (ou mesmo de Humanreferência de tipo). Se fosse esse o caso, esse método poderia ter sido usado Humanem primeiro lugar e não precisaríamos apresentá-lo CanSpeak.
precisa
Livrar-se da interface não está vinculado a ser específico na assinatura do método, você pode livrar-se da interface se, e somente se, houver apenas seres humanos e sempre houver apenas seres humanos que "CanSpeak".
Newtopian
1
@ Newtopian O motivo que introduzimos CanSpeakem primeiro lugar não é o fato de termos algo que o implementa ( Human), mas de algo que o utiliza (o método). O objetivo de CanSpeaké dissociar esse método da classe concreta Human. Se não tivéssemos métodos que tratassem as coisas de maneira CanSpeakdiferente, não faria sentido distinguir as coisas assim CanSpeak. Nós não criamos uma interface só porque temos um método ...
Idan Arye
2

Você pode adicionar o Communicate to Animal. Cachorro late, Humano fala, Selo .. uhh ... Eu não sei o que selo faz.

Mas parece que seu método foi desenvolvido para if (Animal is Human) Speak ();

A pergunta que você pode querer fazer é: qual é a alternativa? É difícil dar uma sugestão, pois não sei exatamente o que você deseja alcançar. Existem situações teóricas em que downcasting / upcasting é a melhor abordagem.

Andrew Hoffman
fonte
15
O selo vai ow ow ow ow , mas a raposa é indefinida.
2

Nesse caso, a implementação padrão de speak()na AbstractAnimalclasse seria:

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

Nesse ponto, você tem uma implementação padrão na classe Abstract - e ela se comporta corretamente.

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

Sim, isso significa que você tem try-catchs espalhados pelo código para lidar com todos speak, mas a alternativa é if(thingy is Human)envolver todas as conversas.

A vantagem da exceção é que, se você tiver outro tipo de coisa em algum momento que fala (um papagaio), não precisará reimplementar todos os seus testes.


fonte
1
Eu não acho que esse seja um bom motivo para usar exceção (a menos que seja código python).
Bryan Chen
1
Não vou votar porque isso tecnicamente funcionaria, mas eu realmente não gosto desse tipo de design. Por que definir um método no nível pai que ele não pode implementar? A menos que você saiba que tipo de objeto é (e se você soubesse - você não teria esse problema), sempre precisará usar uma tentativa / captura para verificar uma exceção completamente evitável. Você pode até usar um canSpeak()método para lidar com isso melhor.
Brandon
2
Eu trabalhei em um projeto que teve muitos defeitos originados da decisão de alguém de reescrever vários métodos de trabalho para gerar uma exceção, porque eles estão usando uma implementação obsoleta. Ele poderia ter corrigido a implementação, mas optou por lançar exceções em todos os lugares. Naturalmente, entramos no controle de qualidade com centenas de bugs. Portanto, sou inclinado a lançar exceções em métodos que não devem ser chamados. Se eles não deveriam ser chamados, exclua-os .
Brandon
2
Alternativa para lançar uma exceção neste caso pode estar fazendo nada. Afinal, eles não podem falar :) #
186
@Brandon: há uma diferença entre projetá-lo desde o início, com exceção, e adaptá-lo mais tarde. Pode-se também olhar para uma interface com um método padrão no Java8, ou não lançando uma exceção e apenas ficar em silêncio. O ponto principal é que, para evitar a necessidade de fazer o downcast, a função precisa ser definida no tipo que está sendo passado.
1

Às vezes, o downcasting é necessário e apropriado. Em particular, geralmente é apropriado nos casos em que se possui objetos que podem ou não ter alguma habilidade, e se deseja usá-la quando existe enquanto manipula objetos sem essa habilidade de alguma maneira padrão. Como um exemplo simples, suponha que Stringse pergunte a se é igual a algum outro objeto arbitrário. Para que um Stringseja igual ao outro String, ele deve examinar o comprimento e a matriz de caracteres de apoio da outra sequência. Se a Stringé perguntado se é igual a Dog, no entanto, ele não pode acessar o comprimento de Dog, mas não deveria; em vez disso, se o objeto ao qual Stringse supostamente se comparar não for umString, a comparação deve usar um comportamento padrão (relatando que o outro objeto não é igual).

O momento em que o downcasting deve ser considerado o mais duvidoso é quando o objeto que está sendo transmitido é "conhecido" como sendo do tipo apropriado. Em geral, se um objeto é conhecido como a Cat, deve-se usar uma variável do tipo Cat, em vez de uma variável do tipo Animal, para se referir a ele. Há momentos em que isso nem sempre funciona, no entanto. Por exemplo, uma Zoocoleção pode conter pares de objetos em slots de matriz pares / ímpares, com a expectativa de que os objetos em cada par possam agir um contra o outro, mesmo que não possam agir sobre os objetos em outros pares. Nesse caso, os objetos em cada par ainda teriam que aceitar um tipo de parâmetro não específico, de forma que eles pudessem, sintaticamente , passar os objetos de qualquer outro par. Assim, mesmo se Cat'splayWith(Animal other)O método só funcionaria quando otherfosse a Cat, Zooseria necessário transmitir a ele um elemento de um Animal[], portanto, seu tipo de parâmetro teria que ser em Animalvez de Cat.

Nos casos em que o downcasting é legitimamente inevitável, deve-se usá-lo sem receio. A questão-chave é determinar quando é possível evitar sensivelmente o downcasting e evitá-lo quando possível.

supercat
fonte
No caso de string, você deve preferir ter um método Object.equalToString(String string). Então você tem boolean String.equal(Object object) { return object.equalStoString(this); }Então, não é necessário fazer downcast: você pode usar o envio dinâmico.
Giorgio
@Giorgio: O envio dinâmico tem seus usos, mas geralmente é ainda pior que o downcasting.
Supercat
maneira despacho dinâmico geralmente ainda pior do que o downcasting? Eu acho que é o contrário
Bryan Chen
@BryanChen: Isso depende da sua terminologia. Eu acho que não Objecttem nenhum equalStoStringmétodo virtual e admito que não sei como o exemplo citado funcionaria em Java, mas em C #, o despacho dinâmico (diferente do despacho virtual) significaria que o compilador tem essencialmente para fazer a pesquisa de nome com base no Reflection na primeira vez em que um método é usado em uma classe, que é diferente do envio virtual (que simplesmente faz uma chamada por um slot na tabela de métodos virtual, necessário para conter um endereço de método válido).
precisa
Do ponto de vista da modelagem, eu preferiria o envio dinâmico em geral. É também a maneira orientada a objetos de selecionar um procedimento com base no tipo de seus parâmetros de entrada.
Giorgio
1

Em uma implementação de um método abstrato que aceita Animals, eu gostaria de usar o método speak ().

Você tem poucas escolhas:

  • Use reflexão para chamar, speakse existir. Vantagem: sem dependência Human. Desvantagem: agora existe uma dependência oculta do nome "speak".

  • Introduzir uma nova interface Speakere fazer downcast na interface. Isso é mais flexível do que depende de um tipo específico de concreto. Tem a desvantagem que você precisa modificar Humanpara implementar Speaker. Isso não funcionará se você não puder modificarHuman

  • Downcast para Human. Isso tem a desvantagem de que você precisará modificar o código sempre que desejar que outra subclasse fale. Idealmente, você deseja estender aplicativos adicionando código sem voltar e alterar repetidamente o código antigo.

Kevin Cline
fonte