Como lidar com os métodos que foram adicionados para subtipos no contexto do polimorfismo?

14

Quando você usa o conceito de polimorfismo, cria uma hierarquia de classes e, usando a referência de pais, chama as funções de interface sem saber qual tipo específico tem o objeto. Isso é ótimo. Exemplo:

Você tem uma coleção de animais e você chama todos os animais de função eat e você não se importa se é um cachorro comendo ou um gato. Mas, na mesma hierarquia de classes, você tem animais que têm outros - além de herdados e implementados da classe Animal , por exemplo, makeEggs , getBackFromTheFreezedState e assim por diante. Então, em alguns casos, você pode querer saber o tipo específico para chamar comportamentos adicionais.

Por exemplo, no caso de ser hora da manhã e se for apenas um animal, ligue para eat , caso contrário, se for humano, chame primeiro washHands , getDressed e só então chame eat . Como lidar com esses casos? O polimorfismo morre. Você precisa descobrir o tipo do objeto, que soa como um cheiro de código. Existe uma abordagem comum para lidar com esses casos?

    
por Narek 19.02.2018 / 14:58
fonte

4 respostas

18

Depende. Infelizmente não há solução genérica. Pense nas suas necessidades e tente descobrir o que essas coisas devem fazer.

Por exemplo, você disse de manhã que diferentes animais fazem coisas diferentes. Que tal introduzir um método getUp() ou prepareForDay() ou algo parecido? Então você pode continuar com o polimorfismo e deixar cada animal executar sua rotina matinal.

Se você quiser diferenciar os animais, não os armazene indiscriminadamente em uma lista.

Se nada mais funcionar, então você pode tentar o Padrão de visitante , que é < href="https://lostechies.com/derekgreer/2010/04/19/double-dispatch-is-a-code-smell/"> tipo de um hack para permitir uma espécie de envio dinâmico onde você pode enviar um visitante que receberá retornos de chamada do tipo exato dos animais. Gostaria de salientar, no entanto, que este deveria ser um último recurso se tudo o resto falhar.

    
por 19.02.2018 / 16:03
fonte
33

Esta é uma boa pergunta e é o tipo de problema que muitas pessoas tentam entender como usar o OO. Eu acho que a maioria dos desenvolvedores luta com isso. Eu gostaria de poder dizer que a maioria passa por isso, mas não tenho certeza se é o caso. A maioria dos desenvolvedores, na minha experiência, acaba usando sacos de propriedade pseudo-OO .

Primeiro, deixe-me ser claro. Isso não é culpa sua. O modo como o OO é tipicamente ensinado é altamente falho. O Animal exemplo é o primeiro infrator, IMO. Basicamente, dizemos, vamos falar sobre objetos, o que eles podem fazer. Um Animal pode eat() e pode speak() . Super. Agora crie alguns animais e codifique como eles comem e falam. Agora você sabe OO, certo?

O problema é que isso está vindo na OO da direção errada. Por que existem animais neste programa e por que eles precisam falar e comer?

Eu tenho dificuldade em pensar em um uso real para um tipo Animal . Tenho certeza de que existe, mas vamos discutir algo que eu acho mais fácil de raciocinar: uma simulação de tráfego. Suponha que queremos modelar o tráfego em vários cenários. Aqui estão algumas coisas básicas que precisamos ter para poder fazer isso.

Vehicle
Road
Signal

Podemos ir mais fundo com todos os tipos de pedestres e trens, mas vamos simplificar.

Vamos considerar Vehicle . Quais capacidades o veículo precisa? Precisa viajar em uma estrada. Precisa ser capaz de parar nos sinais. Ele precisa ser capaz de navegar pelas interseções.

interface Vehicle {
  move(Road road);
  navigate(Road... intersection);
}

Isso provavelmente é simples demais, mas é um começo. Agora. E quanto a todas as outras coisas que um veículo pode fazer? Eles podem se desligar de uma estrada e entrar em uma vala. Isso é parte da simulação? Não. Não precisa disso. Alguns carros e ônibus têm sistemas hidráulicos que permitem que eles saltem ou se ajoelhem, respectivamente. Isso é parte da simulação? Não. Não precisa disso. A maioria dos carros queima gasolina. Alguns não. A usina de energia é parte da simulação? Não. Não precisa disso. Tamanho da roda? Não precisa disso. Navegação GPS? Sistema de informação e entretenimento? Não precisa de em.

