Cuándo usar interfaces (prueba de unidad, IoC?)

17

Sospecho que he cometido un error de escolar aquí, y estoy buscando una aclaración. Muchas de las clases en mi solución (C #), me atrevo a decir la mayoría, terminé escribiendo la interfaz correspondiente. Por ejemplo, una interfaz "ICalculator" y una clase "Calculator" que lo implementa, aunque es probable que nunca reemplace esa calculadora con una implementación diferente. Además, la mayoría de estas clases residen en el mismo proyecto que sus dependencias: realmente solo necesitan serlo internal, pero terminaron siendo publicun efecto secundario de la implementación de sus respectivas interfaces.

Creo que esta práctica de crear interfaces para todo surgió de algunas falsedades:

1) Originalmente pensé que era necesaria una interfaz para crear simulacros de prueba unitaria (estoy usando Moq), pero desde entonces descubrí que una clase se puede burlar si sus miembros lo son virtual, y tiene un constructor sin parámetros (corrígeme si Me equivoco).

2) Originalmente pensé que era necesaria una interfaz para registrar una clase con el marco IoC (Castle Windsor), p. Ej.

Container.Register(Component.For<ICalculator>().ImplementedBy<Calculator>()...

cuando de hecho podría registrar el tipo concreto contra sí mismo:

Container.Register(Component.For<Calculator>().ImplementedBy<Calculator>()...

3) El uso de interfaces, por ejemplo, parámetros del constructor para la inyección de dependencias, da como resultado un "acoplamiento flojo".

Entonces, ¿me he vuelto loco con las interfaces? Soy consciente de los escenarios en los que "normalmente" usaría una interfaz, por ejemplo, exponer una API pública, o para cosas como la funcionalidad "conectable". Mi solución tiene un pequeño número de clases que se ajustan a tales casos de uso, pero me pregunto si todas las demás interfaces son innecesarias y deberían eliminarse. Con respecto al punto 3) anterior, ¿no estaría violando el "acoplamiento flojo" si hiciera esto?

Editar : - Solo estoy jugando con Moq, y parece requerir métodos para ser públicos y virtuales, y tener un constructor público sin parámetros, para poder burlarse de ellos. ¿Entonces parece que no puedo tener clases internas?

Andrew Stephens
fuente
Estoy bastante seguro de que C # no requiere que hagas pública una clase para implementar una interfaz, incluso si la interfaz es pública ...
vaughandroid
1
Se supone que no debes probar a miembros privados . Esto puede verse como un indicador del antipatrón de la clase de Dios. Si siente la necesidad de probar la funcionalidad privada de la unidad, generalmente es una buena idea encapsular esa funcionalidad en su propia clase.
Spoike
@Spoike el proyecto en cuestión es un proyecto de interfaz de usuario, donde parecía natural hacer todas las clases internas, pero todavía necesitan pruebas unitarias? He leído en otra parte sobre no necesitar probar métodos internos, pero nunca he entendido las razones.
Andrew Stephens
Primero pregúntese cuál es la unidad bajo prueba. ¿Es toda la clase o un método? Esa unidad debe ser pública para que la unidad se pruebe adecuadamente. En proyectos de UI, generalmente separo la lógica comprobable en controladores / modelos de vista / servicios que tienen métodos públicos. Las vistas / widgets reales se conectan a los controladores / modelos de vista / servicios y se prueban mediante pruebas de humo regulares (es decir, inicie la aplicación y comience a hacer clic). Puede tener escenarios que deberían probar la funcionalidad subyacente y realizar pruebas unitarias en los widgets conduce a pruebas frágiles y debe hacerse con cuidado.
Spoike
@Spoike, ¿estás haciendo públicos tus métodos de VM solo para que sean accesibles para nuestras pruebas unitarias? Tal vez ahí es donde me estoy equivocando, tratando de hacer que todo sea interno. Sin embargo, creo que mis manos podrían estar vinculadas con Moq, lo que parece requerir que las clases (no solo los métodos) sean públicas para burlarse de ellas (vea mi comentario sobre la respuesta de Telastyn).
Andrew Stephens

Respuestas:

7

En general, si crea una interfaz que solo tiene un implementador, solo está escribiendo dos veces y perdiendo el tiempo. Las interfaces por sí solas no proporcionan un acoplamiento suelto si están estrechamente acopladas a una implementación ... Dicho esto, si quieres burlarte de estas cosas en las pruebas unitarias, generalmente es una gran señal de que inevitablemente necesitarás más de un implementador en realidad código.

Sin embargo, consideraría un poco un olor a código si casi todas sus clases tienen interfaces. Eso significa que casi todos están trabajando entre sí de una forma u otra. Sospecharía que las clases están haciendo demasiado (ya que no hay clases auxiliares) o que ha abstraído demasiado (¡oh, quiero una interfaz de proveedor para la gravedad ya que eso podría cambiar!).

