Existe um padrão de linguagem ou design que permita a * remoção * de comportamento ou propriedades de objetos em uma hierarquia de classes?

28

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.

Sebastien Diot
fonte
3
"sem ter que usar alguns hacks horríveis em todos os lugares": desabilitar um comportamento é um truque horrível: isso implicaria que function save_yourself_from_crashing_airplane(Bird b) { f.fly() }seria muito mais complicado. (como disse Peter Török, viola LSP)
keppla
Uma combinação do padrão e da herança da estratégia pode permitir que você "compor sobre" o comportamento herdado de supertipos específicos? Quando você diz: " 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?
StuperUser
11
Pode-se, naturalmente, apenas jogar um NotSupportedExceptionde Penguin.fly().
Felix Dombek
No que diz respeito aos idiomas, você certamente pode desimplementar um método em uma classe filho. Por exemplo, em Ruby: class Penguin < Bird; undef fly; end;. Se você deve é outra pergunta.
Nathan Long
Isso quebraria o princípio liskov e, sem dúvida, o ponto principal da OOP.
deadalnix

Respostas:

17

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:

var Penguin = Object.create(Bird);
Penguin.fly = undefined;
Penguin.swim = function () { ... };

Nesse caso específico, Penguinestá ativamente sombreando o Bird.flymétodo herdado gravando uma flypropriedade com valor undefinedno objeto.

Agora você pode dizer que Penguinnão pode mais ser tratado como normal Bird. Mas, como mencionado, no mundo real, simplesmente não pode. Porque estamos modelando Birdcomo uma entidade voadora.

A alternativa é não assumir que o Bird's possa voar. Seria sensato ter uma Birdabstraçã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:

// for some value of Object.make
var Penguin = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Swimmer, ...
);
var Hawk = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Flyer, Carnivore, ...
);

Se você está curioso, eu tenho uma implementação deObject.make

Adição:

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".

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.

Raynos
fonte
10
"no mundo real, simplesmente não pode". Sim pode. Um pinguim é um pássaro. A capacidade de voar não é propriedade de um pássaro, é simplesmente uma propriedade coincidente da maioria das espécies de pássaros. As propriedades que definem os pássaros são "animais de penas, alados, bípedes, endotérmicos, postura de ovos e vertebrados" (Wikipedia) - nada sobre voar para lá.
Pd #
2
@pdr novamente depende da sua definição de pássaro. Como eu estava usando o termo "Pássaro", quis dizer a abstração de classe que usamos para representar os pássaros, incluindo o método fly. Mencionei também que você pode tornar sua abstração de classe menos específica. Também um pinguim não é emplumado.
Raynos
2
@ Raynos: Os pinguins são realmente de penas. Suas penas são bastante curtas e densas, é claro.
Jon Purdy
@ JonPurdy justo o suficiente, eu sempre imagino que eles tinham peles.
21411 Raynos
+1 em geral, e em particular no "mamute". RI MUITO!
Sebastien Diot
28

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.

Péter Török
fonte
11
O LSP é para tipos , não para classes .
Jörg W Mittag
2
@ PéterTörök: Esta questão não existiria de outra forma :-) Eu posso pensar em dois exemplos de Ruby. Classé uma subclasse de Modulemesmo que Classnão-é-A Module. Mas ainda faz sentido ser uma subclasse, pois reutiliza grande parte do código. OTOH, StringIOIS-A IO, mas os dois não têm nenhum relacionamento de herança (além do óbvio de ambos herdarem Object, é claro), porque não compartilham nenhum código. As classes são para compartilhamento de código, os tipos são para descrever protocolos. IOe StringIOtêm o mesmo protocolo, portanto, o mesmo tipo, mas suas classes não são relacionadas.
Jörg W Mittag
11
@ JörgWMittag, OK, agora entendo melhor o que você quer dizer. No entanto, para mim, seu primeiro exemplo parece mais um uso indevido de herança do que a expressão de algum problema fundamental que você parece sugerir. Herança pública O IMO não deve ser usado para reutilizar a implementação, apenas para expressar relacionamentos de subtipo (é-a). E o fato de poder ser mal utilizado não a desqualifica - não consigo imaginar nenhuma ferramenta utilizável de qualquer domínio que não possa ser mal utilizada.
Péter Török
2
Para as pessoas que votaram positivamente nesta resposta: observe que isso realmente não responde à pergunta, especialmente após o esclarecimento editado. Eu não acho que essa resposta mereça um voto negativo, porque o que ele diz é muito verdadeiro e importante saber, mas ele realmente não respondeu à pergunta.
jhocking
11
Imagine um Java em que apenas interfaces são tipos, classes não e subclasses são capazes de "desimplementar" interfaces de suas superclasses, e você tem uma idéia aproximada, eu acho.
Jörg W Mittag
15

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, FlightlessBirdque têm o comportamento correto injetado por uma fábrica, que os subtipos relevantes, por exemplo, Penguin : FlightlessBirdsão automaticamente, e qualquer outra coisa realmente específica é manipulada pela fábrica, como é óbvio.

