Uma interface é considerada 'vazia' se herdar de outras interfaces?

12

As interfaces vazias são geralmente consideradas práticas inadequadas, até onde eu sei - especialmente quando coisas como atributos são suportadas pelo idioma.

No entanto, uma interface é considerada 'vazia' se herdar de outras interfaces?

interface I1 { ... }
interface I2 { ... } //unrelated to I1

interface I3
    : I1, I2
{
    // empty body
}

Qualquer coisa que implementa I3precisará implementar I1e I2, e objetos de diferentes classes que herdam I3podem então ser usados alternadamente (veja abaixo), então é certo para chamar I3 vazio ? Se sim, qual seria a melhor maneira de arquitetar isso?

// with I3 interface
class A : I3 { ... }
class B : I3 { ... }

class Test {
    void foo() {
        I3 something = new A();
        something = new B();
        something.SomeI1Function();
        something.SomeI2Function();
    }
}

// without I3 interface
class A : I1, I2 { ... }
class B : I1, I2 { ... }

class Test {
    void foo() {
        I1 something = new A();
        something = new B();
        something.SomeI1Function();
        something.SomeI2Function(); // we can't do this without casting first
    }
}
Kai
fonte
1
Ser incapaz de especificar várias interfaces como tipo de parâmetro / campo etc. é uma falha de design em C # / .NET.
CodesInChaos
Eu acho que a melhor maneira de arquitetar essa parte deve entrar em uma questão separada. No momento, a resposta aceita não tem nada a ver com o título da pergunta.
Kapol
@CodesInChaos Não, não é, já que existem duas maneiras de fazer isso. Uma é nesta questão, a outra maneira seria especificar um parâmetro de tipo genérico para o argumento do método e usar a restrição where para especificar as duas interfaces.
Andy
Faz sentido que uma classe que implemente I1 e I2, mas não I3, não deva ser capaz de ser usada foo? (Se a resposta for "sim", então vá em frente!)
user253751
@Andy Embora eu não concorde totalmente com a alegação de CodesInChaos de que é uma falha , não acho que parâmetros de tipo genérico no método sejam uma solução - ele empurra a responsabilidade de conhecer um tipo usado na implementação do método para qualquer está chamando o método, que parece errado. Talvez alguns sintaxe como var : I1, I2 something = new A();por variáveis locais seria bom (se ignorarmos os pontos bons levantadas na resposta de Konrad)
Kai

Respostas:

27

Qualquer coisa que implemente I3 precisará implementar I1 e I2, e objetos de diferentes classes que herdam I3 podem ser usados ​​de forma intercambiável (veja abaixo), então é correto chamar I3 vazio? Se sim, qual seria a melhor maneira de arquitetar isso?

"Agrupar" I1e I2no I3oferece um bom atalho (?) Ou algum tipo de açúcar de sintaxe para onde você quiser que ambos sejam implementados.

O problema dessa abordagem é que você não pode impedir que outros desenvolvedores implementem explicitamente I1 e I2 lado a lado, mas não funciona nos dois sentidos - embora : I3seja equivalente a : I1, I2, : I1, I2não é equivalente a : I3. Mesmo se eles forem funcionalmente idênticos, uma classe que ofereça suporte a ambos I1e I2não será detectada como suporte I3.

Isso significa que você está oferecendo duas maneiras de realizar a mesma coisa - "manual" e "doce" (com o açúcar da sintaxe). E isso pode ter um impacto ruim na consistência do código. Oferecendo 2 maneiras diferentes de realizar a mesma coisa aqui, ali e em outro lugar, já resulta em 8 combinações possíveis :)

void foo() {
    I1 something = new A();
    something = new B();
    something.SomeI1Function();
    something.SomeI2Function(); // we can't do this without casting first
}

OK, você não pode. Mas talvez seja realmente um bom sinal?

Se I1e I2implicar responsabilidades diferentes (caso contrário, não haveria necessidade de separá-las), então talvez foo()esteja errado tentar fazer somethingduas responsabilidades diferentes de uma só vez?

Em outras palavras, pode ser que foo()ela própria viola o princípio da Responsabilidade Única, e o fato de requerer verificações de tipo de transmissão e de tempo de execução para ser realizado é uma bandeira vermelha que nos preocupa.

EDITAR:

Se você insistiu em garantir que isso foorequer algo que implementa I1e I2, você pode passá-los como parâmetros separados:

void foo(I1 i1, I2 i2) 

E eles podem ser o mesmo objeto:

foo(something, something);

O que fooimporta se são da mesma instância ou não?

