Uma falha bem conhecida das hierarquias de classe tradicionais é que elas são ruins quando se trata de modelar o mundo real. Como exemplo, tentando representar as espécies de animais com classes. Na verdade, existem vários problemas ao fazer isso, mas um que eu nunca vi uma solução é quando uma subclasse "perde" um comportamento ou propriedade definida em uma superclasse, como um pinguim que não pode voar (não provavelmente são exemplos melhores, mas esse é o primeiro que me vem à cabeça).
Por um lado, você não deseja definir, para cada propriedade e comportamento, algum sinalizador que especifique se ele está presente, e verificá-lo sempre antes de acessar esse comportamento ou propriedade. Você gostaria apenas de dizer que os pássaros podem voar, de maneira simples e clara, na classe Bird. Mas seria bom se alguém pudesse definir "exceções" depois, sem ter que usar alguns hacks horríveis em todos os lugares. Isso geralmente acontece quando um sistema é produtivo há um tempo. De repente, você encontra uma "exceção" que não se encaixa no design original e não deseja alterar grande parte do seu código para acomodá-lo.
Portanto, existem alguns padrões de linguagem ou design que podem lidar com esse problema de maneira limpa, sem exigir grandes alterações na "superclasse" e todo o código que o utiliza? Mesmo que uma solução lide apenas com um caso específico, várias soluções juntas podem formar uma estratégia completa.
Depois de pensar mais, percebo que esqueci o Princípio da Substituição de Liskov. É por isso que você não pode fazer isso. Supondo que você defina "características / interfaces" para todos os principais "grupos de recursos", é possível implementar livremente características em diferentes ramos da hierarquia, como a característica Voar que pode ser implementada pelo Birds, e algum tipo especial de esquilo e peixe.
Portanto, minha pergunta pode ser "Como eu posso desimplementar uma característica?" Se a sua superclasse for Java Serializable, você também deve ser um, mesmo que não haja como serializar seu estado, por exemplo, se você continha um "Socket".
Uma maneira de fazer isso é sempre definir todas as suas características em pares desde o início: Flying e NotFlying (que lançariam UnsupportedOperationException, se não for verificado). O Não-traço não definiria nenhuma nova interface e poderia ser simplesmente verificado. Soa como uma solução "barata", especialmente se usada desde o início.
fonte
function save_yourself_from_crashing_airplane(Bird b) { f.fly() }
seria muito mais complicado. (como disse Peter Török, viola LSP)" it would be nice if one could define "exceptions" afterward, without having to use some horrible hacks everywhere"
você considera um método de fábrica que controla o comportamento hacky?NotSupportedException
dePenguin.fly()
.class Penguin < Bird; undef fly; end;
. Se você deve é outra pergunta.Respostas:
Como outros já mencionaram, você teria que ir contra o LSP.
No entanto, pode-se argumentar que uma subclasse é apenas uma extensão arbitrária de uma superclasse. É um novo objeto por si só e a única relação com a superclasse é que ele é usado como base.
Isso pode fazer sentido lógico, ao invés de dizer que o pinguim é um pássaro. Você está dizendo que o Penguin herda algum subconjunto de comportamento de Bird.
Geralmente, as linguagens dinâmicas permitem que você expresse isso facilmente, um exemplo usando JavaScript a seguir:
Nesse caso específico,
Penguin
está ativamente sombreando oBird.fly
método herdado gravando umafly
propriedade com valorundefined
no objeto.Agora você pode dizer que
Penguin
não pode mais ser tratado como normalBird
. Mas, como mencionado, no mundo real, simplesmente não pode. Porque estamos modelandoBird
como uma entidade voadora.A alternativa é não assumir que o Bird's possa voar. Seria sensato ter uma
Bird
abstração que permita a todos os pássaros herdar dela, sem falhas. Isso significa apenas fazer suposições que todas as subclasses podem conter.Geralmente a idéia de Mixin se aplica bem aqui. Tenha uma classe base muito fina e misture todos os outros comportamentos a ela.
Exemplo:
Se você está curioso, eu tenho uma implementação de
Object.make
Adição:
Você não "desimplementa" uma característica. Você simplesmente corrige sua hierarquia de herança. Ou você pode cumprir seu contrato de superclasse ou não deve estar fingindo que é desse tipo.
É aqui que a composição do objeto brilha.
Além disso, Serializable não significa que tudo deve ser serializado, apenas significa que "o estado de seu interesse" deve ser serializado.
Você não deve usar uma característica "NotX". Isso é apenas um inchaço horrível de código. Se uma função espera um objeto voador, ela deve travar e queimar quando você dá um mamute.
fonte
AFAIK todas as línguas baseadas em herança são construídas no Princípio de Substituição de Liskov . Remover / desabilitar uma propriedade de classe base em uma subclasse violaria claramente o LSP, portanto, não acho que essa possibilidade seja implementada em nenhum lugar. O mundo real é realmente confuso e não pode ser modelado com precisão por abstrações matemáticas.
Algumas línguas fornecem características ou mixins, precisamente para lidar com esses problemas de maneira mais flexível.
fonte
Class
é uma subclasse deModule
mesmo queClass
não-é-AModule
. Mas ainda faz sentido ser uma subclasse, pois reutiliza grande parte do código. OTOH,StringIO
IS-AIO
, mas os dois não têm nenhum relacionamento de herança (além do óbvio de ambos herdaremObject
, é claro), porque não compartilham nenhum código. As classes são para compartilhamento de código, os tipos são para descrever protocolos.IO
eStringIO
têm o mesmo protocolo, portanto, o mesmo tipo, mas suas classes não são relacionadas.Fly()
está no primeiro exemplo em: Head First Design Patterns para The Strategy Pattern , e esta é uma boa situação para explicar por que você deve "Favorecer a composição sobre a herança". .Você pode misturar composição e herança tendo supertipos de
FlyingBird
,FlightlessBird
que têm o comportamento correto injetado por uma fábrica, que os subtipos relevantes, por exemplo,Penguin : FlightlessBird
são automaticamente, e qualquer outra coisa realmente específica é manipulada pela fábrica, como é óbvio.fonte
O problema real que você está assumindo não
Bird
é umFly
método? Por que não:Agora, o problema óbvio é a herança múltipla (
Duck
), então o que você realmente precisa são de interfaces:fonte
Primeiro, SIM, qualquer linguagem que permita fácil modificação dinâmica de objeto permitiria que você fizesse isso. No Ruby, por exemplo, você pode remover facilmente um método.
Mas como Péter Török disse, isso violaria o LSP .
Nesta parte, vou esquecer o LSP e assumir que:
Você disse :
Parece que o que você quer é " pedir perdão, em vez de permissão ", do Python
Apenas faça seu Penguin lançar uma exceção ou herdar de uma classe NonFlyingBird que lança uma exceção (pseudo-código):
A propósito, o que você escolher: levantando uma exceção ou removendo um método, no final, o seguinte código (supondo que seu idioma suporte a remoção de método):
lançará uma exceção de tempo de execução.
fonte
Como alguém apontado acima nos comentários, pinguins são pássaros, pinguins não voam, portanto nem todos os pássaros podem voar.
Portanto, Bird.fly () não deve existir ou pode não funcionar. Eu prefiro o primeiro.
Tendo o FlyingBird estendido, o Bird tem um método .fly () que seria correto, é claro.
fonte
O verdadeiro problema com o exemplo do fly () é que a entrada e a saída da operação não estão definidas corretamente. O que é necessário para um pássaro voar? E o que acontece depois que o vôo é bem-sucedido? Os tipos de parâmetros e tipos de retorno para a função fly () devem ter essas informações. Caso contrário, seu design depende de efeitos colaterais aleatórios e tudo pode acontecer. A parte de qualquer coisa é o que causa todo o problema, a interface não está definida corretamente e todos os tipos de implementação são permitidos.
Então, em vez disso:
Você deve ter algo parecido com isto:
Agora, define explicitamente os limites da funcionalidade - seu comportamento de vôo tem apenas uma flutuação para decidir - a distância do solo, quando determinada a posição. Agora, todo o problema se resolve automaticamente. Um pássaro que não pode voar apenas retorna 0,0 dessa função e nunca sai do chão. É um comportamento correto para isso e, uma vez decidido um float, você sabe que implementou completamente a interface.
Pode ser difícil codificar um comportamento real para os tipos, mas essa é a única maneira de especificar suas interfaces corretamente.
Editar: eu quero esclarecer um aspecto. Essa versão float-> float da função fly () também é importante porque define um caminho. Esta versão significa que o único pássaro não pode se duplicar magicamente enquanto está voando. É por isso que o parâmetro é flutuação única - é a posição no caminho que o pássaro segue. Se você quiser caminhos mais complexos, então Point2d posinpath (float x); que usa o mesmo x da função fly ().
fonte
Tecnicamente, você pode fazer isso em praticamente qualquer linguagem dinâmica / tipo typo (JavaScript, Ruby, Lua, etc.), mas é quase sempre uma péssima idéia. Remover métodos de uma classe é um pesadelo de manutenção, semelhante ao uso de variáveis globais (ou seja, você não pode dizer em um módulo que o estado global não foi modificado em outro lugar).
Bons padrões para o problema que você descreveu são Decorator ou Strategy, projetando uma arquitetura de componentes. Basicamente, em vez de remover comportamentos desnecessários das subclasses, você cria objetos adicionando os comportamentos necessários. Então, para construir a maioria dos pássaros, você adicionaria o componente voador, mas não o adicione aos seus pingüins.
fonte
Peter mencionou o Princípio da Substituição de Liskov, mas acho que isso precisa ser explicado.
Assim, se um pássaro (objeto x do tipo T) puder voar (q (x)), um pinguim (objeto y do tipo S) poderá voar (q (y)), por definição. Mas esse claramente não é o caso. Existem também outras criaturas que podem voar, mas não são do tipo Bird.
Como você lida com isso depende do idioma. Se um idioma suporta herança múltipla, você deve usar uma classe abstrata para criaturas que possam voar; se um idioma preferir interfaces, essa é a solução (e a implementação do fly deve ser encapsulada em vez de herdada); ou, se um idioma suportar Duck Typing (sem trocadilhos), basta implementar um método fly nas classes que podem e chamar, se houver.
Mas todas as propriedades de uma superclasse devem ser aplicadas a todas as suas subclasses.
[Em resposta à edição]
A aplicação de uma "característica" do CanFly ao Bird não é melhor. Ainda está sugerindo o código de chamada que todos os pássaros podem voar.
Uma característica nos termos que você definiu é exatamente o que Liskov quis dizer quando disse "propriedade".
fonte
Deixe-me começar mencionando (como todo mundo) o Princípio da Substituição de Liskov, que explica por que você não deve fazer isso. No entanto, a questão do que você deve fazer é de design. Em alguns casos, pode não ser importante que o Penguin não consiga voar. Talvez você possa fazer com que o Penguin jogue InsufficientWingsException quando solicitado a voar, desde que você esteja claro na documentação de Bird :: fly (), que pode ser usado para pássaros que não podem voar. De ter um teste para ver se ele realmente pode voar, embora isso incha a interface.
A alternativa é reestruturar suas aulas. Vamos criar a classe "FlyingCreature" (ou melhor, uma interface, se você estiver lidando com o idioma que permite). "Bird" não herda de FlyingCreature, mas você pode criar "FlyingBird" que sim. Cotovia, abutre e águia todos herdam de FlyingBird. Pinguim não. Apenas herda de Bird.
É um pouco mais complicado que a estrutura ingênua, mas tem a vantagem de ser preciso. Você notará que todas as classes esperadas estão lá (Bird) e o usuário geralmente pode ignorar as 'inventadas' (FlyingCreature) se não for importante se sua criatura pode voar ou não.
fonte
A maneira típica de lidar com essa situação é lançar algo como um
UnsupportedOperationException
resp (Java).NotImplementedException
(C #).fonte
Muitas boas respostas com muitos comentários, mas nem todas concordam, e só posso escolher uma, por isso vou resumir aqui todas as visões com as quais concordo.
0) Não assuma "digitação estática" (fiz quando perguntei, porque faço Java quase exclusivamente). Basicamente, o problema depende muito do tipo de idioma usado.
1) Deve-se separar a hierarquia de tipos da hierarquia de reutilização de código no design e na cabeça, mesmo se elas se sobrepuserem principalmente. Geralmente, use classes para reutilização e interfaces para tipos.
2) A razão pela qual o Bird IS-A Fly normalmente é porque a maioria dos pássaros pode voar, por isso é prático do ponto de vista da reutilização de código, mas dizer que o Bird IS-A Fly está realmente errado, pois há pelo menos uma exceção (Pinguim).
3) Nas linguagens estática e dinâmica, você pode simplesmente lançar uma exceção. Mas isso só deve ser usado se for declarado explicitamente no "contrato" da classe / interface que declara a funcionalidade, caso contrário, é uma "quebra de contrato". Isso também significa que agora você precisa estar preparado para capturar a exceção em todos os lugares, para escrever mais código no site de chamada, e é um código feio.
4) Em algumas linguagens dinâmicas, é realmente possível "remover / ocultar" a funcionalidade de uma superclasse. Se a verificação da presença da funcionalidade é como você verifica o "IS-A" nesse idioma, essa é uma solução adequada e sensata. Se, por outro lado, a operação "IS-A" é outra coisa que ainda diz que seu objeto "deveria" implementar a funcionalidade que está faltando, então o código de chamada assumirá que a funcionalidade está presente e a chamará e falhará, portanto é uma espécie de quantia para lançar uma exceção.
5) A melhor alternativa é realmente separar a característica Fly da característica Bird. Portanto, um pássaro voador precisa estender / implementar explicitamente o Bird e o Fly / Flying. Este é provavelmente o design mais limpo, pois você não precisa "remover" nada. A única desvantagem é agora que quase todos os pássaros precisam implementar o Bird e o Fly, para que você escreva mais código. A maneira de contornar isso é ter uma classe intermediária FlyingBird, que implementa Bird e Fly, e representa o caso comum, mas essa solução alternativa pode ser de uso limitado, sem herança múltipla.
6) Outra alternativa que não requer herança múltipla é usar composição em vez de herança. Cada aspecto de um animal é modelado por uma classe independente, e um pássaro concreto é uma composição de pássaro, e possivelmente voa ou nada, ... Você obtém a reutilização completa do código, mas precisa executar uma ou mais etapas adicionais para obter a funcionalidade Flying, quando você tem uma referência de um pássaro concreto. Além disso, o idioma natural "objeto IS-A Fly" e "objeto AS-A Fly (elenco)" não funcionará mais, portanto, você deve inventar sua própria sintaxe (algumas linguagens dinâmicas podem ter uma maneira de contornar isso). Isso pode tornar seu código mais complicado.
7) Defina sua característica Fly, de forma que ofereça uma saída clara para algo que não pode voar. Fly.getNumberOfWings () pode retornar 0. Se Fly.fly (direction, currentPotinion) retornar a nova posição após o vôo, então Penguin.fly () poderá retornar a currentPosition sem alterá-la. Você pode acabar com um código que tecnicamente funciona, mas existem algumas ressalvas. Em primeiro lugar, algum código pode não ter um comportamento óbvio de "não fazer nada". Além disso, se alguém chamar x.fly (), esperaria que ele fizesse algo , mesmo que o comentário diga que fly () não pode fazer nada . Finalmente, o pingüim IS-A Flying ainda retornaria verdadeiro, o que pode ser confuso para o programador.
8) Faça como 5), mas use a composição para contornar casos que exigiriam herança múltipla. Essa é a opção que eu preferiria para uma linguagem estática, pois 6) parece mais complicado (e provavelmente requer mais memória porque temos mais objetos). Uma linguagem dinâmica pode tornar 6) menos complicado, mas duvido que seja menos complicado que 5).
fonte
Defina um comportamento padrão (marque-o como virtual) na classe base e substitua-o conforme necessário. Dessa forma, todos os pássaros podem "voar".
Até os pinguins voam, deslizando pelo gelo a zero altitude!
O comportamento de voar pode ser substituído, se necessário.
Outra possibilidade é ter uma interface Fly. Nem todos os pássaros implementam essa interface.
As propriedades não podem ser removidas, por isso é importante saber quais propriedades são comuns em todos os pássaros. Eu acho que é mais um problema de design garantir que propriedades comuns sejam implementadas no nível base.
fonte
Eu acho que o padrão que você está procurando é um bom polimorfismo antigo. Embora você possa remover uma interface de uma classe em alguns idiomas, provavelmente não é uma boa ideia pelos motivos apresentados por Péter Török. Em qualquer linguagem OO, no entanto, você pode substituir um método para alterar seu comportamento, e isso inclui não fazer nada. Para emprestar seu exemplo, você pode fornecer um método Penguin :: fly () que execute um dos seguintes procedimentos:
As propriedades podem ser um pouco mais fáceis de adicionar e remover, se você planejar com antecedência. Você pode armazenar propriedades em um mapa / dicionário / matriz associativa em vez de usar variáveis de instância. Você pode usar o padrão Factory para produzir instâncias padrão dessas estruturas, para que um Bird proveniente da BirdFactory sempre comece com o mesmo conjunto de propriedades. A codificação de valor-chave da Objective-C é um bom exemplo desse tipo de coisa.
Nota: A lição séria dos comentários abaixo é que, embora substituir a remoção de um comportamento possa funcionar, nem sempre é a melhor solução. Se você precisar fazer isso de maneira significativa, considere um sinal forte de que seu gráfico de herança é defeituoso. Nem sempre é possível refatorar as classes das quais você herda, mas quando é, geralmente é a melhor solução.
Usando seu exemplo do Penguin, uma maneira de refatorar seria separar a habilidade de vôo da classe Bird. Como nem todos os pássaros podem voar, incluindo um método fly () no Bird era inapropriado e leva diretamente ao tipo de problema que você está perguntando. Portanto, mova o método fly () (e talvez decolagem () e land ()) para uma classe ou interface Aviator (dependendo do idioma). Isso permite criar uma classe FlyingBird que herda de Bird e Aviator (ou herda de Bird e implementa Aviator). O Penguin pode continuar herdando diretamente do Bird, mas não do Aviator, evitando assim o problema. Esse arranjo também pode facilitar a criação de classes para outros itens voadores: FlyingFish, FlyingMammal, FlyingMachine, AnnoyingInsect e assim por diante.
fonte