“Se um método for reutilizado sem alterações, coloque o método em uma classe base, caso contrário, crie uma interface”, uma boa regra geral?


10

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 cate dog, que estendem a classe mammale têm um único método pet(). Em seguida, adicionamos a classe alligator, que não estende nada e tem um único método slither().

Agora, queremos adicionar um eat()método para todos eles.

Se a implementação do eat()método for exatamente a mesma para cat, doge alligator, devemos criar uma classe base (digamos animal), que implementa esse método.

No entanto, se sua implementação alligatordiferir da menor maneira possível, devemos criar uma IEatinterface e criar mammale alligatorimplementar.

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?


27
alligatorA implementação de eatdifere, é claro, na medida em que aceita cate dogcomo parâmetros.
sbichenko

1
Onde você realmente deseja que uma classe base abstrata compartilhe implementações, mas deve usar interfaces para a extensibilidade correta, geralmente é possível usar características . Ou seja, se o seu idioma suportar isso.
amon

2
Quando você se pinta em um canto, é melhor estar no mais próximo da porta.
101313 JeffO

10
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 reutilização de código deve ser realizada (idealmente) através da composição. Seu colega de trabalho está errado.
riwalk

2
Um professor de CS ensinou que as superclasses deveriam ser is ae as interfaces são acts likeou is. Então, um is amamífero de cachorro e acts likeum 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 de isseria The cake is eatableou The book is writable.
precisa

Respostas:


13

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 PetEatingBehaviorfunção que define as funções alimentares de cães e gatos. Então você pode ter IEate 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 IEatcontém menos informações que a Mammalclasse 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


Você poderia fornecer um exemplo básico de PetEatingBehaviorimplementação?
sbichenko

1
@exizt Primeiro, sou ruim em escolher nomes. Então PetEatingBehaviorprovavelmente é 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 o eatmé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 seu eatmétodo. Você simplesmente precisa criar um PetEatingBehaviormembro e encaminhar as chamadas para dog.eat/cat.eat.
Simon Bergot

Eu não posso acreditar que esta é a única resposta de muitos para dirigir o OP longe de heranças :( Herança não é para reutilização de código!
Danny Tuppeny

1
@DannyTuppeny Por que não?
sbichenko

4
@exizt Herdar de uma classe é um grande negócio; você está assumindo uma dependência (provavelmente permanente) de algo, que em muitos idiomas você pode ter apenas um. A reutilização de código é uma coisa bastante trivial para usar essa classe base, geralmente apenas a. E se você acabar com métodos em que deseja compartilhá-los com outra classe, mas ela já tiver uma classe base por causa da reutilização de outros métodos (ou mesmo por fazer parte de uma hierarquia "real" usada polimorficamente (?)). (. Por exemplo, um herda de viaturas de Veículo) Use herança quando ClassA é um tipo de ClassB, não quando ele compartilha alguns detalhes de implementação interna :-)
Danny Tuppeny

11

Vale a pena seguir esta regra de ouro?

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


1
Relendo esta resposta três anos depois, considero este um ótimo ponto: 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 para misturar à toa com outra funcionalidade"
sbichenko

9

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 ae as interfaces são acts likeou is. Então, um is amamífero de cachorro e acts likeum comedor. Isso nos diria que o mamífero deve ser uma classe e o comedor deve ser uma interface. Um exemplo de isseria The cake is eatableor The book is writable(making both eatableand writableinterfaces).

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.


6

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 um foreachloop.

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:

A composição é geralmente mais apropriada que a herança. ... [B] como o código que implementa uma subclasse se espalha entre a base e a subclasse, pode ser mais difícil entender uma implementação. A subclasse não pode substituir funções que não são virtuais, portanto, a subclasse não pode alterar a implementação.

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 duplique FileStreamo código. Pessoas inteligentes reconhecem que isso é estúpido. Você deve apenas ter um FileStreamobjeto em sua classe (como uma variável local ou membro) e usar sua funcionalidade.

O FileStreamexemplo 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.


Portanto, se eu entendi direito, a injeção de dependência fornece os mesmos recursos de reutilização de código que a herança. Mas para que você usaria herança, então? Você forneceria um caso de uso? (Eu realmente apreciei a um com FileStream)
sbichenko

1
@exizt, não, ele não fornece os mesmos recursos de reutilização de código. Ele fornece melhores recursos de reutilização de código (em muitos casos), pois mantém as implementações bem contidas. As classes são interagidas explicitamente por meio de seus objetos e API pública, em vez de ganhar implicitamente recursos e alterar completamente metade da funcionalidade, sobrecarregando as funções existentes.
riwalk

4

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:

public interface IFoodEater
{
    void Eat(IFood food);
}

public class Animal : IFoodEater
{
    private IFoodProcessor _foodProcessor;
    public Animal(IFoodProcessor foodProcessor)
    {
        _foodProcessor = foodProcessor;
    }

    public void Eat(IFood food)
    {
        _foodProcessor.Process(food);
    }
}

Ou implemente um padrão de visitante em que um FoodProcessortenta 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.


1
O IFoodEater é meio redundante. O que mais você espera poder comer? Sim, animais como exemplos são uma coisa terrível.
Euphoric

1
E as plantas carnívoras? :) Sério, um grande problema com o não uso de interfaces nesse caso é que você vinculou intimamente o conceito de comer com Animals e é muito mais trabalhoso introduzir novas abstrações para as quais a classe base Animal não é apropriada.
Julia Hayward

1
Eu acredito que "comer" é a idéia errada aqui: seja mais abstrato. Que tal uma IConsumerinterface. Carnívoros podem consumir carne, herbívoros consomem plantas e plantas consomem luz solar e água.

2

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:

public abstract class Person
    {
        /// <summary>
        /// Inheritors must implement a hello
        /// </summary>
        /// <returns>Hello</returns>
        public abstract string SayHello();
        /// <summary>
        /// Inheritors can use base functionality, or override
        /// </summary>
        /// <returns></returns>
        public virtual string SayGoodBye()
        {
            return "Good Bye";
        }
        /// <summary>
        /// Base Functionality that is inherited 
        /// </summary>
        public void DoSomething()
        {
            //Do Something Here
        }
    }

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.


1

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.


Espere, por que reimplementar eat()em todos os animais? Nós podemos fazer mammalimplementá-lo, não podemos? No entanto, entendo sua opinião sobre os requisitos.
sbichenko

@exizt: Desculpe, li sua pergunta incorretamente.
Euphoric

1

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.


1

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:

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

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

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


5
People put way too much faith into interfaces, and too little faith into classes.Engraçado. Costumo pensar que o oposto é verdadeiro.
Simon Bergot

1
"Isso não viola o princípio da substituição de Liskov". Mas está extremamente perto de se tornar uma violação do LSP. Melhor prevenir do que remediar.
Euphoric

1

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.

IAnimalinterface é 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 na IAnimalinterface 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.


0

Não tenho certeza se essa é a melhor regra geral por aí. Você pode colocar um abstractmétodo na classe base e fazer com que cada classe o implemente. Como todo mundo mammaltem que comer, faria sentido fazer isso. Então cada um mammalpode usar seu único eatmé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á.


-1

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.

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.