Equilibrio de inyección de dependencia con diseño de API pública

13

He estado contemplando cómo equilibrar el diseño comprobable utilizando la inyección de dependencia con el suministro de API pública fija simple. Mi dilema es: la gente querría hacer algo así var server = new Server(){ ... }y no tener que preocuparse por crear las muchas dependencias y gráficos de dependencias que Server(,,,,,,)pueda tener. Durante el desarrollo, no me preocupo demasiado, ya que uso un marco IoC / DI para manejar todo eso (no estoy usando los aspectos de gestión del ciclo de vida de ningún contenedor, lo que complicaría aún más las cosas).

Ahora, es poco probable que las dependencias se vuelvan a implementar. La realización de componentes en este caso es casi exclusivamente para la capacidad de prueba (¡y un diseño decente!) En lugar de crear costuras para la extensión, etc. Las personas 99.999% de las veces desearán usar una configuración predeterminada. Entonces. Podría codificar las dependencias. ¡No quiero hacer eso, perdemos nuestras pruebas! Podría proporcionar un constructor predeterminado con dependencias codificadas y uno que tenga dependencias. Eso es ... desordenado, y probablemente sea confuso, pero viable. Podría hacer que la dependencia que recibe el constructor sea interna y hacer que mi unidad pruebe un ensamblaje amigo (suponiendo C #), que ordena la API pública pero deja una desagradable trampa oculta al acecho por mantenimiento. Tener dos constructores que están implícitamente conectados en lugar de explícitamente sería un mal diseño en general en mi libro.

Por el momento eso es lo menos malvado que se me ocurre. Opiniones? ¿Sabiduría?

kolektiv
fuente

Respuestas:

11

La inyección de dependencia es un patrón poderoso cuando se usa bien, pero con demasiada frecuencia sus profesionales se vuelven dependientes de algún marco externo. La API resultante es bastante dolorosa para aquellos de nosotros que no queremos vincular libremente nuestra aplicación con cinta adhesiva XML. No te olvides de Plain Old Objects (POO).

Primero considere cómo esperaría que alguien use su API, sin el marco involucrado.

  • ¿Cómo instanciar el servidor?
  • ¿Cómo se extiende el servidor?
  • ¿Qué quieres exponer en la API externa?
  • ¿Qué quieres ocultar en la API externa?

Me gusta la idea de proporcionar implementaciones predeterminadas para sus componentes y el hecho de que tenga esos componentes. El siguiente enfoque es una práctica OO perfectamente válida y decente:

public Server() 
    : this(new HttpListener(80), new HttpResponder())
{}

public Server(Listener listener, Responder responder)
{
    // ...
}

Siempre que documente cuáles son las implementaciones predeterminadas para los componentes en los documentos de la API y proporcione un constructor que realice toda la configuración, debería estar bien. Los constructores en cascada no son frágiles siempre que el código de configuración ocurra en un constructor maestro.

Esencialmente, todos los constructores de cara al público tendrían las diferentes combinaciones de componentes que desea exponer. Internamente, llenarían los valores predeterminados y diferirían a un constructor privado que complete la configuración. En esencia, esto le proporciona algo de SECO y es bastante fácil de probar.

Si necesita proporcionar friend acceso a sus pruebas para configurar el objeto Servidor para aislarlo para las pruebas, hágalo. No será parte de la API pública, que es con lo que debes tener cuidado.

Solo sea amable con sus consumidores de API y no requiera un marco IoC / DI para usar su biblioteca. Para tener una idea del dolor que está causando, asegúrese de que sus pruebas unitarias no dependan tampoco del marco IoC / DI. (Es una molestia personal para mascotas, pero tan pronto como introduce el marco ya no es una prueba unitaria, se convierte en una prueba de integración).

Berin Loritsch
fuente
Estoy de acuerdo, y ciertamente no soy fanático de la sopa de configuración de IoC: la configuración XML no es algo que yo use en lo que respecta a IoC. Ciertamente, exigir el uso de IoC es exactamente lo que estaba evitando: es el tipo de suposición que un diseñador de API público nunca puede hacer. Gracias por el comentario, ¡estaba en la línea en la que estaba pensando y es tranquilizador tener un control de cordura de vez en cuando!
kolektiv
-1 Esta respuesta básicamente dice "no use la inyección de dependencia" y contiene suposiciones inexactas. En su ejemplo, si el HttpListenerconstructor cambia, la Serverclase también debe cambiar. Están acoplados Una mejor solución es lo que @Winston Ewert dice a continuación, que es proporcionar una clase predeterminada con dependencias preespecificadas, fuera de la API de la biblioteca. Luego, el programador no está obligado a configurar todas las dependencias, ya que usted toma la decisión por él, pero aún tiene la flexibilidad para cambiarlas más adelante. Esto es un gran problema cuando tienes dependencias de terceros.
M. Dudley
FWIW el ejemplo proporcionado se llama "inyección bastarda". stackoverflow.com/q/2045904/111327 stackoverflow.com/q/6733667/111327
M. Dudley
1
@MichaelDudley, ¿leíste la pregunta del OP? Todos los "supuestos" se basaron en la información proporcionada por el OP. Durante un tiempo, la "inyección bastarda" como la llamaron, fue el formato de inyección de dependencia preferido, particularmente para sistemas cuyas composiciones no cambian durante el tiempo de ejecución. Los setters y getters también son otra forma de inyección de dependencia. Simplemente utilicé ejemplos basados ​​en lo que proporcionó el OP.
Berin Loritsch
4

En Java, en tales casos, es habitual tener una configuración "predeterminada" construida por una fábrica, que se mantiene independiente del servidor que se comporta "correctamente". Entonces, su usuario escribe:

var server = DefaultServer.create();

mientras que el Serverconstructor todavía acepta todas sus dependencias y puede usarse para una personalización profunda.

fdreger
fuente
+1, SRP. La responsabilidad de la oficina de un dentista no es construir la oficina de un dentista. La responsabilidad de un servidor no es construir un servidor.
R. Schmitz
2

¿Qué hay de proporcionar una subclase que proporciona la "API pública"

class StandardServer : Server
{
    public StandardServer():
        this( depends1, depends2, depends3)
    {
    }
}

El usuario puede new StandardServer()y estará en camino. También pueden usar la clase base del servidor si desean tener más control sobre cómo funciona el servidor. Este es más o menos el enfoque que uso. (No uso un marco ya que aún no he visto el punto).

Esto todavía expone la API interna, pero creo que deberías. Ha dividido sus objetos en diferentes componentes útiles que deberían funcionar de forma independiente. No se sabe cuándo un tercero podría querer usar uno de los componentes de forma aislada.

Winston Ewert
fuente
1

Podría hacer que la dependencia que recibe el constructor sea interna y hacer que mi unidad pruebe un ensamblaje amigo (suponiendo C #), que ordena la API pública pero deja una desagradable trampa oculta al acecho por mantenimiento.

Eso no me parece una "trampa oculta desagradable". El constructor público debería estar llamando al interno con las dependencias "predeterminadas". Mientras esté claro que el constructor público no debería modificarse, todo debería estar bien.

Luego.
fuente
0

Estoy totalmente de acuerdo con tu opinión. No queremos contaminar el límite de uso de componentes solo con el propósito de realizar pruebas unitarias. Este es todo el problema de la solución de prueba basada en DI.

Sugeriría usar el patrón InjectableFactory / InjectableInstance . InjectableInstance son clases de utilidad reutilizables simples que son poseedores de valores mutables . La interfaz del componente contiene una referencia a este titular de valor que se inicializó con la implementación predeterminada. La interfaz proporcionará un método singleton get () que delegará al titular del valor. El cliente componente llamará al método get () en lugar de nuevo para obtener una instancia de implementación para evitar la dependencia directa. Durante el tiempo de prueba, opcionalmente podemos reemplazar la implementación predeterminada con una simulación. A continuación, usaré un ejemplo de TimeProviderclase, que es una abstracción de Date (), por lo que permite que las pruebas unitarias se inyecten y simulen para simular diferentes momentos. Lo siento, usaré Java aquí ya que estoy más familiarizado con Java. C # debería ser similar.

public interface TimeProvider {
  // A mutable value holder with default implementation
  InjectableInstance<TimeProvider> instance = InjectableInstance.of(Impl.class);  
  static TimeProvider get() { return instance.get(); }  // Singleton method.

  class Impl implements TimeProvider {        // Default implementation                                    
    @Override public Date getDate() { return new Date(); }
    @Override public long getTimeMillis() { return System.currentTimeMillis(); }
  }

  class Mock implements TimeProvider {   // Mock implemention
    @Setter @Getter long timeMillis = System.currentTimeMillis();
    @Override public Date getDate() { return new Date(timeMillis); }
    public void add(long offset) { timeMillis += offset; }
  }

  Date getDate();
  long getTimeMillis();
}

// The client of TimeProvider
Order order = new Order().setCreationDate(TimeProvider.get().getDate()));

// In the unit testing
TimeProvider.Mock timeMock = new TimeProvider.Mock();
TimeProvider.instance.setInstance(timeMock);  // Inject mock implementation

InjectableInstance es bastante fácil de implementar, tengo una implementación de referencia en Java. Para más detalles, consulte la publicación de mi blog Indirección de dependencia con fábrica inyectable

Jianwu Chen
fuente
Entonces ... ¿reemplazas la inyección de dependencia con un singleton mutable? Eso suena horrible, ¿cómo ejecutarías pruebas independientes? ¿Abre un nuevo proceso para cada prueba o los fuerza a una secuencia específica? Java no es mi fuerte, ¿entendí mal algo?
nvoigt
En el proyecto Java Maven, la instancia compartida no es un problema, ya que Junit Engine ejecutará cada prueba en un cargador de clases diferente de forma predeterminada, sin embargo, encuentro este problema en algunos IDE, ya que puede reutilizar el mismo cargador de clases. Para resolver este problema, la clase proporciona un método de reinicio que se llamará para restablecer el estado original después de cada prueba. En la práctica, es una situación rara que diferentes pruebas quieran usar diferentes simulacros. Hemos aplicado este patrón para proyectos a gran escala durante años, todo se ejecutó sin problemas, disfrutamos de un código legible y limpio sin sacrificar la portabilidad y la capacidad de prueba.
Jianwu Chen