Implementar una interfaz cuando no necesita una de las propiedades

31

Muy claro. Estoy implementando una interfaz, pero hay una propiedad que es innecesaria para esta clase y, de hecho, no debería usarse. Mi idea inicial era hacer algo como:

int IFoo.Bar
{
    get { raise new NotImplementedException(); }
}

Supongo que no hay nada de malo en esto, per se, pero no se siente "bien". ¿Alguien más se ha encontrado con una situación similar antes? Si es así, ¿cómo lo enfocaste?

Chris Pratt
fuente
1
Recuerdo vagamente que hay una clase de uso semi común en C # que implementa una interfaz pero declara explícitamente en la documentación que cierto método no está implementado. Trataré de ver si puedo encontrarlo.
Mage Xy
Definitivamente me interesaría ver eso, si puedes encontrarlo.
Chris Pratt
23
Puedo señalar varios casos de esto en la biblioteca de .NET, Y TODOS SON RECONOCIDOS COMO MALOS ERRORES TERRIBLES . Esta es una violación icónica y común del Principio de sustitución de Liskov: las razones para no violar el LSP se pueden encontrar en mi respuesta aquí
Jimmy Hoffa
3
¿Está obligado a implementar esta interfaz específica, o podría introducir una superinterfaz y usarla?
Christopher Schultz
66
"una propiedad que es innecesaria para esta clase": si una parte de una interfaz es necesaria depende de los clientes de la interfaz, no de los implementadores. Si una clase no puede implementar razonablemente un miembro de una interfaz, entonces la clase no es la adecuada para la interfaz. Esto puede significar que la interfaz está mal diseñada, probablemente tratando de hacer demasiado, pero eso no ayuda a la clase.
Sebastian Redl

Respuestas:

51

Este es un ejemplo clásico de cómo las personas deciden violar el Principio de Subtitulación de Liskov. Lo desaconsejo encarecidamente, pero recomendaría posiblemente una solución diferente:

  1. Quizás la clase que está escribiendo no proporciona la funcionalidad que prescribe la interfaz si no tiene uso de todos los miembros de la interfaz.
  2. Alternativamente, esa interfaz puede estar haciendo varias cosas y podría estar separada según el Principio de segregación de interfaz.

Si el primero es el caso para usted, simplemente no implemente la interfaz en esa clase. Piense en ello como un enchufe eléctrico donde el orificio de conexión a tierra es innecesario, por lo que en realidad no se conecta a tierra. ¡No conectas nada con tierra y no es gran cosa! Pero tan pronto como use algo que necesite un terreno, podría encontrarse con un fracaso espectacular. Es mejor no perforar un agujero falso en el suelo. Entonces, si su clase no hace realmente lo que pretende la interfaz, no implemente la interfaz.


Aquí hay algunos bits rápidos de wikipedia:

El Principio de sustitución de Liskov puede formularse simplemente como "No fortalezca las precondiciones y no debilite las postcondiciones".

Más formalmente, el principio de sustitución de Liskov (LSP) es una definición particular de una relación de subtipo, llamada subtipo de comportamiento (fuerte), que fue inicialmente presentada por Barbara Liskov en un discurso de apertura de la conferencia de 1987 titulado Abstracción y jerarquía de datos. Es una relación semántica más que simplemente sintáctica porque tiene la intención de garantizar la interoperabilidad semántica de los tipos en una jerarquía , [...]

Para la interoperabilidad semántica y la sustituibilidad entre diferentes implementaciones de los mismos contratos, necesita que todos se comprometan con los mismos comportamientos.


El principio de segregación de interfaz habla de la idea de que las interfaces deben separarse en conjuntos cohesivos, de modo que no requiera una interfaz que haga muchas cosas diferentes cuando solo desea una instalación. Piense nuevamente en la interfaz de una toma de corriente, también podría tener un termostato, pero dificultaría la instalación de una toma de corriente y podría hacer que sea más difícil de usar para fines que no sean de calefacción. Al igual que una toma de corriente con un termostato, las interfaces grandes son difíciles de implementar y difíciles de usar.

