¿Se considera una interfaz 'vacía' si hereda de otras interfaces?

12

Las interfaces vacías generalmente se consideran una mala práctica, por lo que puedo decir, especialmente donde el lenguaje admite cosas como los atributos.

Sin embargo, ¿se considera una interfaz 'vacía' si hereda de otras interfaces?

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

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

Todo lo que implemente I3deberá implementarse I1y I2, y los objetos de diferentes clases que hereden I3se pueden usar indistintamente (ver más abajo), entonces, ¿es correcto llamar a I3 vacío ? Si es así, ¿cuál sería una mejor manera de arquitectura esto?

// 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
fuente
1
No poder especificar varias interfaces como tipo de parámetro / campo, etc. es una falla de diseño en C # / .NET.
CodesInChaos
Creo que la mejor forma de arquitectura de esta parte debería entrar en una pregunta separada. En este momento, la respuesta aceptada no tiene nada que ver con el título de la pregunta.
Kapol
@CodesInChaos No, no es así, ya hay dos formas de hacerlo. Una es en esta pregunta, la otra forma sería especificar un parámetro de tipo genérico para el argumento del método y usar la restricción where para especificar ambas interfaces.
Andy
¿Tiene sentido que una clase que implementa I1 e I2, pero no I3, no pueda ser utilizada por foo? (Si la respuesta es "sí", ¡adelante!)
user253751
@Andy Aunque no estoy totalmente de acuerdo con la afirmación de CodesInChaos de que es una falla como tal, no creo que los parámetros de tipo genérico en el método sean una solución, empuja la responsabilidad de conocer un tipo utilizado en la implementación del método está llamando al método, que se siente mal. Quizás alguna sintaxis como var : I1, I2 something = new A();para las variables locales sería buena (si ignoramos los buenos puntos planteados en la respuesta de Konrad)
Kai

Respuestas:

27

Cualquier cosa que implemente I3 necesitará implementar I1 e I2, y los objetos de diferentes clases que heredan I3 se pueden usar indistintamente (ver más abajo), ¿es correcto llamar a I3 vacío? Si es así, ¿cuál sería una mejor manera de arquitectura esto?

"Bundling" I1y I2into I3ofrece un buen acceso directo (?), O algún tipo de azúcar de sintaxis para donde quiera que se implementen ambos.

El problema con este enfoque es que no puede evitar que otros desarrolladores implementen de manera explícita I1 y I2 en paralelo, pero no funciona en ambos sentidos, aunque : I3es equivalente a : I1, I2, : I1, I2no es equivalente a : I3. Incluso si son funcionalmente idénticos, una clase que admite ambos I1y I2no se detectará como compatible I3.

Esto significa que está ofreciendo dos formas de lograr lo mismo: "manual" y "dulce" (con su sintaxis de azúcar). Y puede tener un impacto negativo en la consistencia del código. Ofrecer 2 formas diferentes de lograr lo mismo aquí, allá y en otro lugar da como resultado 8 combinaciones posibles ya :)

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

OK, no puedes ¿Pero tal vez es realmente una buena señal?

Si I1e I2implica diferentes responsabilidades (de lo contrario, no habría necesidad de separarlas en primer lugar), entonces ¿tal vez foo()es incorrecto intentar somethingrealizar dos responsabilidades diferentes de una sola vez?

En otras palabras, podría ser que en foo()sí mismo viola el principio de Responsabilidad Única, y el hecho de que requiera una verificación de tipo de lanzamiento y tiempo de ejecución para llevarlo a cabo es una señal de alerta que nos alarma.

EDITAR:

Si insistió en asegurarse de que footoma algo que se implemente I1y I2, podría pasarlos como parámetros separados:

void foo(I1 i1, I2 i2) 

Y podrían ser el mismo objeto:

foo(something, something);

¿Qué fooimporta si son la misma instancia o no?

Pero si le importa (por ejemplo, porque no son apátridas), o simplemente cree que esto se ve feo, uno podría usar genéricos en su lugar:

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

Ahora las restricciones genéricas se encargan del efecto deseado sin contaminar el mundo exterior con nada. Este es C # idiomático, basado en comprobaciones en tiempo de compilación, y se siente más correcto en general.

