Interfaces em uma classe abstrata

30

Meu colega de trabalho e eu temos opiniões diferentes sobre o relacionamento entre classes base e interfaces. Eu acredito que uma classe não deve implementar uma interface, a menos que essa classe possa ser usada quando uma implementação da interface for necessária. Em outras palavras, eu gosto de ver código assim:

interface IFooWorker { void Work(); }

abstract class BaseWorker {
    ... base class behaviors ...
    public abstract void Work() { }
    protected string CleanData(string data) { ... }
}

class DbWorker : BaseWorker, IFooWorker {
    public void Work() {
        Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
    }
}

O DbWorker é o que obtém a interface do IFooWorker, porque é uma implementação instantânea da interface. Cumpre completamente o contrato. Meu colega de trabalho prefere o quase idêntico:

interface IFooWorker { void Work(); }

abstract class BaseWorker : IFooWorker {
    ... base class behaviors ...
    public abstract void Work() { }
    protected string CleanData(string data) { ... }
}

class DbWorker : BaseWorker {
    public void Work() {
        Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
    }
}

Onde a classe base obtém a interface e, em virtude disso, todos os herdeiros da classe base também são dessa interface. Isso me incomoda, mas não consigo apresentar razões concretas pelas quais, fora da "classe base, não se pode sustentar por si só como uma implementação da interface".

Quais são os prós e os contras do método dele versus o meu e por que um deve ser usado em detrimento do outro?

Bryan Boettcher
fonte
Sua sugestão se assemelha muito à herança de diamantes , o que pode causar muita confusão mais adiante.
Spoike 6/09/12
@ Spike: Como ver isso?
Niing 02/04
@Niing o link que forneci no comentário tem um diagrama de classes simples e uma explicação simples de uma linha para o que é isso. é chamado de "problema de diamante" porque a estrutura de herança é basicamente desenhada como uma forma de diamante (isto é, ♦ ️).
Spoike 15/04
@ Spike: por que essa pergunta causará herança de diamante?
Niing 15/04
1
@ Ni: A herança de BaseWorker e IFooWorker com o mesmo método chamado Worké a questão da herança de diamantes (nesse caso, ambos cumprem um contrato sobre o Workmétodo). Em Java, você precisa implementar os métodos da interface, para Workevitar a questão de qual método o programa deve usar. Idiomas como C ++, no entanto, não desambiguam isso para você.
Spoike 23/04

Respostas:

24

Eu acredito que uma classe não deve implementar uma interface, a menos que essa classe possa ser usada quando uma implementação da interface for necessária.

BaseWorkercumpre esse requisito. Só porque você não pode instanciar diretamente um BaseWorkerobjeto não significa que você não pode ter um BaseWorker ponteiro que cumpra o contrato. De fato, esse é basicamente o objetivo das classes abstratas.

Além disso, é difícil distinguir do exemplo simplificado que você postou, mas parte do problema pode ser que a interface e a classe abstrata são redundantes. A menos que você tenha outras classes implementadas IFooWorkerque não derivam BaseWorker, você não precisa da interface. As interfaces são apenas classes abstratas com algumas limitações adicionais que facilitam a herança múltipla.

Novamente, é difícil dizer a partir de um exemplo simplificado, o uso de um método protegido, referindo-se explicitamente à base de uma classe derivada, e a falta de um local inequívoco para declarar a implementação da interface são sinais de aviso de que você está usando herança de maneira inadequada em vez de composição. Sem essa herança, toda a sua pergunta se torna discutível.

