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?
fonte
Work
é a questão da herança de diamantes (nesse caso, ambos cumprem um contrato sobre oWork
método). Em Java, você precisa implementar os métodos da interface, paraWork
evitar a questão de qual método o programa deve usar. Idiomas como C ++, no entanto, não desambiguam isso para você.Respostas:
BaseWorker
cumpre esse requisito. Só porque você não pode instanciar diretamente umBaseWorker
objeto não significa que você não pode ter umBaseWorker
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
IFooWorker
que não derivamBaseWorker
, 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.
fonte
BaseWorker
não é instanciado diretamente, não significa que um dadoBaseWorker myWorker
não seja implementadoIFooWorker
.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.
fonte
IFoo
e umBaseFoo
, isso implica queIFoo
não é uma interface de baixa granularidade adequada ou queBaseFoo
está usando herança onde a composição pode ser uma escolha melhor.MyDaoFoo
ouSqlFoo
ou o queHibernateFoo
quer que seja, para indicar que é apenas uma possível árvore de classes implementandoIFoo
? É 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 ...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.
fonte
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:
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.
fonte
Gostaria de fazer a pergunta, o que acontece quando você muda
IFooWorker
, como adicionar um novo método?Se
BaseWorker
implementar 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.
fonte
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.
fonte
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:
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.
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:
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.
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.
fonte
Há outro aspecto no porquê da
DbWorker
implementaçãoIFooWorker
, como você sugere.No caso do estilo do seu colega de trabalho, se por algum motivo uma refatoração posterior ocorrer e
DbWorker
for considerada não estender aBaseWorker
classe abstrata ,DbWorker
aIFooWorker
interface será perdida. Isso pode, mas não necessariamente, ter um impacto nos clientes que o consomem, se esperarem aIFooWorker
interface.fonte
Para começar, eu gosto de definir interfaces e classes abstratas de maneira diferente:
No seu caso, todos os trabalhadores implementam
work()
porque é para isso que o contrato exige. Portanto, sua classe baseBaseworker
deve 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 precisamwork()
. 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,
DbWorker
satisfaz aIFooWorker
interface, mas se existe uma maneira específica que essa classe fazwork()
, ela realmente precisa sobrecarregar a definição herdadaBaseWorker
. O motivo pelo qual não deve implementar a interfaceIFooWorker
diretamente é que isso não é algo especialDbWorker
. Se você fizesse isso toda vez que implementasse classes semelhantesDbWorker
, 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.
fonte
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.
fonte