El principio de segregación de interfaz (ISP) establece que ningún cliente debería verse obligado a depender de métodos que no utiliza. [1] El ISP divide las interfaces que son muy grandes en pequeñas y más específicas para que los clientes solo tengan que conocer los métodos que les interesan.

Jimmy Hoffa
fuente
Absolutamente. Probablemente por eso no me pareció bien, en primer lugar. A veces solo necesitas un suave recordatorio de que estás haciendo algo estúpido. Gracias.
Chris Pratt
@ChrisPratt es un error muy común, por eso los formalismos pueden ser valiosos: clasificar los olores de código ayuda a identificarlos más rápidamente y a recordar las soluciones utilizadas anteriormente.
Jimmy Hoffa
4

Me parece bien, si esta es tu situación.

Sin embargo, me parece que su interfaz (o uso de la misma) se rompe si una clase derivada no implementa realmente todo. Considere dividir esa interfaz.

Descargo de responsabilidad: esto requiere una herencia múltiple para funcionar correctamente, y no tengo idea de si C # lo admite.

La ligereza corre con Mónica
fuente
66
C # no admite la herencia múltiple de clases, pero sí es compatible con las interfaces.
mgw854
2
Creo que tienes razón. La interfaz es un contrato después de todo, e incluso si sé que esta clase en particular no se usará de manera que rompa cualquier cosa que utilice la interfaz si esta propiedad está deshabilitada, eso no es obvio.
Chris Pratt
@ChrisPratt: Sí.
Lightness compite con Monica el
4

Me he encontrado con esta situación. De hecho, como se señaló en otra parte, el BCL tiene tales instancias ... Trataré de proporcionar mejores ejemplos y proporcionar algunas razones:

Cuando tiene una interfaz ya enviada que mantiene por razones de compatibilidad y ...

  • La interfaz contiene miembros que están obsoletos o están descartados. Por ejemplo BlockingCollection<T>.ICollection.SyncRoot(entre otros) mientras ICollection.SyncRootno es obsoleto per se, arrojará NotSupportedException.

  • La interfaz contiene miembros que están documentados como opcionales y que la implementación puede generar la excepción. Por ejemplo, en MSDN al respecto IEnumerator.Resetdice:

El método Reset se proporciona para la interoperabilidad COM. No necesariamente necesita ser implementado; en cambio, el implementador puede simplemente lanzar una NotSupportedException.

  • Por un error en el diseño de la interfaz, debería haber sido más de una interfaz en primer lugar. Es un patrón común en BCL implementar versiones de solo lectura de contenedores con NotSupportedException. Lo he hecho yo mismo, es lo que se espera ahora ... ICollection<T>.IsReadOnlyRegreso truepara que puedas decirles una parte. El diseño correcto habría sido tener una versión legible de la interfaz, y luego la interfaz completa hereda de eso.

  • No hay mejor interfaz para usar. Por ejemplo, tengo una clase que le permite acceder a los elementos por índice, verificar si contiene un elemento y en qué índice tiene algún tamaño, puede copiarlo en una matriz ... parece un trabajo IList<T>pero mi clase tiene tiene un tamaño fijo y no admite agregar ni quitar, por lo que funciona más como una matriz que como una lista. Pero no hay IArray<T>en el BCL .

  • La interfaz pertenece a una API que se transfiere a múltiples plataformas, y en la implementación de una plataforma en particular, algunas partes no son compatibles. Idealmente, habría alguna forma de detectarlo, de modo que el código portátil que usa dicha API pueda decidir llamar o no esas partes ... pero si las llama, es totalmente apropiado obtenerlas NotSupportedException. Esto es particularmente cierto, si este es un puerto a una nueva plataforma que no estaba prevista en el diseño original.


También considere por qué no es compatible?

