¿Cómo puedo reducir el esfuerzo manual para envolver bibliotecas de terceros con un modelo de objetos más grande?

16

Al igual que el autor de esta pregunta de 2012 y esta de 2013 , tengo una biblioteca de terceros que necesito envolver para probar correctamente mi aplicación. La respuesta principal dice:

Siempre desea envolver tipos y métodos de terceros detrás de una interfaz. Esto puede ser tedioso y doloroso. A veces puedes escribir un generador de código o usar una herramienta para hacer esto.

En mi caso, la biblioteca es para un modelo de objetos y, en consecuencia, tiene una mayor cantidad de clases y métodos que tendrían que ajustarse para que esta estrategia tenga éxito. Más allá de simplemente "tedioso y doloroso", esto se convierte en una barrera difícil para las pruebas.

En los 4 años transcurridos desde esta pregunta, soy consciente de que los marcos de aislamiento han recorrido un largo camino. Mi pregunta es: ¿existe ahora una forma más simple de lograr el efecto de envoltura completa de bibliotecas de terceros? ¿Cómo puedo eliminar el dolor de este proceso y reducir el esfuerzo manual?


Mi pregunta no es un duplicado de las preguntas a las que inicialmente me vinculé, ya que mi pregunta es sobre la reducción del esfuerzo manual de envolver. Esas otras preguntas solo preguntan si la envoltura tiene sentido, no cómo el esfuerzo puede mantenerse pequeño.

Tom Wright
fuente
¿Qué lenguaje de programación y de qué tipo de biblioteca estás hablando?
Doc Brown
@DocBrown C # y una biblioteca de manipulación de PDF.
Tom Wright
2
Comencé una publicación en meta para encontrar algún soporte para reabrir su pregunta.
Doc Brown
Gracias @DocBrown: espero que haya algunas perspectivas interesantes por ahí.
Tom Wright
1
Cuando no obtengamos mejores respuestas en las próximas 48 horas, otorgaré una recompensa por esto.
Doc Brown

Respuestas:

4

Suponiendo que no está buscando un marco burlón, ya que son ultra ubicuos y fáciles de encontrar , hay algunas cosas que vale la pena señalar por adelantado:

  1. "Nunca" hay algo que "siempre" debas hacer.
    No siempre es mejor concluir una biblioteca de terceros. Si su aplicación depende intrínsecamente de una biblioteca, o si está literalmente construida alrededor de una o dos bibliotecas principales, no pierda el tiempo terminando. Si las bibliotecas cambian, su aplicación deberá cambiar de todos modos.
  2. Está bien usar pruebas de integración.
    Esto es especialmente cierto alrededor de límites que son estables, intrínsecos a su aplicación o que no se pueden burlar fácilmente. Si se cumplen esas condiciones, envolver y burlarse será complicado y tedioso. En ese caso, evitaría ambos: no envuelva y no se burle; solo escribe pruebas de integración. (Si las pruebas automatizadas son un objetivo).
  3. Las herramientas y el marco no pueden eliminar la complejidad lógica.
    En principio, una herramienta solo puede reducir las repeticiones. Pero no existe un algoritmo automatizado para tomar una interfaz compleja y simplificarla, y mucho menos tomar la interfaz X y adaptarla a sus necesidades. (¡Solo usted conoce ese algoritmo!) Por lo tanto, si bien existen indudablemente herramientas que pueden generar envoltorios delgados, sugeriría que aún no son ubicuas porque, al final, todavía necesita codificar de manera inteligente y, por lo tanto, manualmente. contra la interfaz incluso si está oculto detrás de un contenedor.

Dicho esto, hay tácticas que puedes usar en muchos idiomas para evitar referirte directamente a una clase. Y en algunos casos, puede "fingir" una interfaz o envoltura delgada que en realidad no existe. En C #, por ejemplo, iría a una de dos rutas:

  1. Use una fábrica y tipeo implícito .

Puede evitar el esfuerzo de envolver completamente una clase compleja con este pequeño combo:

// "factory"
class PdfDocumentFactory {
  public static ExternalPDFLibraryDocument Build() {
    return new ExternalPDFLibraryDocument();
  }
}

// code that uses the factory.
class CoreBusinessEntity {
  public void DoImportantThings() {
    var doc = PdfDocumentFactory.Build();

    // ... i have no idea what your lib does, so, I'm making stuff but.
    // but, you can do whatever you want here without explicitly
    // referring to the library's actual types.
    doc.addHeader("Wee");
    doc.getAllText().makeBiggerBy(4).makeBold().makeItalic();
    return doc.exportBinaryStreamOrSomething();
  }
}