StuperUser
fonte
11
Mencionei o padrão Decorator em minha resposta, mas o padrão de estratégia também funciona muito bem.
jhocking
11
+1 em "Favorecer a composição sobre a herança". No entanto, a necessidade de padrões de design especiais para implementar a composição em linguagens de tipo estatístico reforça meu viés em relação a linguagens dinâmicas como Ruby.
Roy Tinker
11

O problema real que você está assumindo não Birdé um Flymétodo? Por que não:

class Bird
{
    // features that all birds have
}

class BirdThatCanSwim : Bird
{
    public void Swim() {...};
}

class BirdThatCanFly : Bird
{
    public void Fly() {...};
}


class Penguin : BirdThatCanSwim { }
class Sparrow : BirdThatCanFly { }

Agora, o problema óbvio é a herança múltipla ( Duck), então o que você realmente precisa são de interfaces:

interface IBird { }
interface IBirdThatCanSwim : IBird { public void Swim(); }
interface IBirdThatCanFly : IBird { public void Fly(); }
interface IBirdThatCanQuack : IBird { public void Quack(); }

class Duck : BirdThatCanFly, IBirdThatCanSwim, IBirdThatCanQuack
{
    public void Swim() {...};
    public void Quack() {...};
}
Scott Whitlock
fonte
3
O problema é que a evolução não segue o Princípio de Substituição de Liskov e herda com a remoção de recursos.
Donal Fellows
7

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:

  • Bird é uma classe com um método fly ()
  • Pinguim deve herdar de Bird
  • O pinguim não pode voar ()
  • Não me importo se é um bom design ou se corresponde ao mundo real, como é o exemplo fornecido nesta pergunta.

Você disse :

Por um lado, você não deseja definir para cada propriedade e comportamento algum sinalizador que especifique se ele está presente, e verifique-o sempre antes de acessar esse comportamento ou propriedade

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):

class Penguin extends Bird {
     function fly():void {
          throw new Exception("Hey, I'm a penguin, I can't fly !");
     }
}

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):

var bird:Bird = new Penguin();
bird.fly();

lançará uma exceção de tempo de execução.

David
fonte
"Apenas faça seu Penguin lançar uma exceção ou herdar de uma classe NonFlyingBird que gera uma exceção" Isso ainda é uma violação do LSP. Ainda está sugerindo que um pinguim possa voar, mesmo que sua implementação do fly esteja falhando. Nunca deve haver um método de voar no Penguin.
Pd #
@pdr: não está sugerindo que um pinguim possa voar, mas que deve voar (é um contrato). A exceção dirá que não pode . A propósito, eu não estou afirmando que é uma boa prática de POO, estou apenas dando uma resposta a uma parte da pergunta #
David
O ponto é que não se espera que um pinguim voe apenas porque é um pássaro. Se eu quiser escrever um código que diga "Se x puder voar, faça isso; caso contrário, faça isso". Eu tenho que usar um try / catch na sua versão, onde eu deveria ser capaz de perguntar ao objeto se ele pode voar (existe um método de conversão ou verificação). Pode ser apenas no texto, mas sua resposta implica que lançar uma exceção é compatível com o LSP.
Pd #
@pdr "Eu tenho que usar um try / catch na sua versão" -> esse é o objetivo de pedir perdão em vez de permissão (porque mesmo um pato poderia ter quebrado suas asas e não conseguir voar). Eu vou consertar o texto.
David
"esse é o objetivo de pedir perdão em vez de permissão". Sim, exceto pelo fato de permitir que a estrutura lance o mesmo tipo de exceção para qualquer método ausente, a "try: except AttributeError:" do Python é exatamente equivalente ao C # 's "se (X é Y) {} else {}" e é imediatamente reconhecível assim sendo. Mas se você lançou deliberadamente uma CannotFlyException para substituir a funcionalidade fly () padrão no Bird, ela se torna menos reconhecível.
Pd #
7

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.

