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


79

Comencé la búsqueda muy larga y ardua 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 con TDD aquí en SO, leí que es una buena idea programar contra interfaces, no objetos.

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


7
Esto es una especie de OOP en lugar de una cosa específica de C # ...
Billy ONeal

2
@Billy ONeal: Podría ser, pero dado que las interfaces actúan de manera diferente en, por ejemplo, C # / Java, me gustaría aprender primero en el lenguaje con el que estoy más familiarizado.

11
@Sergio Boombastic: el concepto de programar en una interfaz no tiene nada que ver con una interfaceen Java o C #. De hecho, cuando se escribió el libro del que se tomó esta cita, ni Java ni C # existían.
Jörg W Mittag

1
@ Jörg: bueno, tiene algo que ver con eso. Las interfaces en los lenguajes de programación orientada a objetos recientes ciertamente están destinadas a ser utilizadas como se describe en la cita.
Michael Petrotta

1
@Michael Petrotta: Sin embargo, no son muy buenos en eso. Por ejemplo, la interfaz de Listdice que después de addagregar un elemento a la lista, el elemento está en la lista y la longitud de la lista aumenta en 1. ¿Dónde dice realmente eso en [ interface List] ( Download.Oracle.Com/javase/7/docs/api/java/util/List.html#add )?
Jörg W Mittag

Respuestas:


83

Considerar:

class MyClass
{
    //Implementation
    public void Foo() {}
}

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

Debido a que MyMethodsolo acepta a MyClass, si desea reemplazar MyClasscon un objeto simulado para realizar una prueba unitaria, 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 en particular. Luego, puede implementar esa interfaz para crear cualquier tipo de simulación que desee con fines de prueba. Incluso hay bibliotecas como Rhino Mocks ' Rhino.Mocks.MockRepository.StrictMock<T>(), que toman cualquier interfaz y crean un objeto simulado sobre la marcha.


14
Nota al margen: no siempre tiene que ser una interfaz real en el sentido en que el idioma que está utilizando utiliza la palabra. También podría ser una clase abstracta que no es del todo irrazonable, dadas las restricciones que Java o C # imponen a la herencia.
Joey

2
@Joey: Podría usar una clase abstracta, sí, pero si lo hace, debe diseñar la clase para que sea heredable, lo que puede ser un poco más complicado. Por supuesto, en lenguajes como C ++, que no tienen una interfaz a nivel de lenguaje, haces exactamente eso: crear una clase abstracta. Sin embargo, tenga en cuenta que en Java y C # es mejor usar la interfaz, porque puede heredar de varias interfaces, pero solo una clase. Permitir la herencia de múltiples interfaces lo alienta a hacer sus interfaces más pequeñas, lo cual es algo bueno TM :)
Billy ONeal

5
@Joey: ni siquiera tienes que usar nada . En Ruby, por ejemplo, la interfaz con la que programa normalmente solo se documenta en inglés, si es que se documenta. Sin embargo, eso no hace que sea menos una interfaz . Por el contrario, colocar interfacepalabras clave en todo el código no significa que esté programando contra interfaces . Experimento mental: tome un horrible código incohesivo estrechamente acoplado. Para cada clase, simplemente cópielo y péguelo, elimine todos los cuerpos del método, reemplace la classpalabra clave con interfacey actualice todas las referencias en el código a ese tipo. ¿El código ahora es mejor?
Jörg W Mittag

Jörg, esa es una buena forma de verlo, de hecho :-)
Joey

Más allá de las burlas, ocultar los detalles de implementación le permite cambiar el código debajo para mejorar el rendimiento, etc. sin cambiar el código de llamada. Java hace esto con List, como ejemplo. La implementación debajo puede ser una matriz, pila, etc.
Brian Pan

18

Todo es cuestión de intimidad. Si codifica a una implementación (un objeto realizado), está 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 establecedores), cuándo deshacerte de él, y probablemente no puedas hacer mucho sin él.

Una interfaz frente al objeto realizado le permite hacer algunas cosas:

  1. Por un lado, puede / debe aprovechar una fábrica para construir instancias del objeto. Los contenedores IOC hacen esto muy bien por usted, o puede hacer los suyos propios. Con las tareas de construcción fuera de su responsabilidad, su código puede simplemente asumir que está obteniendo lo que necesita. En el otro lado del muro de la fábrica, puede construir instancias reales o simular instancias de la clase. En producción, usaría real, por supuesto, pero para las pruebas, es posible que desee crear instancias simuladas dinámicamente o stubped para probar varios estados del sistema sin tener que ejecutar el sistema.
  2. No es necesario que sepa dónde está el objeto. Esto es útil en sistemas distribuidos donde el objeto con el que desea hablar puede ser o no local para su proceso o incluso para el sistema. Si alguna vez programó Java RMI o EJB de skool antiguo, conoce la rutina de "hablar con la interfaz" que ocultaba un proxy que realizaba las tareas de red remota y ordenación 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 de destino.

