¿Qué quieren decir los progtwigdores cuando dicen: “Código contra una interfaz, no un objeto”?

Comencé la ardua y larga búsqueda para aprender y aplicar TDD a mi flujo de trabajo. Tengo la impresión de que TDD encaja muy bien con los principios de IoC.

Después de examinar algunas de las preguntas etiquetadas de TDD aquí en SO, leo que es una buena idea progtwigr contra interfaces, no objetos.

¿Puede proporcionar ejemplos de código simple de qué es esto y cómo aplicarlo en casos de uso real? Los ejemplos simples son clave para mí (y otras personas que desean aprender) para comprender los conceptos.

Muchas gracias.

Considerar:

 class MyClass { //Implementation public void Foo() {} } class SomethingYouWantToTest { public bool MyMethod(MyClass c) { //Code you want to test c.Foo(); } } 

Debido a que MyMethod solo acepta una MyClass , si desea reemplazar MyClass con un objeto de prueba para probar la unidad, no puede. Mejor es usar una interfaz:

 interface IMyClass { void Foo(); } class MyClass : IMyClass { //Implementation public void Foo() {} } class SomethingYouWantToTest { public bool MyMethod(IMyClass c) { //Code you want to test c.Foo(); } } 

Ahora puede probar MyMethod , porque usa solo una interfaz, no una implementación concreta concreta. Luego puede implementar esa interfaz para crear cualquier tipo de simulacro o simulación que desee para fines de prueba. Incluso hay bibliotecas como Rhino Rhino.Mocks.MockRepository.StrictMock()Rhino.Mocks.MockRepository.StrictMock() , que toman cualquier interfaz y construyen un objeto simulado sobre la marcha.

Es todo una cuestión de intimidad. Si codifica una implementación (un objeto realizado) se encuentra en una relación muy íntima con ese “otro” código, como consumidor de él. Significa que debe saber cómo construirlo (es decir, qué dependencias tiene, posiblemente como parámetros de constructor, posiblemente como instaladores), cuándo deshacerse de él, y es probable que no pueda hacer mucho sin él.

Una interfaz en frente del objeto realizado le permite hacer algunas cosas:

  1. Por un lado, puede / debería aprovechar una fábrica para construir instancias del objeto. Los contenedores de COI lo hacen muy bien, o puede hacer los suyos propios. Con las tareas de construcción fuera de su responsabilidad, su código puede asumir que está obteniendo lo que necesita. En el otro lado del muro de la fábrica, puedes construir instancias reales o simular instancias de la clase. En producción, utilizaría real, por supuesto, pero para la prueba, puede crear instancias trucadas o dinámicas para probar varios estados del sistema sin tener que ejecutar el sistema.
  2. No tienes que saber dónde está el objeto. Esto es útil en sistemas distribuidos donde el objeto con el que desea hablar puede o no ser local para su proceso o incluso para el sistema. Si alguna vez programó Java RMI o el viejo skool EJB, conoce la rutina de “hablar con la interfaz” que ocultaba un proxy que hacía las tareas de red y clasificación remota que su cliente no tenía que preocuparse. WCF tiene una filosofía similar de “hablar con la interfaz” y dejar que el sistema determine cómo comunicarse con el objeto / servicio objective.

** ACTUALIZACIÓN ** Hubo una solicitud de un ejemplo de un contenedor IOC (fábrica). Hay muchos por ahí para casi todas las plataformas, pero en su núcleo funcionan así:

  1. Inicializas el contenedor en tu rutina de inicio de aplicaciones. Algunos marcos hacen esto a través de archivos de configuración o código, o ambos.

  2. Usted “registra” las implementaciones que desea que el contenedor cree para usted como fábrica para las interfaces que implementan (por ejemplo: registrar MyServiceImpl para la interfaz del servicio). Durante este proceso de registro, normalmente hay alguna política de comportamiento que puede proporcionar, como por ejemplo si se crea una instancia nueva cada vez o si se usa una sola instancia (tonelada).

  3. Cuando el contenedor crea objetos para usted, inyecta cualquier dependencia en esos objetos como parte del proceso de creación (es decir, si su objeto depende de otra interfaz, a su vez se proporciona una implementación de esa interfaz y así sucesivamente).

Pseudo-codishly podría verse así:

 IocContainer container = new IocContainer(); //Register my impl for the Service Interface, with a Singleton policy container.RegisterType(Service, ServiceImpl, LifecyclePolicy.SINGLETON); //Use the container as a factory Service myService = container.Resolve(); //Blissfully unaware of the implementation, call the service method. myService.DoGoodWork(); 

Al progtwigr en una interfaz, se escribirá un código que utiliza una instancia de una interfaz, no un tipo concreto. Por ejemplo, puede usar el siguiente patrón, que incorpora la inyección de constructor. La inyección de constructores y otras partes de la inversión de control no son necesarias para poder progtwigr contra las interfaces, sin embargo, como vienes desde la perspectiva de TDD e IoC, lo he cableado de esta manera para darte un contexto con el que estás con suerte familiar con.

 public class PersonService { private readonly IPersonRepository repository; public PersonService(IPersonRepository repository) { this.repository = repository; } public IList PeopleOverEighteen { get { return (from e in repository.Entities where e.Age > 18 select e).ToList(); } } } 

El objeto de repository se transfiere y es un tipo de interfaz. El beneficio de pasar una interfaz es la capacidad de ‘cambiar’ la implementación concreta sin cambiar el uso.

Por ejemplo, uno supondría que en tiempo de ejecución, el contenedor IoC inyectará un repository que está conectado para golpear la base de datos. Durante el tiempo de prueba, puede pasar un repository simulado o de PeopleOverEighteen para ejercitar su método PeopleOverEighteen .

Significa pensar genérico. No específico.

Supongamos que tiene una aplicación que notifica al usuario que le envía algún mensaje. Si trabajas usando una interfaz IMessage por ejemplo

 interface IMessage { public void Send(); } 

puede personalizar, por usuario, la forma en que recibe el mensaje. Por ejemplo, alguien quiere que se le notifique con un correo electrónico, por lo que su IoC creará una clase concreta de EmailMessage. Algunos otros quieren SMS, y usted crea una instancia de SMSMessage.

En todos estos casos, el código para notificar al usuario nunca cambiará. Incluso si agrega otra clase concreta.

La gran ventaja de progtwigr contra las interfaces al realizar pruebas unitarias es que le permite aislar un fragmento de código de cualquier dependencia que desee analizar por separado o simular durante la prueba.

Un ejemplo que he mencionado aquí antes es el uso de una interfaz para acceder a los valores de configuración. En lugar de mirar directamente a ConfigurationManager, puede proporcionar una o más interfaces que le permiten acceder a los valores de configuración. Normalmente, debe proporcionar una implementación que lea desde el archivo de configuración, pero para las pruebas puede usar una que solo devuelva valores de prueba o arroje excepciones o lo que sea.

Considere también su capa de acceso a datos. Tener su lógica de negocios estrechamente vinculada a una implementación de acceso a datos en particular lo hace difícil de probar sin tener una base de datos completa a mano con los datos que necesita. Si su acceso a los datos está oculto detrás de las interfaces, puede proporcionar solo los datos que necesita para la prueba.

El uso de interfaces aumenta el “área de superficie” disponible para las pruebas, lo que permite pruebas de granulación más finas que realmente prueban las unidades individuales de su código.

Pruebe su código como alguien que lo usaría después de leer la documentación. No pruebe nada según el conocimiento que tenga porque ha escrito o leído el código. Desea asegurarse de que su código se comporte como se espera.

En el mejor de los casos, debería poder usar sus pruebas como ejemplos, los doctests en Python son un buen ejemplo para esto.

Si sigues estas pautas, cambiar la implementación no debería ser un problema.

También en mi experiencia, es una buena práctica probar cada “capa” de su aplicación. Tendrás unidades atómicas, que en sí mismas no tienen dependencias y tendrás unidades que dependen de otras unidades hasta que finalmente llegues a la aplicación, que en sí misma es una unidad.

Debe probar cada capa, no confíe en que al probar la unidad A también prueba la unidad B de la que depende la unidad A (también se aplica a la herencia). Esto también debe tratarse como un detalle de implementación, incluso aunque puede sentir que se está repitiendo a sí mismo.

Tenga en cuenta que es poco probable que las pruebas escritas cambien mientras que el código que prueban cambiará casi definitivamente.

En la práctica, también existe el problema de IO y el mundo exterior, por lo que desea utilizar interfaces para que pueda crear simulaciones si es necesario.

En los lenguajes más dynamics, esto no es un gran problema, aquí puede usar la tipificación de pato, herencia múltiple y mixins para componer casos de prueba. Si comienza a desagradar la herencia en general, probablemente lo esté haciendo bien.

Este screencast explica el desarrollo ágil y TDD en la práctica para c #.

Al codificar contra una interfaz, significa que en su prueba, puede usar un objeto simulado en lugar del objeto real. Al usar un buen marco simulacro, puede hacer en su objeto simulado lo que quiera.