alex
fonte
Eu concordo, o Fly deve ser uma interface que o pássaro possa implementar. Pode ser implementado como um método, bem como com o comportamento padrão que pode ser substituído, mas uma abordagem mais limpa está usando uma interface.
precisa
6

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:

class Bird {
public:
   virtual void fly()=0;
};

Você deve ter algo parecido com isto:

   class Bird {
   public:
      virtual float fly(float x) const=0;
   };

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

tp1
fonte
11
Eu gosto bastante da sua resposta. Eu acho que merece mais votos.
Sebastien Diot
2
Excelente resposta. O problema é que a pergunta apenas agita as mãos sobre o que fly () realmente faz. Qualquer implementação real do fly teria, no mínimo, um destino - fly (destino do Coordinate) que, no caso do pinguim, poderia ser substituído para implementar {return currentPosition)}
Chris Cudmore
4

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.

jhocking
fonte
3

Peter mencionou o Princípio da Substituição de Liskov, mas acho que isso precisa ser explicado.

Seja q (x) uma propriedade comprovável sobre objetos x do tipo T. Então q (y) deve ser comprovável para objetos y do tipo S, em que S é um subtipo de T.

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".

pdr
fonte
2

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.

DJClayworth
fonte
0

A maneira típica de lidar com essa situação é lançar algo como um UnsupportedOperationExceptionresp (Java). NotImplementedException(C #).

user281377
fonte
Contanto que você documente essa possibilidade no Bird.
quer
0

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).

Sebastien Diot
fonte
0

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.

class eagle : bird, IFly
class penguin : bird

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.

Jon Raynor
fonte
-1

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:

  • nada
  • lança uma exceção
  • chama o método Penguin :: swim ()
  • afirma que o pinguim está debaixo d'água (eles meio que "voam" pela água)

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.

Caleb
fonte
2
-1 por sugerir chamar Penguin :: swim (). Isso viola o princípio de menos espanto e fará com que programadores de manutenção em todos os lugares xinguem seu nome.
quer
11
@DJClayworth Como o exemplo estava do lado ridículo, em primeiro lugar, o voto negativo por violação do comportamento inferido de fly () e swim () parece um pouco demais. Mas se você realmente quer olhar para isso com seriedade, eu concordo que é mais provável que você vá para o outro lado e implemente swim () em termos de fly (). Os patos nadam remando com os pés; pinguins nadam batendo as asas.
Caleb
11
Concordo que a pergunta foi tola, mas o problema é que já vi pessoas fazendo isso na vida real - use chamadas existentes que "realmente não fazem nada" para implementar funcionalidades raras. Ele realmente estraga o código e geralmente termina com a necessidade de escrever "if (! (MyBird instanceof Penguin)) fly ();" em muitos lugares, enquanto espera que ninguém crie uma classe de avestruz.
quer
A afirmação é ainda pior. Se eu tiver uma matriz de Birds, todos com o método fly (), não quero uma falha de asserção ao chamar fly ().
DJClayworth
11
Não li a documentação do Penguin , porque recebi uma variedade de Birds e não sabia que um Penguin estaria na matriz. Eu li a documentação do Bird que dizia que quando eu chamo fly () o pássaro voa. Se essa documentação declarasse claramente que uma exceção poderia ser lançada se o pássaro não estivesse em vôo, eu teria permitido isso. Se dissesse que chamar fly () às vezes fazia nadar, eu teria mudado para usar uma biblioteca de classes diferente. Ou foi tomar uma bebida muito grande.
DJClayworth