Veo I3 : I2, I1algo como un esfuerzo por emular tipos de unión en un lenguaje que no lo admite de forma inmediata (C # o Java no, mientras que los tipos de unión existen en lenguajes funcionales).

Tratar de emular una construcción que no es realmente compatible con el lenguaje a menudo es torpe. Del mismo modo, en C # puede usar métodos de extensión e interfaces si desea emular mixins . ¿Posible? Si. ¿Buena idea? No estoy seguro.

Konrad Morawski
fuente
4

Si hubiera una importancia particular para que una clase implementara ambas interfaces, entonces no veo nada de malo en crear una tercera interfaz que herede de ambas. Sin embargo, tendría cuidado de no caer en la trampa de las cosas de ingeniería excesiva. Una clase podría heredar de uno u otro, o de ambos. A menos que mi programa tenga muchas clases en estos tres casos, es un poco demasiado crear una interfaz para ambos, y ciertamente no si esas dos interfaces no tienen relación entre sí (no hay interfaces IComparableAndSerializable, por favor).

Le recuerdo que también puede crear una clase abstracta que herede de ambas interfaces, y puede usar esa clase abstracta en lugar de I3. Creo que sería un error decir que no deberías hacerlo, pero al menos deberías tener una buena razón.

Neil
fuente
¿Eh? Ciertamente puede implementar dos interfaces en una sola clase. No heredas las interfaces, las implementas.
Andy
@Andy ¿Dónde dije que una sola clase no puede implementar dos interfaces? Por lo que se refiere al uso de la palabra "heredar" en lugar de "implementar", semántica. En C ++, la herencia funcionaría perfectamente bien ya que lo más parecido a las interfaces son las clases abstractas.
Neil
La herencia y la implementación de la interfaz no son los mismos conceptos. Las clases que heredan múltiples subclases pueden conducir a algunos problemas extraños, que no es el caso con la implementación de múltiples interfaces. Y la falta de interfaces de C ++ no significa que la diferencia desaparezca, significa que C ++ está limitado en lo que puede hacer. La herencia es una relación "es-a", mientras que las interfaces especifican "puede-hacer".
Andy
@Andy Problemas extraños causados ​​por colisiones entre métodos implementados en dichas clases, sí. Sin embargo, eso ya no es una interfaz que no está en el nombre ni en la práctica. Las clases abstractas que declaran pero no implementan métodos son, en todo sentido, la palabra, excepto el nombre, una interfaz. La terminología cambia entre los idiomas, pero eso no significa que una referencia y un puntero no sean el mismo concepto. Es un punto pedante, pero si insiste en esto, tendremos que aceptar no estar de acuerdo.
Neil
"La terminología cambia entre idiomas" No, no lo hace. La terminología OO es consistente en todos los idiomas; Nunca he escuchado una clase abstracta que signifique algo más en cada lenguaje OO que he usado. Sí, puede tener la semántica de una interfaz en C ++, pero el punto es que no hay nada que impida que alguien vaya y agregue comportamiento a una clase abstracta en ese lenguaje y rompa esa semántica.
Andy
2

Las interfaces vacías generalmente se consideran una mala práctica.

Estoy en desacuerdo. Lea sobre las interfaces de marcadores .

Mientras que una interfaz típica especifica la funcionalidad (en forma de declaraciones de método) que una clase de implementación debe soportar, una interfaz de marcador no necesita hacerlo. La mera presencia de dicha interfaz indica un comportamiento específico por parte de la clase implementadora.

radarbob
fuente
Me imagino que el OP los conoce, pero en realidad son un poco controvertidos. Si bien algunos autores los recomiendan (por ejemplo, Josh Bloch en "Java efectivo"), algunos afirman que son un antipatrón y un legado de tiempos en que Java todavía no tenía anotaciones. (Las anotaciones en Java son aproximadamente el equivalente de los atributos en .NET). Mi impresión es que son mucho más populares en el mundo de Java que .NET.
Konrad Morawski
1
Los uso de vez en cuando, pero se siente un poco difícil: lo malo, fuera de mi cabeza, es que no puedes evitar el hecho de que son heredados, por lo que una vez que marcas una clase con una, todos sus niños obtienen marcado también si lo quieres o no. Y parecen alentar la mala práctica de usar en instanceoflugar del polimorfismo
Konrad Morawski