Karl Bielefeldt
fonte
1
+1 por apontar que, só porque BaseWorkernão é instanciado diretamente, não significa que um dado BaseWorker myWorkernão seja implementado IFooWorker.
Avner Shahar-Kashtan
2
Eu discordo que as interfaces são apenas classes abstratas. As classes abstratas geralmente definem alguns detalhes de implementação (caso contrário, uma interface teria sido usada). Se esses detalhes não são do seu agrado, você deve agora mudar todos os usos da classe base para outra coisa. As interfaces são rasas; eles definem o relacionamento "plug-and-socket" entre dependências e dependentes. Como tal, o acoplamento rígido a qualquer detalhe da implementação não é mais uma preocupação. Se uma classe que herda a interface não é do seu agrado, retire-a e coloque outra coisa completamente; seu código pode se importar menos.
Keith
5
Ambos têm vantagens, mas geralmente a interface sempre deve ser usada para a dependência, independentemente de existir ou não uma classe abstrata que define implementações básicas. A combinação de interface e classe abstrata fornece o melhor dos dois mundos; a interface mantém o relacionamento plug-and-socket superficial raso, enquanto a classe abstrata fornece o código comum útil. Você pode remover e substituir qualquer nível disso abaixo da interface à vontade.
Keith
Por "ponteiro" , você quer dizer uma instância de um objeto (a quem um ponteiro faria referência)? As interfaces não são classes, são tipos e podem atuar como um substituto incompleto para herança múltipla, não como uma substituição - ou seja, elas "incluem comportamento de várias fontes em uma classe".
samis 26/06
46

Eu teria que concordar com seu colega de trabalho.

Nos dois exemplos apresentados, o BaseWorker define o método abstrato Work (), o que significa que todas as subclasses são capazes de cumprir o contrato do IFooWorker. Nesse caso, acho que o BaseWorker deve implementar a interface, e essa implementação seria herdada por suas subclasses. Isso evitará que você precise indicar explicitamente que cada subclasse é realmente um IFooWorker (o princípio DRY).

Se você não estivesse definindo Work () como um método do BaseWorker, ou se o IFooWorker tivesse outros métodos que as subclasses do BaseWorker não desejariam ou precisariam, então (obviamente) você teria que indicar quais subclasses realmente implementam o IFooWorker.

Matthew Flynn
fonte
11
O +1 teria postado a mesma coisa. Desculpe insta, mas acho que seu colega de trabalho também está certo neste caso, se algo garante cumprir um contrato, ele deve herdar esse contrato, se a garantia é cumprida por uma interface, uma classe abstrata ou uma classe concreta. Simplificando: o garante deve herdar o contrato que garante.
Jimmy Hoffa
2
Eu geralmente concordo, mas gostaria de salientar que palavras como "base", "padrão", "padrão", "genérico" etc. são odores de código em um nome de classe. Se uma classe base abstrata tem quase o mesmo nome que uma interface, mas com uma dessas palavras de doninha incluídas, geralmente é um sinal de abstração incorreta; interfaces definem o que algo faz , herança de tipo define o que é . Se você tiver um IFooe um BaseFoo, isso implica que IFoonão é uma interface de baixa granularidade adequada ou que BaseFooestá usando herança onde a composição pode ser uma escolha melhor.
Aaronaught 5/09/12
@Aaronaught Bom ponto, embora possa ser que realmente exista algo que todas ou a maioria das implementações do IFoo precisam herdar (como um identificador para um serviço ou a implementação do DAO).
Matthew Flynn
2
Então a classe base não deve ser chamada MyDaoFooou SqlFooou o que HibernateFooquer que seja, para indicar que é apenas uma possível árvore de classes implementando IFoo? É ainda mais de um cheiro para mim se uma classe base é acoplado a uma biblioteca ou plataforma específica e não há nenhuma menção a isso em nome ...
Aaronaught
@Aaronaught - Sim. Eu concordo completamente.
Matthew Flynn
11

Eu geralmente concordo com seu colega de trabalho.

Vamos usar seu modelo: a interface é implementada apenas pelas classes filho, mesmo que a classe base também imponha a exposição dos métodos IFooWorker. Primeiro, é redundante; se a classe filho implementa a interface ou não, eles são obrigados a substituir os métodos expostos do BaseWorker e, da mesma forma, qualquer implementação do IFooWorker deve expor a funcionalidade, independentemente de obter ou não ajuda do BaseWorker.