A veces InvalidOperationExceptiones una mejor opción. Por ejemplo, una forma más de agregar polimorfismo en una clase es tener varias implementaciones de una interfaz interna y su código está eligiendo cuál instanciar dependiendo de los parámetros dados en el constructor de la clase. [Esto es particularmente útil si sabe que el conjunto de opciones es fijo y no desea permitir que se introduzcan clases de terceros por inyección de dependencia.] He hecho esto para hacer una copia de seguridad de ThreadLocal porque la implementación de seguimiento y no seguimiento son demasiado lejos, y ¿qué implica ThreadLocal.Valuesla implementación sin seguimiento?InvalidOperationExceptionaunque no depende del estado del objeto. En este caso, presenté la clase yo mismo, y sabía que este método tenía que implementarse simplemente lanzando una excepción.

A veces un valor predeterminado tiene sentido. Por ejemplo, en lo ICollection<T>.IsReadOnlymencionado anteriormente, tiene sentido devolver 'verdadero' o 'falso' según el caso. Entonces ... ¿cuál es la semántica de IFoo.Bar? quizás haya algún valor predeterminado razonable para devolver.


Anexo: si tiene el control de la interfaz (y no necesita quedarse con ella por compatibilidad) no debería haber un caso en el que deba lanzar NotSupportedException. Sin embargo, es posible que tenga que dividir la interfaz en dos o más interfaces más pequeñas para tener el ajuste adecuado para su caso, lo que puede conducir a la "contaminación" en situaciones extremas.

Theraot
fuente
0

¿Alguien más se ha encontrado con una situación similar antes?

Sí, lo hicieron los diseñadores de la biblioteca .Net. La clase Diccionario hace lo que estás haciendo. Utiliza una implementación explícita para ocultar efectivamente * algunos de los métodos IDictionary. Es explicó mejor aquí , pero para resumir, con el fin de utilizar Agregar del diccionario, CopyTo, o quitar métodos que toman KeyValuePairs, primero hay que convertir el objeto a una IDictionary.

* No está "ocultando" los métodos en el sentido estricto de la palabra "ocultar" como lo usa Microsoft . Pero no conozco un término mejor en este caso.

usuario2023861
fuente
... lo cual es terrible.
Lightness compite con Monica el
¿Por qué el voto negativo?
user2023861
2
Probablemente porque no respondiste la pregunta. Ish Más o menos.
Lightness compite con Monica el
@LightnessRacesinOrbit, actualicé mi respuesta para hacerlo más claro. Espero que ayude al OP.
user2023861
2
Parece que me responde la pregunta. El hecho de que pueda ser una buena o mala idea no parece estar dentro del alcance de la pregunta, pero aún puede ser útil para que las respuestas lo indiquen.
Ellesedil
-6

Siempre puede implementar y devolver un valor benigno o un valor predeterminado. Después de todo, es solo una propiedad. Podría devolver 0 (la propiedad predeterminada de int), o cualquier valor que tenga sentido en su implementación (int.MinValue, int.MaxValue, 1, 42, etc.)

//We don't officially implement this property
int IFoo.Bar
{
     { get; }
}

Lanzar una excepción parece una mala forma.

Jon Raynor
fuente
2
¿Por qué? ¿Cómo es mejor devolver datos incorrectos?
Lightness compite con Monica el
1
Esto rompe LSP: vea la respuesta de Jimmy Hoffa para una explicación de por qué.
55
Si no hay posibles valores de retorno correctos, es mejor lanzar una excepción que devolver un valor incorrecto. No importa lo que haga, su programa funcionará mal si invoca accidentalmente esta propiedad. Pero si lanzas una excepción, entonces será obvio por qué está funcionando mal.
Tanner Swett
¡Ahora no sé cómo el valor sería incorrecto cuando estás implementando la interfaz! Es tu implementación, puede ser lo que quieras.
Jon Raynor
¿Qué pasa si el valor correcto era desconocido en el momento de la compilación?
Andy