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


14

Ao usar o conceito de polimorfismo, você cria uma hierarquia de classes e, usando a referência dos 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 solicita a função de todos os animais eate não se importa se é um cachorro comendo ou um gato. Mas na mesma hierarquia de classes você tem animais que possuem outros - além de herdados e implementados da classe Animal, por exemplo makeEggs, getBackFromTheFreezedStatee assim por diante. Portanto, em alguns casos em que você trabalha, você pode querer saber o tipo específico para chamar comportamentos adicionais.

Por exemplo, no caso, é tempo de manhã e se ele é apenas um animal, então você chamar eat, caso contrário, se ele é um ser humano, em seguida, chamar primeiro washHands, getDressede só então chamar eat. Como lidar com esses casos? 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?


7
O tipo de polimorfismo que você descreveu é chamado de subtipo de polimorfismo , mas não é o único tipo (consulte Polimorfismo ). Você não precisa criar uma hierarquia de classes para executar o polimorfismo (e na verdade eu argumentaria que a herança não é o método mais comum para obter o subtipo de polimorfismo, a implementação de uma interface é muito mais prevalente).
Vincent Savard

24
Se você definir uma Eaterinterface com o eat()método, então, como cliente, você não se importará que uma Humanimplementação tenha que chamar pela primeira vez washHands()e getDressed()são detalhes da implementação dessa classe. Se, como cliente, você se importa com esse fato, provavelmente não está usando a ferramenta correta para o trabalho.
Vincent Savard

3
Você também deve considerar que, de manhã, um ser humano pode precisar getDressedantes dele eat, não é o caso do almoço. Dependendo das circunstâncias, washHands();if !dressed then getDressed();[code to actually eat]pode ser a melhor maneira de implementar isso para um ser humano. Outra possibilidade é o que acontece se outras coisas exigirem isso washHandse / ou getDressedforem chamadas? Suponha que você tenha leaveForWork? Pode ser necessário estruturar o fluxo do seu programa para que ele seja chamado muito antes disso.
Duncan X Simpson

1
Lembre-se de que verificar o tipo exato pode ser um cheiro de código no OOP, mas é uma prática muito comum no FP (ou seja, use a correspondência de padrões para determinar o tipo de uma união discriminada e, em seguida, agir sobre ela).
Theodoros Chatzigiannakis

3
Cuidado com exemplos de sala de hierarquias OO da escola, como animais. Programas reais quase nunca têm taxonomias tão limpas. Por exemplo, ericlippert.com/2015/04/27/wizards-and-warriors-part-one . Ou, se você quiser ficar louco e questionar todo o paradigma: a programação orientada a objetos é ruim .
Jpmc26

Respostas:


18

Depende. Infelizmente não há solução genérica. Pense nos seus requisitos e tente descobrir o que essas coisas devem fazer.

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

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

Se nada mais funcionar, você pode tentar o Padrão do Visitante , que é uma espécie de hack para permitir um tipo de despacho dinâmico, no qual você pode enviar um visitante que receberá retornos de tipo exato dos animais. Eu enfatizaria, no entanto, que esse deveria ser o último recurso se tudo mais falhar.


33

Essa é 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 supera isso, mas não tenho certeza de que seja esse o caso. A maioria dos desenvolvedores, na minha experiência, acaba usando pacotes de propriedades pseudo-OO .

Primeiro, deixe-me esclarecer. Isso não é culpa sua. A maneira como o OO é ensinado é altamente falha. O Animalexemplo é o principal infrator, a IMO. Basicamente, dizemos, vamos falar sobre objetos, o que eles podem fazer. Uma Animallata 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á chegando ao OO na direção errada. Por que existem animais neste programa e por que eles precisam falar e comer?

É difícil pensar em um uso real para um Animaltipo. Tenho certeza de que existe, mas vamos discutir algo que 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 todo tipo de coisas que os pedestres e trens, mas manteremos simples.

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

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

Provavelmente é muito simples, mas é um começo. Agora. E quanto a todas as outras coisas que um veículo pode fazer? Eles podem sair de uma estrada e entrar em uma vala. Isso faz parte da simulação? Não. Não precisa disso. Alguns carros e ônibus têm sistemas hidráulicos que permitem saltar ou se ajoelhar, respectivamente. Isso faz parte da simulação? Não. Não precisa disso. A maioria dos carros queima gasolina. Alguns não. A usina faz 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 lazer? Não preciso deles.

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 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. Depois que você fizer isso, comece a definir classes que implementam esses comportamentos. Os 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 posteriormente sem alterar o código de chamada. A única maneira de funcionar é se as necessidades do código de chamada determinarem o que ocorre na interface. Não há como definir todos os comportamentos de todas as coisas possíveis que poderiam ser pensadas mais tarde.