Isso também torna sua hierarquia ambígua; "Todos os IFooWorkers são BaseWorkers" não é necessariamente uma afirmação verdadeira, nem "Todos os BaseWorkers são IFooWorkers". Portanto, se você deseja definir uma variável de instância, parâmetro ou tipo de retorno que possa ser qualquer implementação do IFooWorker ou BaseWorker (aproveitando a funcionalidade exposta comum, que é um dos motivos para herdar em primeiro lugar), nenhum dos isso é garantido para ser abrangente; alguns BaseWorkers não serão atribuídos a uma variável do tipo IFooWorker e vice-versa.

O modelo do seu colega de trabalho é muito mais fácil de usar e substituir. "Todos os BaseWorkers são IFooWorkers" agora é uma afirmação verdadeira; você pode atribuir qualquer instância do BaseWorker a qualquer dependência do IFooWorker, sem problemas. A declaração oposta "Todos os IFooWorkers são BaseWorkers" não é verdadeira; que permite substituir o BaseWorker pelo BetterBaseWorker e seu código de consumo (que depende do IFooWorker) não precisará dizer a diferença.

KeithS
fonte
Gosto do som desta resposta, mas não acompanho bem a última parte. Você está sugerindo que o BetterBaseWorker seria uma classe abstrata independente que implementa o IFooWorker, para que meu hipotético plugin pudesse simplesmente dizer "oh, garoto! O Better Base Worker está finalmente fora!" e altere qualquer referência do BaseWorker para BetterBaseWorker e chame-a de um dia (talvez brinque com os novos valores retornados legais que me permitem remover grandes pedaços do meu código redundante etc.)?
Anthony
Mais ou menos, sim. A grande vantagem é que você reduzirá o número de referências ao BaseWorker que precisarão ser alteradas para serem BetterBaseWorkers; a maior parte do seu código não fará referência direta à classe concreta, em vez disso, usando o IFooWorker; portanto, quando você altera o que é atribuído a essas dependências do IFooWorker (propriedades de classes, parâmetros de construtores ou métodos), o código usando o IFooWorker não deve ver nenhuma diferença. em uso.
Keiths
6

Eu preciso adicionar um aviso para essas respostas. Acoplar a classe base à interface cria uma força na estrutura dessa classe. No seu exemplo básico, é óbvio que os dois devem ser acoplados, mas isso pode não ser verdade em todos os casos.

Pegue as classes de estrutura de coleção do Java:

abstract class AbstractList
class LinkedList extends AbstractList implements Queue

O fato de o contrato de fila ser implementado pelo LinkedList não levou a preocupação ao AbstractList .

Qual a distinção entre os dois casos? O objetivo do BaseWorker sempre foi (conforme comunicado por seu nome e interface) implementar as operações no IWorker . O objetivo do AbstractList e o do Queue são divergentes, mas um descendente do primeiro ainda pode implementar o último contrato.

Mihai Danila
fonte
Isso acontece na metade do tempo. Ele sempre prefere implementar a interface na classe base, e eu sempre prefiro implementá-la no concreto final. O cenário que você apresentou acontece com frequência e faz parte do motivo pelo qual as interfaces na base me incomodam.
11559 Bryan Boettcher #
1
Certo, insta e considero o uso de interfaces dessa maneira uma faceta do uso excessivo de herança que vemos em muitos ambientes de desenvolvimento. Como o GoF disse em seu livro de padrões de design, prefere composição a herança , e manter a interface fora da classe base é uma das maneiras de promover esse mesmo princípio.
Mihai Danila
1
Recebo o aviso, mas você notará que a classe abstrata do OP incluía métodos abstratos correspondentes à interface: um BaseWorker é implicitamente um IFooWorker. Tornar explícito torna esse fato mais utilizável. Entretanto, existem inúmeros métodos incluídos na Fila que não estão incluídos no AbstractList e, como tal, um AbstractList não é uma Fila, mesmo que seus filhos possam ser. AbstractList e Queue são ortogonais.
Matthew Flynn
Obrigado por esse insight; Concordo que o caso do OP se enquadra nessa categoria, mas senti que havia uma discussão maior em busca de uma regra mais profunda e queria compartilhar minhas observações sobre uma tendência sobre a qual li (e até me por um breve período) de uso excessivo de herança, talvez porque essa seja uma das ferramentas da caixa de ferramentas da OOP.
Mihai Danila
Dang, já faz seis anos. :)
Mihai Danila
2