** ACTUALIZACIÓN ** Se solicitó un ejemplo de contenedor IOC (fábrica). Hay muchos disponibles para casi todas las plataformas, pero en esencia funcionan así:

  1. Inicializa el contenedor en la rutina de inicio de sus 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 una fábrica para las interfaces que implementan (por ejemplo: registrar 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 si se usa una sola (tonelada) instancia

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

//Blissfully unaware of the implementation, call the service method.
myService.DoGoodWork();

+1 para el punto 1. Eso fue muy claro y comprensible. El punto 2 pasó por encima de mi cabeza. : P

1
@Sergio: Lo que hoserdude significa es que hay muchos casos en los que su código nunca sabe cuál es el implementador real del objeto, porque lo implementa el Framework, o alguna otra biblioteca, en su nombre, automáticamente.
Billy ONeal

¿Puede dar un buen ejemplo básico de una fábrica de contenedores de IoC?
bulltorious

9

Al programar contra una interfaz, escribirá código que use 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 del constructor y otras partes de la inversión de control no son necesarias para poder programar contra interfaces, sin embargo, dado que viene desde la perspectiva de TDD e IoC, lo he conectado de esta manera para darle un contexto que espero 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 del 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, se podría suponer que en tiempo de ejecución, el contenedor de IoC inyectará un repositorio que está conectado a la base de datos. Durante el tiempo de prueba, puede pasar un repositorio simulado o de código auxiliar para ejercitar su PeopleOverEighteenmétodo.


3
+1: debe tenerse en cuenta que no hay ninguna razón por la que realmente necesite algo como un contenedor de IoC para usar las interfaces de manera efectiva.
Billy ONeal

Entonces, básicamente, ¿el único beneficio que obtengo es la capacidad de usar un marco de simulación?

Y extensibilidad. La capacidad de prueba es un efecto secundario agradable de un sistema altamente cohesivo y poco acoplado. Al pasar las interfaces, elimina la preocupación sobre lo que hace la clase real. Ya no le preocupa cómo lo hace, sino solo que afirma que lo hace . Esto refuerza la separación de preocupaciones y le permite concentrarse en el requisito específico del trabajo actual.
Michael Shimmins

@Sergio: Necesitas algo como esto para hacer cualquier tipo de simulacro. No solo marcos. @Michael: Pensé que "DDD" era "Desarrollo impulsado por el desarrollador" ... ¿se supone que es TDD?
Billy ONeal

1
@Billy - Asumía el desarrollo impulsado por el dominio
Michael Shimmins

3

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

Suponga que tiene una aplicación que notifica al usuario enviándole algún mensaje. Si trabaja usando una interfaz IMessage por ejemplo

interface IMessage
{
    public void Send();
}

puede personalizar, por usuario, la forma en que reciben el mensaje. Por ejemplo, alguien quiere ser notificado con un correo electrónico y entonces su IoC creará una clase concreta EmailMessage. Otro quiere SMS y usted crea una instancia de SMSMessage.

En todos estos casos, el código de notificación al usuario nunca se modificará. Incluso si agrega otra clase concreta.


@Billy: la parte "O" de [SOLID] ( en.wikipedia.org/wiki/Solid_(object-oriented_design) , ¡gracias!
Lorenzo


@Billy: oops ... se comió el último paréntesis. Este debería funcionar
Lorenzo


2

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 la prueba.

Un ejemplo que he mencionado aquí antes en alguna parte 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, proporcionaría 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 negocio estrechamente acoplada a una implementación de acceso a datos en particular dificulta la prueba sin tener a mano una base de datos completa 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 realizar pruebas más detalladas que realmente prueban unidades individuales de su código.


2

Pruebe su código como alguien que lo usaría después de leer la documentación. No pruebe nada basándose en los conocimientos que tiene 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, las pruebas de documentación en Python son un buen ejemplo de 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ás unidades atómicas, que en sí mismas no tienen dependencias y tendrás unidades que dependerán 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 el hecho de que al probar la unidad A, también prueba la unidad B de qué unidad A depende (la regla se aplica también a la herencia). Esto también debe tratarse como un detalle de implementación, incluso aunque puede sentir como si se estuviera repitiendo.

Tenga en cuenta que es poco probable que cambien las pruebas una vez escritas, 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 poder crear simulaciones si es necesario.

En lenguajes más dinámicos, esto no es un gran problema, aquí puede usar la escritura pato, herencia múltiple y mixins para componer casos de prueba. Si empieza a no gustarle la herencia en general, probablemente lo esté haciendo bien.


1

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

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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.