Telastyn
fuente
1
Gracias por la respuesta. Como se mencionó, hay algunas razones equivocadas por las que hice lo que hice, aunque sabía que la mayoría de las interfaces nunca tendrían más de una implementación. Parte del problema es que estoy usando el marco Moq, que es excelente para burlarse de las interfaces, pero para burlarse de una clase debe ser público, tener un ctr público sin parámetros, y los métodos para burlarse deben ser 'públicos virtuales').
Andrew Stephens
La mayoría de las clases que estoy probando son internas, y no quiero hacerlas públicas solo para hacerlas verificables (aunque se podría argumentar, para eso he escrito todas estas interfaces). Si me deshago de mis interfaces, ¡probablemente necesite encontrar un nuevo marco de imitación!
Andrew Stephens
1
@ AndrewStephens: no todo el mundo necesita burlarse. Si las clases son lo suficientemente sólidas (o bien acopladas) como para no necesitar interfaces, son lo suficientemente sólidas como para usarlas tal como están en las pruebas unitarias. El tiempo / complejidad que los mueve no justifica los beneficios de la prueba.
Telastyn
-1, ya que a menudo no sabes cuántos implementadores necesitas cuando escribes el código por primera vez. Si usa una interfaz desde el principio, no tiene que cambiar clientes al escribir implementadores adicionales.
Wilbert
1
@wilbert: claro, una interfaz pequeña es más legible que la clase, pero no estás eligiendo una u otra, tienes la clase o una clase y una interfaz. Y puede que estés leyendo demasiado en mi respuesta. No digo que nunca haga interfaces para una sola implementación; la mayoría debería, de hecho, ya que la mayoría de las interacciones deberían requerir esa flexibilidad. Pero lea la pregunta nuevamente. Hacer una interfaz para todo es solo culto de carga. IoC no es una razón. Las burlas no son una razón. Los requisitos son una razón para implementar esa flexibilidad.
Telastyn
4

1) Incluso si las clases concretas son imitables, aún requiere que usted haga miembros virtualy proporcione un constructor sin parámetros, que puede ser molesto y no deseado. Usted (o los nuevos miembros del equipo) pronto se encontrarán agregando sistemáticamente virtualconstructores sin parámetros en cada nueva clase sin pensarlo dos veces, solo porque "así es como funcionan las cosas".

Una interfaz es IMO, una idea mucho mejor cuando necesita burlarse de una dependencia, porque le permite diferir la implementación hasta cuando realmente la necesita, y lo convierte en un buen contrato claro de la dependencia.

3) ¿Cómo es eso una falsedad?

Creo que las interfaces son excelentes para definir protocolos de mensajería entre objetos de colaboración. Puede haber casos en que la necesidad de ellos sea discutible, como ocurre con las entidades de dominio, por ejemplo. Sin embargo, siempre que tenga un socio estable (es decir, una dependencia inyectada en lugar de una referencia transitoria) con la que necesita comunicarse, generalmente debería considerar usar una interfaz.

guillaume31
fuente
3

La idea de IoC es hacer que los tipos de concreto sean reemplazables. Entonces, incluso (en este momento) solo tiene una sola Calculadora que implementa ICalculator, una segunda implementación podría depender solo de la interfaz y no de los detalles de implementación de la Calculadora.

Por lo tanto, la primera versión de su registro IoC-Container es la correcta y la que debe usar en el futuro.

Si haces que tus clases sean públicas o internas, no está realmente relacionado, y tampoco lo está el moq-ing.

Wilbert
fuente
2

Realmente estaría más preocupado por el hecho de que parece sentir la necesidad de "burlarse" de casi todos los tipos en todo su proyecto.

Los dobles de prueba son principalmente útiles para aislar su código del mundo externo (librerías de terceros, E / S de disco, E / S de red, etc.) o de cálculos realmente caros. Ser demasiado liberal con su marco de aislamiento (¿REALMENTE lo necesita? ¿Su proyecto es tan complejo?) Puede conducir fácilmente a pruebas que se unen a la implementación, y hace que su diseño sea más rígido. Si escribe un montón de pruebas que verifican que alguna clase llamó al método X e Y en ese orden, y luego pasó los parámetros a, byc al método Z, ¿REALMENTE está obteniendo valor? A veces, obtiene pruebas como esta cuando prueba el registro o la interacción con bibliotecas de terceros, pero debería ser la excepción, no la regla.

Tan pronto como su marco de aislamiento le indique que debe hacer que las cosas sean públicas y que siempre debe tener constructores sin parámetros, es hora de evadirse, porque en este punto su marco lo ESTÁ FORZANDO a escribir código malo (peor).

Hablando más específicamente sobre las interfaces, creo que son útiles en algunos casos clave:

  • Cuando necesita despachar llamadas de método a diferentes tipos, por ejemplo, cuando tiene implementaciones múltiples (esta es la obvia, ya que la definición de una interfaz en la mayoría de los idiomas es solo una colección de métodos virtuales)

  • Cuando necesite poder aislar el resto de su código de alguna clase. Esta es la forma normal de abstraer el sistema de archivos y el acceso a la red y la base de datos, etc., desde la lógica central de su aplicación.

  • Cuando necesita inversión de control para proteger los límites de la aplicación. Esta es una de las formas más poderosas posibles para administrar dependencias. Todos sabemos que los módulos de alto nivel y los módulos de bajo nivel no deberían conocerse entre sí, porque cambiarán por diferentes razones y NO DESEAS tener dependencias en HttpContext en tu modelo de dominio o en algún marco de representación Pdf en tu biblioteca de multiplicación de matrices . Hacer que sus clases de límites implementen interfaces (incluso si nunca se reemplazan) ayuda a aflojar el acoplamiento entre capas, y reduce drásticamente el riesgo de que las dependencias se filtren a través de las capas de tipos que conocen muchos otros tipos.

sara
fuente
1

Me inclino hacia la idea de que, si alguna lógica es privada, entonces la implementación de esa lógica no debería preocupar a nadie y, como tal, no debería ser probada. Si alguna lógica es lo suficientemente compleja como para querer probarla, esa lógica probablemente tenga un papel distinto y, por lo tanto, debería ser pública. Por ejemplo, si extraigo algo de código en un método auxiliar privado, encuentro que generalmente es algo que podría sentarse en una clase pública separada (un formateador, un analizador, un mapeador, lo que sea).
En cuanto a las interfaces, dejo que la necesidad de tener que tropezar o burlarse de la clase me lleve. Cosas como repos, generalmente quiero poder tropezar o burlarme. Un mapeador en una prueba de integración, (generalmente) no tanto.

Stefan Billiet
fuente