¿Qué quieren decir los programadores cuando dicen, " Código contra una interfaz, no un objeto."?


He comenzado la larga y ardua búsqueda de aprender y aplicar TDD a mi flujo de trabajo. Tengo la impresión de que el TDD encaja muy bien con los principios del CoI.

Después de navegar por algunas de las preguntas etiquetadas TDD aquí en SO, leí que es una buena idea programar contra interfaces, no contra objetos.

¿Puede proporcionar ejemplos de código simples de qué es esto y cómo aplicarlo en casos de uso reales? Ejemplos simples es clave para mí (y otras personas que quieren aprender) para comprender la concepto.

Muchas Gracias.

Author: Billy ONeal, 2010-12-16

7 answers

Considere:

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 un MyClass, si desea reemplazar MyClass con un objeto simulado para realizar pruebas unitarias, no puede hacerlo. 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 utiliza solo una interfaz, no una implementación concreta en particular. A continuación, puede implementar esa interfaz para crear cualquier tipo de mock o fake que desee para fines de prueba. Incluso hay bibliotecas como Rhino Mocks' Rhino.Mocks.MockRepository.StrictMock<T>(), que toman cualquier interfaz y lo construyen un objeto simulado sobre la marcha.

 77
Author: Billy ONeal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-26 19:38:12

Todo es cuestión de intimidad. Si codificas para una implementación (un objeto realizado) estás en una relación bastante íntima con ese "otro" código, como consumidor de él. Significa que tienes que saber cómo construirlo (es decir, qué dependencias tiene, posiblemente como parámetros de constructor, posiblemente como setters), cuándo deshacerte de él, y probablemente no puedas hacer mucho sin él.

Una interfaz delante del objeto realizado le permite hacer algunas cosas -

  1. Para uno usted puede / debe aprovechar una fábrica para construir instancias del objeto. Los contenedores del COI hacen esto muy bien para usted, o usted puede hacer su propio. 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 fábrica, puede construir instancias reales o instancias simuladas de la clase. En producción usaría real, por supuesto, pero para probar, es posible que desee crear instancias stubbed o dinámicamente burladas 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 a su proceso o incluso al sistema. Si alguna vez programó Java RMI o old skool EJB, conoce la rutina de "hablar con la interfaz" que ocultaba un proxy que hacía las tareas de red remota y marshalling que su cliente no tenía que preocuparse. WCF tiene una filosofía similar de "hable con la interfaz" y deje que el sistema determine cómo comunicarse con el objeto/servicio de destino.

* * ACTUALIZAR ** Se solicitó un ejemplo de contenedor COI (Fábrica). Hay muchos por ahí para casi todas las plataformas, pero en su núcleo funcionan así:

  1. Inicializa el contenedor en la rutina de inicio de aplicaciones. Algunos frameworks 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 una fábrica para las interfaces que implementan (por ejemplo: register MyServiceImpl para la interfaz de servicio). Durante este proceso de registro, normalmente hay alguna política de comportamiento que puede proporcionar, como si se crea una nueva instancia cada vez o se usa una sola instancia(ton)

  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, una implementación de esa interfaz se proporciona a su vez y así sucesivamente).

Pseudo-codiciosamente 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<Service>();

//Blissfully unaware of the implementation, call the service method.
myService.DoGoodWork();
 18
Author: hoserdude,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-03 05:08:36

Al programar contra una interfaz, escribirá código que usa una instancia de una interfaz, no un tipo concreto. Por ejemplo, puede usar el siguiente patrón, que incorpora inyección de constructor. La inyección de constructor y otras partes de la inversión del control no son necesarias para poder programar contra interfaces, sin embargo, ya que viene desde la perspectiva de TDD y IoC, lo he cableado de esta manera para darle un contexto que espero le resulte familiar con.

public class PersonService
{
    private readonly IPersonRepository repository;

    public PersonService(IPersonRepository repository)
    {
        this.repository = repository;
    }

    public IList<Person> PeopleOverEighteen
    {
        get
        {
            return (from e in repository.Entities where e.Age > 18 select e).ToList();
        }
    }
}

El objeto repositorio se pasa y es un tipo de interfaz. El beneficio de pasar una interfaz es la capacidad de "intercambiar" la implementación concreta sin cambiar el uso.

Por ejemplo, uno asumiría que en tiempo de ejecución el contenedor IoC inyectará un repositorio que está cableado para golpear la base de datos. Durante el tiempo de prueba, puede pasar en un repositorio mock o stub para ejercer su método PeopleOverEighteen.

 8
Author: Michael Shimmins,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-12-16 00:57:05

Significa pensar genérico. No es específico.

Supongamos que tiene una aplicación que notifica al usuario enviándole algún mensaje. Si trabaja utilizando 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 ser notificado con un correo electrónico y por lo que su CoI creará una clase concreta EmailMessage. Algunos otros quieren SMS, y se crea una instancia de SmsMessage.

En todos estos casos, el código para notificar el usuario nunca será cambiado. Incluso si agregas otra clase concreta.

 3
Author: Lorenzo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-12-16 00:55:05

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

Un ejemplo que he mencionado aquí antes en algún lugar 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 permitan acceder a los valores de configuración. Normalmente usted suministraría un implementación que lee desde el archivo de configuración, pero para probar puede usar uno que solo devuelve valores de prueba o arroja excepciones o lo que sea.

Considere también su capa de acceso a datos. Tener su lógica de negocio estrechamente acoplada a una implementación de acceso a datos en particular hace que sea 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.

Usando interfaces aumenta la" superficie " disponible para las pruebas, lo que permite pruebas de grano más fino que realmente prueban unidades individuales de su código.

 2
Author: Andrew Kennan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-12-16 00:54:19

Pruebe su código como alguien que lo usaría después de leer la documentación. No pruebe nada basado en el conocimiento que tiene porque ha escrito o leído el código. Debe asegurarse de que su código se comporta como se espera.

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

Si sigue 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á unidades atómicas, que en sí mismas no tienen dependencias y tendrá unidades que dependen de otras unidades hasta que finalmente llegue a la aplicación que en sí misma es una unidad.

Debe probar cada capa, no confíe en el hecho de que al probar la unidad A también prueba la unidad B de la que depende la unidad A (la regla se aplica también a la herencia.) Esto, también, debe ser tratado como un detalle de implementación, a pesar de que podría sentir como si te estuvieras repitiendo a ti 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 burlas si es necesario.

En lenguajes más dinámicos esto no es un gran problema, aquí puede usar duck typing, herencia múltiple y mixins para componer casos de prueba. Si empiezas no le gusta la herencia en general, probablemente lo está haciendo bien.

 2
Author: DasIch,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-12-26 16:54:05

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 de simulación, puede hacer en su objeto simulado lo que quiera.

 1
Author: BЈовић,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-12-16 11:30:38