Si puede evitar almacenar estos objetos como miembros, ya sea a través de un enfoque más "funcional" o almacenándolos en un diccionario (o lo que sea ), este enfoque tiene el beneficio de la verificación de tipos en tiempo de compilación sin que sus entidades comerciales centrales necesiten saber exactamente con qué clase están trabajando.

Todo lo que se requiere es que, en el momento de la compilación, la clase devuelta por su fábrica tenga los métodos que está utilizando su objeto comercial.

  1. Utiliza la escritura dinámica .

Esto está en la misma línea que usar el tipeo implícito , pero implica otra compensación: pierde las comprobaciones de tipo de compilación y obtiene la capacidad de agregar anónimamente dependencias externas como miembros de la clase e inyectar sus dependencias.

class CoreBusinessEntity {
  dynamic Doc;

  public void InjectDoc(dynamic Doc) {
    Doc = doc;
  }

  public void DoImortantThings() {
    Doc.addHeader("Wee");
    Doc.getAllText().makeBiggerBy(4).makeBold().makeItalic();
    return Doc.exportBinaryStreamOrSomething();
  }
}

Con estas dos tácticas, cuando llega el momento de burlarse ExternalPDFLibraryDocument, como dije antes, tienes algo de trabajo que hacer, pero es un trabajo que deberías hacer de todos modos . Y, con esta construcción, ha evitado tediosamente definir cientos de clases de envoltorios pequeños y delgados. Simplemente ha utilizado la biblioteca sin mirarla directamente, en su mayor parte.

Dicho todo esto, hay tres grandes razones por las que aún consideraría concluir explícitamente una biblioteca de terceros, ninguna de las cuales sugiere un uso de una herramienta o marco:

  1. La biblioteca específica no es intrínseca a la aplicación.
  2. Sería muy costoso cambiarlo sin envolverlo.
  3. No me gusta la API en sí.

Si no tengo un nivel de preocupación en las tres áreas, no hagas ningún esfuerzo significativo para terminarlo. Y, si tiene alguna preocupación en las tres áreas, un envoltorio delgado generado automáticamente no será de gran ayuda.

Si ha decidido cerrar una biblioteca, el uso más eficiente y efectivo de su tiempo es construir su aplicación contra la interfaz que desee ; no contra una API existente.

Dicho de otra manera, preste atención al consejo clásico: posponga cada decisión que pueda. Cree primero el "núcleo" de su aplicación. Código contra interfaces que eventualmente harán lo que desea, que eventualmente se cumplirá con "cosas periféricas" que aún no existen. Cerrar las brechas según sea necesario.

Este esfuerzo puede no parecer un ahorro de tiempo; pero si siente que necesita un envoltorio, esta es la forma más eficiente de hacerlo de manera segura.

Piénsalo de esta manera.

Debe codificar contra esta biblioteca en algún rincón oscuro de su código, incluso si está envuelto. Si te burlas de la biblioteca durante las pruebas, hay un esfuerzo manual inevitable allí, incluso si está envuelto. Pero eso no significa que deba reconocer directamente esa biblioteca por su nombre en la mayor parte de su aplicación.

TLDR

Si vale la pena envolver la biblioteca, use tácticas para evitar referencias directas y generalizadas a su biblioteca de terceros, pero no tome atajos para generar envoltorios delgados. Desarrolle su lógica de negocios primero, sea deliberado sobre sus interfaces y haga emerger sus adaptadores orgánicamente, según sea necesario.

Y, si se trata de eso, no tenga miedo de las pruebas de integración. Son un poco más difusos, pero aún ofrecen evidencia de código de trabajo, y todavía se pueden hacer fácilmente para mantener a raya las regresiones.