Mas se isso se importa (por exemplo, porque eles não são apátridas), ou você apenas acha isso feio, pode-se usar genéricos:

void Foo<T>(T something) where T: I1, I2

Agora, restrições genéricas cuidam do efeito desejado sem poluir o mundo exterior com nada. Este é um C # idiomático, baseado em verificações em tempo de compilação, e parece mais correto no geral.

Considero I3 : I2, I1um esforço para emular tipos de união em uma linguagem que não a suporta imediatamente (C # ou Java não, enquanto os tipos de união existem em linguagens funcionais).

Tentar imitar uma construção que não é realmente suportada pelo idioma geralmente é desajeitado. Da mesma forma, em C #, você pode usar métodos e interfaces de extensão se quiser emular mixins . Possível? Sim. Boa ideia? Não tenho certeza.

Konrad Morawski
fonte
4

Se havia uma importância particular em ter uma classe implementando ambas as interfaces, não vejo nada de errado em criar uma terceira interface que herda de ambas. No entanto, eu tomaria cuidado para não cair na armadilha das coisas de excesso de engenharia. Uma classe pode herdar de uma ou outra, ou de ambas. A menos que meu programa tenha muitas classes nesses três casos, é um pouco demais criar uma interface para ambos, e certamente não se essas duas interfaces não tiverem relação uma com a outra (sem interfaces IComparableAndSerializable, por favor).

Lembro que você também pode criar uma classe abstrata que herda de ambas as interfaces e pode usar essa classe abstrata no lugar do I3. Eu acho que seria errado dizer que você não deve fazê-lo, mas pelo menos deve ter um bom motivo.

Neil
fonte
Hã? Você certamente pode implementar duas interfaces em uma única classe. Você não herda interfaces, as implementa.
Andy
@ Andy Onde eu disse que uma única classe não pode implementar duas interfaces? Para o que diz respeito ao uso da palavra "herdar" no lugar de "implementar", semântica. Em C ++, herdar funcionaria perfeitamente, pois o mais próximo das interfaces são as classes abstratas.
Neil
Herança e implementação de interface não são os mesmos conceitos. Classes que herdam várias subclasses podem levar a alguns problemas estranhos, o que não é o caso da implementação de várias interfaces. E o C ++ sem interfaces não significa que a diferença desaparece, significa que o C ++ é limitado no que pode fazer. A herança é um relacionamento "is-a", enquanto as interfaces especificam "can-do".
22415 Andy
@ Andy Questões estranhas causadas por colisões entre métodos implementados nas classes mencionadas, sim. No entanto, isso não é mais uma interface nem no nome nem na prática. Classes abstratas que declaram mas não implementam métodos são, em todos os sentidos da palavra, exceto o nome, uma interface. A terminologia muda entre idiomas, mas isso não significa que uma referência e um ponteiro não sejam o mesmo conceito. É um ponto pedante, mas se você insistir nisso, teremos que concordar em discordar.
214 Neil
"A terminologia muda entre os idiomas" Não, não. A terminologia OO é consistente entre idiomas; Eu nunca ouvi uma classe abstrata significando qualquer outra coisa em cada idioma OO que eu usei. Sim, você pode ter a semântica de uma interface em C ++, mas a questão é que não há nada que impeça alguém de adicionar comportamento a uma classe abstrata nessa linguagem e quebrar essa semântica.
217 Andy
2

Interfaces vazias são geralmente consideradas más práticas

Discordo. Leia sobre interfaces de marcador .

Enquanto uma interface típica especifica a funcionalidade (na forma de declarações de método) que uma classe de implementação deve suportar, uma interface de marcador não precisa fazê-lo. A mera presença de tal interface indica um comportamento específico por parte da classe de implementação.

radarbob
fonte
Imagino que o OP esteja ciente deles, mas na verdade eles são um pouco controversos. Enquanto alguns autores as recomendam (por exemplo, Josh Bloch em "Java Efetivo"), alguns afirmam que são um antipadrão e um legado de épocas em que o Java ainda não tinha anotações. (As anotações em Java são aproximadamente o equivalente a atributos no .NET). Minha impressão é que eles são muito mais populares no mundo Java que o .NET.
Konrad Morawski
1
Eu as uso de vez em quando, mas parece meio hackish - o lado ruim da minha cabeça é que você não pode evitar o fato de que elas são herdadas, então, quando você marca uma aula com uma, todas as crianças ficam marcado também, quer você queira ou não. E eles parecem incentivar má prática de usar instanceofem vez de polimorfismo
Konrad Morawski