Um colega meu criou uma regra prática para escolher entre criar uma classe base ou uma interface.
Ele diz:
Imagine cada novo método que você está prestes a implementar. Para cada uma delas, considere o seguinte: esse método será implementado por mais de uma classe exatamente nesta forma, sem nenhuma alteração? Se a resposta for "sim", crie uma classe base. Em qualquer outra situação, crie uma interface.
Por exemplo:
Considere as classes
cat
edog
, que estendem a classemammal
e têm um único métodopet()
. Em seguida, adicionamos a classealligator
, que não estende nada e tem um único métodoslither()
.Agora, queremos adicionar um
eat()
método para todos eles.Se a implementação do
eat()
método for exatamente a mesma paracat
,dog
ealligator
, devemos criar uma classe base (digamosanimal
), que implementa esse método.No entanto, se sua implementação
alligator
diferir da menor maneira possível, devemos criar umaIEat
interface e criarmammal
ealligator
implementar.
Ele insiste que esse método cobre todos os casos, mas me parece uma simplificação excessiva.
Vale a pena seguir esta regra de ouro?
fonte
alligator
A implementação deeat
difere, é claro, na medida em que aceitacat
edog
como parâmetros.is a
e as interfaces sãoacts like
ouis
. Então, umis a
mamífero de cachorro eacts like
um comedor. Isso nos diria que o mamífero deve ser uma classe e o comedor deve ser uma interface. Sempre foi um guia muito útil. Nota: Um exemplo deis
seriaThe cake is eatable
ouThe book is writable
.Respostas:
Eu não acho que essa seja uma boa regra de ouro. Se você está preocupado com a reutilização de código, pode implementar uma
PetEatingBehavior
função que define as funções alimentares de cães e gatos. Então você pode terIEat
e reutilizar o código juntos.Hoje em dia vejo cada vez menos razões para usar a herança. Uma grande vantagem é a facilidade de uso. Tome uma estrutura de GUI. Uma maneira popular de projetar uma API para isso é expor uma enorme classe base e documentar quais métodos o usuário pode substituir. Portanto, podemos ignorar todas as outras coisas complexas que nosso aplicativo precisa gerenciar, pois uma implementação "padrão" é dada pela classe base. Mas ainda podemos personalizar as coisas reimplementando o método correto quando necessário.
Se você se apegar à interface da sua API, seu usuário geralmente tem mais trabalho a fazer para fazer exemplos simples funcionarem. Porém, a API com interfaces geralmente é menos acoplada e mais fácil de manter pelo simples motivo que
IEat
contém menos informações que aMammal
classe base. Portanto, os consumidores dependerão de um contrato mais fraco, você terá mais oportunidades de refatoração, etc.Para citar Rich Hickey: Simples! = Fácil
fonte
PetEatingBehavior
implementação?PetEatingBehavior
provavelmente é o errado. Não posso fornecer uma implementação, pois este é apenas um exemplo de brinquedo. Mas posso descrever as etapas de refatoração: ele possui um construtor que usa todas as informações usadas por gatos / cães para definir oeat
método (instância de dentes, instância de estômago, etc.). Ele contém apenas o código comum a cães e gatos usados em seueat
método. Você simplesmente precisa criar umPetEatingBehavior
membro e encaminhar as chamadas para dog.eat/cat.eat.É uma regra prática decente, mas conheço alguns lugares em que a violo.
Para ser justo, eu uso classes base (abstratas) muito mais do que meus colegas. Faço isso como programação defensiva.
Para mim (em muitos idiomas), uma classe base abstrata adiciona a principal restrição de que pode haver apenas uma classe base. Ao optar por usar uma classe base em vez de uma interface, você está dizendo "esta é a funcionalidade principal da classe (e de qualquer herdeiro) e é inapropriado misturar-se com outras funcionalidades". As interfaces são o oposto: "Esta é uma característica de um objeto, não necessariamente sua principal responsabilidade".
Esse tipo de opção de design cria guias implícitos para seus implementadores sobre como eles devem usar seu código e, por extensão, escreva seu código. Devido ao seu maior impacto na base de código, costumo levar essas coisas em consideração mais fortemente ao tomar a decisão de design.
fonte
O problema com a simplificação do seu amigo é que determinar se um método precisará ou não mudar é excepcionalmente difícil. É melhor usar uma regra que fale com a mentalidade por trás de superclasses e interfaces.
Em vez de descobrir o que você deve fazer olhando para o que você acha que é a funcionalidade, observe a hierarquia que você está tentando criar.
Aqui está como um professor meu ensinou a diferença:
As superclasses devem ser
is a
e as interfaces sãoacts like
ouis
. Então, umis a
mamífero de cachorro eacts like
um comedor. Isso nos diria que o mamífero deve ser uma classe e o comedor deve ser uma interface. Um exemplo deis
seriaThe cake is eatable
orThe book is writable
(making botheatable
andwritable
interfaces).Usar uma metodologia como essa é razoavelmente simples e fácil e faz com que você codifique a estrutura e os conceitos, e não o que você acha que o código fará. Isso facilita a manutenção, o código mais legível e o design mais simples de criar.
Independentemente do que o código possa realmente dizer, se você usar um método como esse, poderá voltar daqui a cinco anos e usar o mesmo método para refazer suas etapas e descobrir facilmente como o seu programa foi projetado e como ele interage.
Apenas meus dois centavos.
fonte
A pedido do exizt, estou expandindo meu comentário para uma resposta mais longa.
O maior erro que as pessoas cometem é acreditar que as interfaces são simplesmente classes abstratas vazias. Uma interface é uma maneira de um programador dizer: "Eu não me importo com o que você me dá, desde que siga esta convenção".
A biblioteca .NET serve como um exemplo maravilhoso. Por exemplo, quando você escreve uma função que aceita um
IEnumerable<T>
, o que você está dizendo é: "Não ligo para como você está armazenando seus dados. Só quero saber que posso usar umforeach
loop.Isso leva a um código muito flexível. De repente, para ser integrado, tudo o que você precisa fazer é seguir as regras das interfaces existentes. Se a implementação da interface for difícil ou confusa, talvez seja uma dica de que você está tentando enfiar uma estaca quadrada em um buraco redondo.
Mas surge a pergunta: "E a reutilização de código? Meus professores de CS me disseram que a herança era a solução para todos os problemas de reutilização de código e que a herança permite que você escreva uma vez e use em todos os lugares, além de curar a menangite no processo de resgate de órfãos. dos mares subindo e não haveria mais lágrimas e assim por diante etc. etc. etc. "
Usar herança apenas porque você gosta do som das palavras "reutilização de código" é uma péssima idéia. O guia de estilo de código do Google mostra esse ponto de maneira bastante concisa:
Para ilustrar por que a herança nem sempre é a resposta, vou usar uma classe chamada MySpecialFileWriter †. Uma pessoa que acredita cegamente que a herança é a solução para todos os problemas argumentaria que você deveria tentar herdar
FileStream
, para que não dupliqueFileStream
o código. Pessoas inteligentes reconhecem que isso é estúpido. Você deve apenas ter umFileStream
objeto em sua classe (como uma variável local ou membro) e usar sua funcionalidade.O
FileStream
exemplo pode parecer artificial, mas não é. Se você tiver duas classes que implementam a mesma interface exatamente da mesma maneira, deverá ter uma terceira classe que encapsule qualquer operação duplicada. Seu objetivo deve ser escrever classes que são blocos reutilizáveis independentes que podem ser montados como legos.Isso não significa que a herança deve ser evitada a todo custo. Há muitos pontos a serem considerados e a maioria será abordada pesquisando a questão "Composição versus herança". Nosso próprio Stack Overflow tem algumas boas respostas sobre o assunto.
No final do dia, os sentimentos de seu colega de trabalho não têm a profundidade ou o entendimento necessários para tomar uma decisão informada. Pesquise o assunto e descubra por si mesmo.
† Ao ilustrar herança, todos usam animais. Isso é inútil. Em 11 anos de desenvolvimento, nunca escrevi uma classe chamada
Cat
, por isso não vou usá-la como exemplo.fonte
FileStream
)Os exemplos raramente fazem sentido, principalmente quando se trata de carros ou animais. A maneira de um animal comer (ou processar alimentos) não está realmente ligada à sua herança; não existe uma maneira única de comer que se aplique a todos os animais. Isso depende mais de suas capacidades físicas (as formas de entrada, processamento e saída, por assim dizer). Os mamíferos podem ser carnívoros, herbívoros ou onívoros, por exemplo, enquanto pássaros, mamíferos e peixes podem comer insetos. Esse comportamento pode ser capturado com certos padrões.
Nesse caso, você está melhor abstraindo comportamentos, assim:
Ou implemente um padrão de visitante em que um
FoodProcessor
tenta alimentar animais compatíveis (ICarnivore : IFoodEater
,MeatProcessingVisitor
). O pensamento de animais vivos pode impedi-lo de rasgar o trato digestivo e substituí-lo por um genérico, mas você está realmente fazendo um favor a eles.Isso fará do seu animal uma classe base e, melhor ainda, uma que você nunca precisará mudar novamente ao adicionar um novo tipo de alimento e um processador adequado, para que você possa se concentrar nas coisas que fazem dessa classe animal específica que você é. trabalhando tão único.
Portanto, sim, as classes base têm seu lugar, a regra geral se aplica (geralmente, nem sempre, como é uma regra geral), mas continue procurando o comportamento que você pode isolar.
fonte
IConsumer
interface. Carnívoros podem consumir carne, herbívoros consomem plantas e plantas consomem luz solar e água.Uma interface é realmente um contrato. Não há funcionalidade. Cabe ao implementador implementar. Não há escolha, pois é necessário implementar o contrato.
As classes abstratas oferecem opções aos implementadores. Pode-se optar por ter um método que todos devem implementar, fornecer um método com funcionalidade básica que possa ser substituída ou fornecer alguma funcionalidade básica que todos os herdeiros possam usar.
Por exemplo:
Eu diria que sua regra de ouro também poderia ser tratada por uma classe base. Em particular, como muitos mamíferos provavelmente "comem" o mesmo, usar uma classe base pode ser melhor do que uma interface, pois é possível implementar um método virtual de comer que a maioria dos herdeiros poderia usar e, em seguida, os casos excepcionais podem substituir.
Agora, se a definição de "comer" varia muito de animal para animal, então talvez a interface seja melhor. Às vezes, essa é uma área cinzenta e depende da situação individual.
fonte
Não.
As interfaces são sobre como os métodos são implementados. Se você está baseando sua decisão de quando usar a interface em como os métodos serão implementados, estará fazendo algo errado.
Para mim, a diferença entre interface e classe base é sobre onde estão os requisitos. Você cria a interface, quando algum trecho de código requer classe com API específica e comportamento implícito que emerge dessa API. Você não pode criar uma interface sem código que realmente a chamará.
Por outro lado, as classes base geralmente são usadas como uma maneira de reutilizar o código, então você raramente se incomoda com o código que chamará essa classe base e só leva em consideração a pesquisa de objetos dos quais essa classe base faz parte.
fonte
eat()
em todos os animais? Nós podemos fazermammal
implementá-lo, não podemos? No entanto, entendo sua opinião sobre os requisitos.Não é realmente uma boa regra geral, porque se você quiser implementações diferentes, poderá sempre ter uma classe base abstrata com um método abstrato. É garantido que todos os herdeiros tenham esse método, mas cada um define sua própria implementação. Tecnicamente, você poderia ter uma classe abstrata com nada além de um conjunto de métodos abstratos, e seria basicamente o mesmo que uma interface.
As classes base são úteis quando você deseja uma implementação real de algum estado ou comportamento que possa ser usado em várias classes. Se você se deparar com várias classes que possuem campos e métodos idênticos com implementações idênticas, provavelmente poderá refatorá-lo em uma classe base. Você só precisa ter cuidado com a maneira como herda. Todo campo ou método existente na classe base deve fazer sentido no herdeiro, especialmente quando é convertido como a classe base.
As interfaces são úteis quando você precisa interagir com um conjunto de classes da mesma maneira, mas não pode prever ou não se importa com a implementação real. Tomemos, por exemplo, algo como uma interface de coleção. Deus conhece apenas todas as inúmeras maneiras pelas quais alguém pode decidir implementar uma coleção de itens. Lista vinculada? Lista de matrizes? Pilha? Fila? Esse método SearchAndReplace não se importa, só precisa ser passado para algo que ele pode chamar Adicionar, Remover e GetEnumerator.
fonte
Estou assistindo as respostas a esta pergunta, para ver o que as outras pessoas têm a dizer, mas vou dizer três coisas que estou inclinado a pensar sobre isso:
As pessoas depositam muita fé nas interfaces e pouca fé nas classes. As interfaces são essencialmente classes básicas muito diluídas, e há ocasiões em que usar algo leve pode levar a coisas como código excessivo.
Não há nada errado em substituir um método, chamar
super.method()
dentro dele e deixar o resto do seu corpo fazer coisas que não interferem na classe base. Isso não viola o princípio de substituição de Liskov .Como regra geral, ser tão rígido e dogmático na engenharia de software é uma má ideia, mesmo que algo seja geralmente uma prática recomendada.
Então, eu pegava o que foi dito com um grão de sal.
fonte
People put way too much faith into interfaces, and too little faith into classes.
Engraçado. Costumo pensar que o oposto é verdadeiro.Quero adotar uma abordagem lingual e semântica para isso. Uma interface não existe para isso
DRY
. Embora possa ser usado como um mecanismo de centralização, além de ter uma classe abstrata que a implementa, não é para DRY.Uma interface é um contrato.
IAnimal
interface é um contrato do tipo tudo ou nada entre jacaré, gato, cachorro e a noção de animal. O que diz essencialmente é "se você quer ser um animal, deve ter todos esses atributos e características ou não é mais um animal".A herança do outro lado é um mecanismo de reutilização. Nesse caso, acho que ter um bom raciocínio semântico pode ajudá-lo a decidir qual método deve ir aonde. Por exemplo, enquanto todos os animais dormem e dormem da mesma forma, não usarei uma classe base para isso. Primeiro, coloquei o
Sleep()
método naIAnimal
interface como uma ênfase (semântica) para o que é preciso para ser um animal; depois, uso uma classe abstrata de base para gato, cachorro e jacaré como uma técnica de programação para não me repetir.fonte
Não tenho certeza se essa é a melhor regra geral por aí. Você pode colocar um
abstract
método na classe base e fazer com que cada classe o implemente. Como todo mundomammal
tem que comer, faria sentido fazer isso. Então cada ummammal
pode usar seu únicoeat
método. Normalmente, uso apenas interfaces para comunicação entre objetos ou como marcador para marcar uma classe como algo. Eu acho que o uso excessivo de interfaces cria códigos desleixados.Também concordo com @ Panancrisis que uma regra geral deve ser apenas uma orientação geral. Não é algo que deveria estar escrito em pedra. Sem dúvida, haverá momentos em que simplesmente não funcionará.
fonte
Interfaces são tipos. Eles não fornecem implementação. Eles simplesmente definem o contrato para lidar com esse tipo. Este contrato deve ser cumprido por qualquer classe que implemente a interface.
O uso de interfaces e classes abstract / base deve ser determinado pelos requisitos de design.
Uma única classe não requer uma interface.
Se duas classes são intercambiáveis dentro de uma função, elas devem implementar uma interface comum. Qualquer funcionalidade implementada de forma idêntica pode ser refatorada para uma classe abstrata que implementa a interface. A interface pode ser considerada redundante nesse ponto se não houver funcionalidade distintiva. Mas então qual é a diferença entre as duas classes?
O uso de classes abstratas como tipos geralmente é confuso, à medida que mais funcionalidades são adicionadas e a hierarquia se expande.
Favorecer a composição sobre a herança - tudo isso desaparece.
fonte