svidgen
fuente
2
Esta es otra publicación que no responde a la pregunta, que explícitamente no era "cuándo concluir", sino "cómo reducir el esfuerzo manual".
Doc Brown
1
Lo siento, pero creo que te perdiste el punto central de la pregunta. No veo cómo sus sugerencias podrían reducir el esfuerzo manual de envolver. Suponga que la lib en juego tiene un modelo de objeto complejo como API, con algunas docenas de clases y cientos de métodos. Su API está bien, ya que es para uso estándar, pero ¿cómo envolverlo para pruebas unitarias con menos dolor / esfuerzo?
Doc Brown
1
TLDR; si quieres los puntos de bonificación, cuéntanos algo que aún no sabemos ;-)
Doc Brown
1
@DocBrown No perdí el punto. Pero, creo que te perdiste el punto de mi respuesta. Invertir el esfuerzo correcto le ahorra mucho trabajo. Hay implicaciones de prueba, pero eso es solo un efecto secundario. El uso de una herramienta para generar automáticamente un envoltorio delgado alrededor de una biblioteca aún lo deja construyendo su biblioteca central alrededor de la API de otra persona y creando simulacros, lo que es un gran esfuerzo que no se puede evitar si insiste en dejar la biblioteca fuera de tus pruebas.
svidgen
1
@DocBrown Esto es absurdo. "¿Esta clase de problemas tiene una clase de herramientas o enfoques?" Si. Claro que lo hace. Codificación defensiva y no ser dogmático sobre las pruebas unitarias ... como dice mi respuesta. Si lo hizo tener una envoltura delgada generado automágicamente, ¿qué valor tendría que proporcionar !? ... No le permitiría inyectar dependencias para la prueba, aún tiene que hacerlo manualmente. Y no le permitiría intercambiar la biblioteca, porque todavía está codificando contra la API de la biblioteca ... ahora es indirecta.
svidgen
9

No pruebe ese código por unidad. Escriba pruebas de integración en su lugar. En algunos casos, las pruebas unitarias, las burlas, son tediosas y dolorosas. Deshazte de las pruebas unitarias y escribe pruebas de integración que realmente hagan llamadas de proveedores.

Tenga en cuenta que estas pruebas deben ejecutarse después de la implementación como una actividad automatizada posterior a la implementación. No se ejecutan como parte de pruebas unitarias o como parte del proceso de compilación.

A veces, una prueba de integración se ajusta mejor que una prueba unitaria en ciertas partes de su aplicación. Los aros que uno debe seguir para hacer que el código sea "comprobable" a veces puede ser perjudicial.

Jon Raynor
fuente
2
Lo primero que pensé cuando leí tu respuesta fue "poco realista para PDF, ya que comparar los archivos a nivel binario no te dice qué cambió o si el cambio es problemático". Luego encontré esta publicación SO más antigua , indica que en realidad podría funcionar (+1).
Doc Brown
@DocBrown la herramienta en el enlace SO no compara los PDF en un nivel binario. Compara la estructura y el contenido de las páginas. Estructura interna del pdf: safaribooksonline.com/library/view/pdf-explained/9781449321581/…
linuxunil el
1
@linuxunil: sí, lo sé, como escribí, primero pensé ... pero luego encontré la solución mencionada anteriormente.
Doc Brown
oh ... ya veo ... tal vez el "realmente podría funcionar" en el final de su respuesta me confundió.
linuxunil
1

Según tengo entendido, esta discusión se centra en las oportunidades para la automatización de la creación de envoltorios en lugar de envolver la idea y la guía de implementación. Trataré de abstraerme de la idea, ya que hay muchas cosas aquí.

Veo que estamos jugando con las tecnologías .NET, por lo tanto, tenemos poderosas capacidades de reflexión en nuestras manos. Puedes considerar:

  1. Herramienta como .NET Wrapper Class Generator . No he usado esa herramienta y sé que funciona en una pila de tecnología heredada, pero tal vez para su caso encajaría. Por supuesto, la calidad del código, el soporte para la inversión de dependencia y la segregación de interfaces deben investigarse por separado. Tal vez hay otras herramientas como esa, pero no busqué mucho.
  2. Escribir su propia herramienta que buscará en el ensamblado referenciado métodos / interfaces públicos y hace la asignación y la generación de código. ¡La contribución a la comunidad sería más que bienvenida!
  3. Si .NET no es un caso ... quizás mire aquí .

Creo que dicha automatización puede constituir un punto base, pero no una solución final. Se requerirá la refactorización manual del código ... o, en el peor de los casos, un rediseño, ya que estoy completamente de acuerdo con lo que escribió svidgen:

Cree su aplicación contra la interfaz que desee; no contra una API de terceros.

tom3k
fuente
Ok, al menos una idea de cómo podría manejarse el problema . Pero supongo que no se basa en la experiencia propia para este caso de uso.
Doc Brown
¡La pregunta se basa en mi propia experiencia con respecto al dominio de la ingeniería de software (envoltura, reflexión, di)! En cuanto a las herramientas, no usé eso como se indica en la respuesta.
tom3k
0

Siga estas pautas al crear bibliotecas de contenedor:

  • exponga solo un pequeño subconjunto de la biblioteca de terceros que necesita en este momento (y amplíe a demanda)
  • mantenga el contenedor lo más delgado posible (no hay lógica allí).
Rumen Georgiev
fuente
1
Solo me pregunto por qué obtuviste 3 votos a favor para una publicación que no entiende el punto de la pregunta.
Doc Brown