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.
" 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
de Penguin.fly()
.
class Penguin < Bird; undef fly; end;
. Se você deve é outra pergunta.
function save_yourself_from_crashing_airplane(Bird b) { f.fly() }
seria muito mais complicado. (como disse Peter Török, viola LSP)