13
Esta é uma boa resposta. "Para esse fim, geralmente é melhor criar interfaces OO a partir do código que interage com elas". Absolutamente, e eu diria que é o único caminho. Você não pode conhecer o contrato público de uma interface apenas através da implementação, ela é sempre definida da perspectiva de seus clientes. (E como uma nota lateral, é isso que TDD é realmente sobre.)
Vincent Savard

@VincentSavard "Eu diria que é o único caminho." Você está certo. Eu acho que a razão de eu não ter tornado isso absoluto é que, uma vez que você tenha a idéia, você pode definir a interface e refiná-la dessa maneira. Em última análise, quando você começa a usar tachinhas de latão, é a única coisa que importa.
19418 JimmyJames

@ jpmc26 Talvez um pouco fortemente redigido. Não tenho certeza se concordo que é raro implementar isso. Não tenho certeza de como as interfaces podem ser úteis se você não as estiver usando dessa maneira, além das interfaces de marcador, que eu acho uma péssima idéia.
21418 JimmyJames

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 eat()exemplo.

É 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ê quiser alguém para vir até você para comer o pequeno-almoço com você, você não dizer -lhes para lavar as mãos e se vestir, eles fazem isso por conta própria quando você convidá-los, ou eles respondem "Não, eu não posso vir acabou, não lavei as mãos e ainda não estou vestida ".

Voltar ao software:

Como um Humanexemplo, não vai comer sem as pré-condições, eu teria o Human's eat()método de fazer washHands()e getDressed()se isso não foi feito. Não deve ser seu trabalho como eat()interlocutor saber sobre essa peculiaridade. A alternativa do humano teimoso seria lançar uma exceção ("não estou preparado para comer!") Se as condições prévias não fossem cumpridas, deixando-o frustrado, mas pelo menos informado que comer não funcionava.

Que tal makeEggs()?

Eu recomendaria mudar sua maneira de pensar. Você provavelmente deseja executar as tarefas matinais programadas de todos os seres. Novamente, como interlocutor, não deve ser seu trabalho saber quais são os deveres deles. Então, eu recomendaria um doMorningDuties()método que todas as classes implementem.


Eu concordo com esta resposta. Narek está certo sobre o cheiro do código. É o design da interface que é malcheiroso, então conserte isso e o seu bem.
Jonathan van de Veen

O que esta resposta descreve é ​​geralmente chamado de Princípio de Substituição de Liskov .
Philipp

2

A resposta é bem simples.

Como lidar com objetos que podem fazer mais do que você espera?

Você não precisa lidar com isso, porque não serve para nada. Uma interface geralmente é projetada dependendo de como será usada. Se sua interface não definir lavar as mãos, você não se preocupará com isso como o responsável pela chamada; se o fizesse, o projetaria de maneira diferente.

Por exemplo, se for hora da manhã e se for apenas um animal, você chamará comer, caso contrário, se for humano, chame primeiro washHands, getDressed e só então coma. Como lidar com esses casos?

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ê implementar IMorningPerformerpara Animalapenas realizar comer, e para Humanvocê também implementá-lo para lavar as mãos e se vestir. Quem chama o método MorningTime pode se importar menos se for humano ou animal. Tudo o que ela quer é a rotina matinal realizada, que cada objeto faz admiravelmente graças ao OO.

Polimorfismo morre.

Ou faz?

Você precisa descobrir o tipo do objeto

Por que está assumindo isso? Eu acho que isso pode ser uma suposição errada.

Existe uma abordagem comum para lidar com esses casos?

Sim, geralmente é resolvido com hierarquia de classe ou interface cuidadosamente projetada. Observe que no exemplo acima não há nada que contradiga o seu exemplo, como você o deu, mas provavelmente você se sentirá insatisfeito, porque fez mais algumas suposições que não escreveu na pergunta no momento em que escrevia. , e essas suposições provavelmente são violadas.

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

Projetar boas hierarquias de classe é difícil e requer muita percepção do domínio de negócios. Para domínios complexos, é feita uma repetição de duas, três ou mais iterações, pois elas refinam o entendimento de como as diferentes entidades em seu domínio de negócios interagem, até chegarem a um modelo adequado.

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


Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.