Gostaria de fazer a pergunta, o que acontece quando você muda IFooWorker, como adicionar um novo método?

Se BaseWorkerimplementar a interface, por padrão, ele terá que declarar o novo método, mesmo que seja abstrato. Por outro lado, se não implementar a interface, você receberá apenas erros de compilação nas classes derivadas.

Por esse motivo, eu faria a classe base implementar a interface , pois talvez eu consiga implementar toda a funcionalidade do novo método na classe base sem tocar nas classes derivadas.

Scott Whitlock
fonte
1

Primeiro pense no que é uma classe base abstrata e no que é uma interface. Considere quando você usaria um ou outro e quando não usaria.

É comum as pessoas pensarem que ambos são conceitos muito semelhantes; na verdade, é uma pergunta de entrevista comum (a diferença entre os dois é ??)

Portanto, uma classe base abstrata fornece algo que as interfaces não podem, que são implementações padrão de métodos. (Há outras coisas, em C # você pode ter métodos estáticos em uma classe abstrata, não em uma interface, por exemplo).

Como exemplo, um uso comum disso é com o IDisposable. A classe abstrata implementa o método Dispose do IDisposable, o que significa que qualquer versão derivada da classe base será automaticamente descartável. Você pode jogar com várias opções. Torne Dispose abstrato, forçando classes derivadas a implementá-lo. Forneça uma implementação padrão virtual, para que não o torne, nem virtual nem abstrato, e faça com que ele chame métodos virtuais chamados de itens como BeforeDispose, AfterDispose, OnDispose ou Disposing, por exemplo.

Portanto, sempre que todas as classes derivadas precisarem suportar a interface, ela continuará na classe base. Se apenas um ou alguns deles precisarem dessa interface, ele entraria na classe derivada.

Na verdade, isso é uma simplificação grosseira. Outra alternativa é ter classes derivadas nem mesmo implementar as interfaces, mas fornecê-las através de um padrão de adaptador. Um exemplo disso que vi recentemente foi em IObjectContextAdapter.

Ian
fonte
1

Ainda estou a anos de compreender completamente a distinção entre uma classe abstrata e uma interface. Toda vez que acho que entendo os conceitos básicos, vou procurar no stackexchange e estou a dois passos para trás. Mas algumas reflexões sobre o tópico e os POs questionam:

Primeiro:

Existem duas explicações comuns para uma interface:

  1. Uma interface é uma lista de métodos e propriedades que qualquer classe pode implementar e, ao implementar uma interface, uma classe garante que esses métodos (e suas assinaturas) e essas propriedades (e seus tipos) estarão disponíveis ao "interagir" com essa classe ou um objeto dessa classe. Uma interface é um contrato.

  2. Interfaces são classes abstratas que não podem / não podem fazer nada. Eles são úteis porque você pode implementar mais de uma, diferentemente das classes pai médias. Tipo, eu poderia ser um objeto da classe BananaBread e herdar do BaseBread, mas isso não significa que também não posso implementar as interfaces IWithNuts e ITastesYummy. Eu poderia até implementar a interface IDoesTheDishes, porque não sou apenas pão, sabe?

Existem duas explicações comuns para uma classe abstrata:

  1. Uma classe abstrata é, sabe, a coisa que não é o que ela pode fazer. É como, a essência, não é realmente uma coisa real. Espere, isso vai ajudar. Um barco é uma classe abstrata, mas um sexy Playboy Yacht seria uma subclasse do BaseBoat.

  2. Li um livro sobre aulas abstratas, e talvez você deva ler esse livro, porque provavelmente não o entende e o fará errado se ainda não o leu.

A propósito, o livro Citers sempre parece impressionante, mesmo que eu ainda vá embora confusa.

Segundo:

No SO, alguém perguntou uma versão mais simples dessa pergunta, o clássico "por que usar interfaces? Qual é a diferença? O que estou perdendo?" E uma resposta usou um piloto da força aérea como um exemplo simples. Não deu certo, mas provocou ótimos comentários, um dos quais mencionou a interface IFlyable com métodos como takeOff, pilotEject etc. Isso realmente me chamou a atenção como um exemplo real do porquê as interfaces não são apenas úteis, mas crucial. Uma interface torna um objeto / classe intuitivo ou, pelo menos, dá a sensação de que é. Uma interface não é para o benefício do objeto ou dos dados, mas para algo que precisa interagir com esse objeto. O clássico Frutas-> Maçã-> Fuji ou Forma-> Triângulo-> Exemplos equivalentes de herança são um ótimo modelo para entender taxonomicamente um determinado objeto com base em seus descendentes. Informa o consumidor e os processadores sobre suas qualidades gerais, comportamentos, se o objeto é um grupo de coisas, mangueira seu sistema se você o colocar no lugar errado ou descreve dados sensoriais, um conector para um armazenamento de dados específico ou dados financeiros necessários para fazer a folha de pagamento.

Um objeto específico do avião pode ou não ter um método para um pouso de emergência, mas eu vou ficar chateado se eu assumir sua terra de emergência, como qualquer outro objeto passível de voar, apenas para acordar coberto no DumbPlane e descobrir que os desenvolvedores foram com o quickLand porque eles pensaram que era tudo a mesma coisa. Assim como eu ficaria frustrado se cada fabricante de parafusos tivesse sua própria interpretação de righty tighty ou se minha TV não tivesse o botão de aumento de volume acima do botão de diminuição de volume.

Classes abstratas são o modelo que estabelece o que qualquer objeto descendente deve ter para se qualificar como essa classe. Se você não possui todas as qualidades de um pato, não importa se a interface do IQuack foi implementada, você é apenas um pinguim estranho. Interfaces são as coisas que fazem sentido, mesmo quando você não pode ter certeza de mais nada. Jeff Goldblum e Starbuck foram capazes de voar naves alienígenas porque a interface era similarmente confiável.

Terceiro:

Eu concordo com o seu colega de trabalho, porque às vezes você precisa aplicar certos métodos desde o início. Se você estiver criando um ORM do Active Record, ele precisará de um método de salvamento. Isso não depende da subclasse que pode ser instanciada. E se a interface ICRUD for portátil o suficiente para não ser acoplada exclusivamente à classe abstrata abstrata, ela poderá ser implementada por outras classes para torná-las confiáveis ​​e intuitivas para qualquer pessoa que já esteja familiarizada com qualquer uma das classes descendentes dessa classe abstrata.

Por outro lado, havia um ótimo exemplo anterior de quando não pular para vincular uma interface à classe abstrata, porque nem todos os tipos de lista implementam (ou deveriam) implementar uma interface de fila. Você disse que esse cenário acontece na metade do tempo, o que significa que você e seu colega de trabalho estão errados na metade do tempo e, portanto, a melhor coisa a fazer é discutir, debater, considerar e, se eles estiverem certos, reconhecer e aceitar o acoplamento . Mas não se torne um desenvolvedor que segue uma filosofia, mesmo quando não é a melhor para o trabalho em questão.

Anthony
fonte
0

Há outro aspecto no porquê da DbWorkerimplementação IFooWorker, como você sugere.

No caso do estilo do seu colega de trabalho, se por algum motivo uma refatoração posterior ocorrer e DbWorkerfor considerada não estender a BaseWorkerclasse abstrata , DbWorkera IFooWorkerinterface será perdida. Isso pode, mas não necessariamente, ter um impacto nos clientes que o consomem, se esperarem a IFooWorkerinterface.

Frederik Krautwald
fonte
-1

Para começar, eu gosto de definir interfaces e classes abstratas de maneira diferente:

Interface: a contract that each class must fulfill if they are to implement that interface
Abstract class: a general class (i.e vehicle is an abstract class, while porche911 is not)

No seu caso, todos os trabalhadores implementam work()porque é para isso que o contrato exige. Portanto, sua classe base Baseworkerdeve implementar esse método explicitamente ou como uma função virtual. Eu sugiro que você coloque esse método em sua classe base por causa do simples fato de que todos os trabalhadores precisam work(). Portanto, é lógico que sua classe básica (mesmo que você não possa criar um ponteiro desse tipo) a inclua como uma função.

Agora, DbWorkersatisfaz a IFooWorkerinterface, mas se existe uma maneira específica que essa classe faz work(), ela realmente precisa sobrecarregar a definição herdada BaseWorker. O motivo pelo qual não deve implementar a interface IFooWorkerdiretamente é que isso não é algo especial DbWorker. Se você fizesse isso toda vez que implementasse classes semelhantes DbWorker, estaria violando o DRY .

Se duas classes implementarem a mesma função de maneiras diferentes, você poderá começar a procurar a maior superclasse comum. Na maioria das vezes você encontrará um. Caso contrário, continue procurando ou desista e aceite que eles não têm coisas suficientes em comum para formar uma classe base.

Arnab Datta
fonte
-3

Uau, todas essas respostas e nenhuma apontando que a interface no exemplo não serve para nada. Você não usa herança e uma interface para o mesmo método, é inútil. Você usa um ou outro. Há muitas perguntas neste fórum com respostas que explicam os benefícios de cada um e dos senários que exigem um ou outro.

Martin Maat
fonte
3
Isso é uma falsidade patente. A interface é um contrato pelo qual se espera que o sistema se comporte, independentemente da implementação. A classe base herdada é uma das muitas implementações possíveis para essa interface. Um sistema de escala grande o suficiente terá várias classes base que satisfazem as várias interfaces (geralmente fechadas com genéricos), exatamente o que essa configuração fez e por que foi útil dividi-las.
Bryan Boettcher
É um exemplo muito ruim do ponto de vista de OOP. Como você diz "a classe base é apenas uma implementação" (deveria ser ou não haveria sentido para a interface), você está dizendo que haverá classes não-trabalhadores implementando a interface IFooWorker. Então você terá uma classe base que é um trabalhador especializado (devemos assumir que também pode haver trabalhadores do bar) e você teria outros trabalhadores que nem sequer são trabalhadores básicos! Isso é atrasado. De mais de uma maneira.
Martin Maat
1
Talvez você esteja preso à palavra "Trabalhador" na interface. E sobre "Fonte de dados"? Se tivermos um IDatasource <T>, podemos trivialmente ter um SqlDatasource <T>, RestDatasource <T>, MongoDatasource <T>. A empresa decide quais fechamentos da interface de genéricos vão para as implementações de fechamento das fontes de dados abstratas.
Bryan Boettcher
Pode fazer sentido usar a herança apenas por uma questão de DRY e colocar alguma implementação comum em uma classe base. Mas você não alinharia nenhum método substituído a nenhum método de interface, a classe base conteria lógica de suporte geral. Se eles se alinharem, isso mostraria que a herança resolve o problema muito bem e a interface não servia para nada. Você usa uma interface nos casos em que a herança não resolve seu problema porque as características comuns que você deseja modelar não se ajustam a uma árvore de classes singke. E sim, a redação do exemplo torna tudo mais errado.
Martin Maat