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 public
un 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?
fuente
Respuestas:
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!).
fuente
1) Incluso si las clases concretas son imitables, aún requiere que usted haga miembros
virtual
y 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áticamentevirtual
constructores 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.
fuente
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.
fuente
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.
fuente
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.
fuente