Você só precisa definir comportamentos que você vai usar. Para esse fim, acho que geralmente é melhor criar interfaces OO a partir do código que interage com elas. Você começa com uma interface vazia e, em seguida, começa a escrever o código que chama os métodos inexistentes. É assim que você sabe quais métodos você precisa na sua interface. Então, uma vez feito isso, você começa a definir classes que implementam esses comportamentos. Comportamentos que não são usados são irrelevantes e não precisam ser definidos.

O ponto principal do OO é que você pode adicionar novas implementações dessas interfaces mais tarde sem alterar o código de chamada. A única maneira que funciona é se as necessidades do código de chamada determinam o que entra na interface. Não há como definir todos os comportamentos de todas as coisas possíveis que poderiam ser pensadas depois.

    
por 19.02.2018 / 17:59
fonte
9

TL; DR:

Pense em uma abstração e métodos que se apliquem a todas as subclasses e cubram tudo o que você precisa.

Vamos primeiro ficar com o seu exemplo eat() .

É uma propriedade de ser humano que, como pré-condição para comer, os humanos querem lavar as mãos e se vestir antes de comer. Se você quer que alguém venha até você para tomar o café da manhã com você, você não os diz para lavar as mãos e se vestir, eles fazem isso sozinhos quando você os convida, ou eles respondem " Não, não posso vir, não lavei as mãos e ainda não estou vestida ".

Voltar ao software:

Como a Human instance não comerá sem as pré-condições, eu teria o método Human eat() do washHands() e getDressed() se isso não tiver sido feito. Não deve ser seu trabalho como o chamador eat() saber sobre essa peculiaridade. A alternativa do teimoso-humano seria lançar uma exceção ("Eu não estou preparado para comer!") Se as condições prévias não forem satisfeitas, deixando você frustrado, mas pelo menos informado que comer não funcionou.

Que tal makeEggs() ?

Eu recomendaria mudar sua maneira de pensar. Você provavelmente quer executar os deveres matinais programados de todos os seres. Novamente, como o chamador não deve ser o seu trabalho para saber quais são os seus deveres. Então, eu recomendo um método doMorningDuties() que todas as classes implementam.

    
por 19.02.2018 / 16:09
fonte
2

A resposta é bem simples.

How to handle objects that can do more than you expect?

Você não precisa lidar com isso porque não serviria para nada. Geralmente, uma interface é projetada dependendo de como ela será usada. Se a sua interface não define a lavagem de mãos, então você não se importa com isso como o chamador da interface; se você tivesse, você teria projetado de maneira diferente.

For example, in case it is morning time and if it is just an animal then you call eat, otherwise if it is a human, then call first washHands, getDressed and only then call eat. How to handle this cases?

Por exemplo, no pseudocódigo:

interface IEater { void Eat(); }
interface IMorningRoutinePerformer { void DoMorningRoutine(); }
interface IAnimal : IEater, IMorningPerformer;
interface IHuman : IEater, IMorningPerformer; 
{
  void WashHands();
  void GetDressed();
}

void MorningTime()
{
   IList<IMorningRoutinePerformer> items = Service.GetMorningPerformers();
   foreach(item in items) { item.DoMorningRoutine(); }
}

Agora você implementa IMorningPerformer para Animal apenas para comer, e para Human você também o implementa para lavar as mãos e se vestir. O chamador do seu método MorningTime poderia se importar menos se fosse humano ou um animal. Tudo o que se quer é a rotina matinal realizada, que cada objeto faz admiravelmente graças à OO.

Polymorphism dies.

Ou isso?

You need to find out the type of the object

Por que estamos assumindo isso? Eu acho que isso poderia ser uma suposição errada.

Is there a common approach to handle this cases?

Sim, geralmente é resolvido com hierarquia de classes ou interfaces cuidadosamente projetada. Note que no exemplo acima não há nada que contradiga o seu exemplo como você o deu, no entanto, você provavelmente se sentirá insatisfeito, porque você fez mais algumas suposições que você não escreveu na questão a partir do momento em que escrevo e essas suposições são provavelmente violadas.

É possível ir a um buraco de coelho apertando suas suposições e eu modificando a resposta para satisfazê-las, mas não acho que seria útil.

Projetar boas hierarquias de classe é difícil e requer muito insight sobre o seu domínio de negócios. Para domínios complexos, são feitas duas, três ou mais iterações, conforme elas refinam seu entendimento de como entidades diferentes em seu domínio de negócios interagem, até chegarem a um modelo adequado.

É aí que faltam exemplos de animais simplistas. Queremos ensinar coisas simples, mas o problema que estamos tentando resolver não é óbvio até que você vá mais fundo, ou seja, tenha considerações e domínios mais complexos.

    
por 20.02.2018 / 04:35
fonte