La mayoría de los proyectos en los que estoy involucrado utilizan varios componentes de código abierto. Como principio general, ¿es una buena idea evitar siempre vincular todos los componentes del código a las bibliotecas de terceros y, en su lugar, utilizar un contenedor de encapsulación para evitar el dolor del cambio?
Como ejemplo, la mayoría de nuestros proyectos PHP usan directamente log4php como marco de registro, es decir, crean instancias a través de \ Logger :: getLogger (), usan los métodos -> info () o -> warn (), etc. En el futuro, sin embargo, puede aparecer un marco de registro hipotético que es mejor de alguna manera. Tal como están las cosas, todos los proyectos que se acoplan estrechamente a las firmas del método log4php tendrían que cambiar, en docenas de lugares, para adaptarse a las nuevas firmas. Obviamente, esto tendría un gran impacto en la base de código y cualquier cambio es un problema potencial.
Para las nuevas bases de código de este tipo de escenario a prueba del futuro, a menudo considero (y a veces implemento) una clase de envoltura para encapsular la funcionalidad de registro y hacer que sea más fácil, aunque no infalible, alterar la forma en que el registro funciona en el futuro con un cambio mínimo ; el código llama al contenedor, el contenedor pasa la llamada al marco de registro del día .
Teniendo en cuenta que hay ejemplos más complicados con otras bibliotecas, ¿estoy sobreingeniería o es una precaución sabia en la mayoría de los casos?
EDITAR: Más consideraciones: el uso de la inyección de dependencia y los dobles de prueba prácticamente requiere que resumamos la mayoría de las API de todos modos ("Quiero comprobar si mi código se ejecuta y actualiza su estado, pero no escribir un comentario de registro / acceder a una base de datos real"). ¿No es esto un decisivo?
fuente
Respuestas:
Si solo usa un pequeño subconjunto de la API de terceros, tiene sentido escribir un contenedor: esto ayuda con la encapsulación y la ocultación de información, asegurando que no exponga una API posiblemente grande a su propio código. También puede ayudar a garantizar que cualquier funcionalidad que no desee utilizar esté "oculta".
Otra buena razón para un contenedor es si espera cambiar la biblioteca de terceros. Si se trata de una infraestructura que sabe que no va a cambiar, no escriba un contenedor para ello.
fuente
Sin saber qué nuevas características supergrandes tendrá este supuesto registrador mejorado futuro, ¿cómo escribiría el contenedor? La opción más lógica es hacer que su contenedor cree una instancia de algún tipo de clase de registrador, y tener métodos como
->info()
o->warn()
. En otras palabras, esencialmente idéntico a su API actual.En lugar de código a prueba de futuro que quizás nunca necesite cambiar, o que requiera una reescritura inevitable de todos modos, prefiero el código "a prueba de pasado". Es decir, en las raras ocasiones en que cambio significativamente un componente, es cuando escribo un contenedor para hacerlo compatible con el código pasado. Sin embargo, cualquier código nuevo usa la nueva API, y refactorizo el código viejo para usarlo siempre que haga un cambio en el mismo archivo de todos modos, o según lo permita la programación. Después de unos meses, puedo quitar el envoltorio y el cambio ha sido gradual y robusto.
Dicho de otra manera, los envoltorios realmente solo tienen sentido cuando ya conoce todas las API que necesita envolver. Un buen ejemplo es si su aplicación actualmente necesita admitir diferentes controladores de bases de datos, sistemas operativos o versiones de PHP.
fuente
Al envolver una biblioteca de terceros, agrega una capa adicional de abstracción sobre ella. Esto tiene algunas ventajas:
Su código base se vuelve más flexible a los cambios.
Si alguna vez necesita reemplazar la biblioteca con otra, solo necesita cambiar su implementación en su contenedor, en un solo lugar . Puede cambiar la implementación del reiniciador y no tiene que cambiar nada sobre otra cosa, en otras palabras, tiene un sistema débilmente acoplado. De lo contrario, tendría que revisar toda su base de código y realizar modificaciones en todas partes, lo que obviamente no es lo que desea.
Puede definir la API del contenedor independientemente de la API de la biblioteca
Las diferentes bibliotecas pueden tener API muy diferentes y, al mismo tiempo, ninguna de ellas puede ser exactamente lo que necesita. ¿Qué sucede si alguna biblioteca necesita un token para pasar junto con cada llamada? Puede pasar el token en su aplicación donde necesite usar la biblioteca o puede protegerlo en algún lugar más centralizado, pero en cualquier caso necesita el token. Su clase de envoltura hace que todo esto sea simple nuevamente, porque puede mantener el token dentro de su clase de envoltura, nunca exponerlo a ningún componente dentro de su aplicación y eliminar por completo la necesidad de ello. Una gran ventaja si alguna vez usó una biblioteca que no enfatiza un buen diseño de API.
Las pruebas unitarias son mucho más simples
Las pruebas unitarias solo deben probar una cosa. Si desea probar la unidad de una clase, debe burlarse de sus dependencias. Esto se vuelve aún más importante si esa clase realiza llamadas de red o accede a algún otro recurso fuera de su software. Al envolver la biblioteca de terceros, es fácil burlarse de esas llamadas y devolver datos de prueba o lo que requiera esa prueba de unidad. Si no tiene esa capa de abstracción, se vuelve mucho más difícil hacer esto, y la mayoría de las veces esto resulta en una gran cantidad de código feo.
Creas un sistema débilmente acoplado
Los cambios en su contenedor no tienen efecto en otras partes de su software, al menos siempre que no cambie el comportamiento de su contenedor. Al introducir una capa de abstracción como este contenedor, puede simplificar las llamadas a la biblioteca y eliminar casi por completo la dependencia de su aplicación en esa biblioteca. Su software solo usará el contenedor y no hará una diferencia en cómo se implementa el contenedor o cómo hace lo que hace.
Ejemplo práctico
Seamos honestos. Las personas pueden discutir sobre las ventajas y desventajas de algo como esto durante horas, por lo que prefiero mostrarles un ejemplo.
Supongamos que tiene algún tipo de aplicación de Android y necesita descargar imágenes. Hay un montón de bibliotecas que facilitan la carga y el almacenamiento en caché de imágenes, por ejemplo, Picasso o Universal Image Loader .
Ahora podemos definir una interfaz que vamos a utilizar para ajustar la biblioteca que terminemos usando:
Esta es la interfaz que ahora podemos usar en toda la aplicación siempre que necesitemos cargar una imagen. Podemos crear una implementación de esta interfaz y usar la inyección de dependencia para inyectar una instancia de esa implementación en todos los lugares donde usamos
ImageService
.Digamos que inicialmente decidimos usar Picasso. Ahora podemos escribir una implementación para la
ImageService
cual utiliza Picasso internamente:Bastante sencillo si me preguntas. El envoltorio alrededor de las bibliotecas no tiene que ser complicado para ser útil. La interfaz y la implementación tienen menos de 25 líneas de código combinadas, por lo que apenas fue un esfuerzo crear esto, pero ya ganamos algo al hacer esto. Ver el
Context
campo en la implementación? El marco de inyección de dependencia de su elección ya se encargará de inyectar esa dependencia antes de que usemos nuestraImageService
, su aplicación ahora no tiene que preocuparse por cómo se descargan las imágenes y las dependencias que pueda tener la biblioteca. Todo lo que ve su aplicación es unaImageService
y, cuando necesita una imagen, llamaload()
con una url, simple y directo.Sin embargo, el beneficio real llega cuando comenzamos a cambiar las cosas. Imagine que ahora necesitamos reemplazar Picasso con Universal Image Loader porque Picasso no es compatible con alguna característica que absolutamente necesitamos en este momento. ¿Ahora tenemos que revisar nuestra base de código y reemplazar tediosamente todas las llamadas a Picasso y luego tratar con docenas de errores de compilación porque olvidamos algunas llamadas de Picasso? No. Todo lo que tenemos que hacer es crear una nueva implementación
ImageService
y decirle a nuestro marco de inyección de dependencias que use esta implementación de ahora en adelante:Como puede ver, la implementación puede ser muy diferente, pero no importa. No tuvimos que cambiar una sola línea de código en ningún otro lugar de nuestra aplicación. Utilizamos una biblioteca completamente diferente que podría tener características completamente diferentes o podría usarse de manera muy diferente, pero a nuestra aplicación simplemente no le importa. Igual que antes, el resto de nuestra aplicación solo ve la
ImageService
interfaz con suload()
método y, sin embargo, este método se implementa ya no importa.Al menos para mí todo esto ya suena bastante bien, ¡pero espera! Aún hay más. Imagine que está escribiendo pruebas unitarias para una clase en la que está trabajando y esta clase usa el
ImageService
. Por supuesto, no puede permitir que sus pruebas unitarias realicen llamadas de red a algún recurso ubicado en algún otro servidor, pero dado que ahora está utilizando elImageService
, puede fácilmenteload()
devolver una estáticaBitmap
utilizada para las pruebas unitarias implementando un simulacroImageService
:Para resumir envolviendo bibliotecas de terceros, su base de código se vuelve más flexible a los cambios, en general más simple, más fácil de probar y reduce el acoplamiento de diferentes componentes en su software, todo lo que se vuelve cada vez más importante cuanto más tiempo mantenga un software.
fuente
Creo que envolver bibliotecas de terceros hoy en caso de que algo mejor venga mañana es una violación muy derrochadora de YAGNI. Si llama repetidamente a código de terceros de una manera peculiar a su aplicación, deberá (debería) refactorizar esas llamadas en una clase de ajuste para eliminar la repetición. De lo contrario, está utilizando completamente la API de la biblioteca y cualquier contenedor se parecería a la biblioteca en sí.
Ahora suponga que aparece una nueva biblioteca con un rendimiento superior o lo que sea. En el primer caso, simplemente reescribe el contenedor para la nueva API. No hay problema.
En el segundo caso, crea un contenedor que adapta la interfaz anterior para controlar la nueva biblioteca. Un poco más de trabajo, pero no hay problema, y no más trabajo del que hubiera hecho si hubiera escrito el contenedor antes.
fuente
La razón básica para escribir un contenedor alrededor de una biblioteca de terceros es para que pueda intercambiar esa biblioteca de terceros sin cambiar el código que la usa. No puede evitar acoplarse a algo, por lo que el argumento dice que es mejor acoplar a una API que ha escrito.
Si vale la pena el esfuerzo es una historia diferente. Ese debate probablemente continuará por mucho tiempo.
Para proyectos pequeños, donde la probabilidad de que tal cambio sea necesario es baja, probablemente sea un esfuerzo innecesario. Para proyectos más grandes, esa flexibilidad puede muy bien superar el esfuerzo adicional para envolver la biblioteca. Sin embargo, es difícil saber si ese es el caso de antemano.
Otra forma de verlo es ese principio básico de abstraer lo que es probable que cambie. Por lo tanto, si la biblioteca de terceros está bien establecida y es poco probable que se modifique, puede estar bien no envolverla. Sin embargo, si la biblioteca de terceros es relativamente nueva, existe una mayor posibilidad de que deba ser reemplazada. Dicho esto, el desarrollo de bibliotecas establecidas ha sido abandonado muchas veces. Entonces, esta no es una pregunta fácil de responder.
fuente
Además de lo que @Oded ya dijo, me gustaría agregar esta respuesta con el propósito especial de iniciar sesión.
Siempre tienen una interfaz para el registro, pero nunca he tenido que sustituir un
log4foo
marco aún.Solo lleva media hora proporcionar la interfaz y escribir el contenedor, por lo que supongo que no perderá demasiado tiempo si resulta innecesario.
Es un caso especial de YAGNI. Aunque no lo necesito, no me lleva mucho tiempo y me siento más seguro. Si realmente llega el día de cambiar el registrador, me alegraré de haber invertido media hora porque me ahorrará más de un día intercambiar llamadas en un proyecto del mundo real. Y nunca he escrito o visto una prueba unitaria para el registro (aparte de las pruebas para la implementación del registrador en sí), así que espere defectos sin el envoltorio.
fuente
Estoy lidiando con este problema exacto en un proyecto en el que estoy trabajando actualmente. Pero en mi caso, la biblioteca es para gráficos y, por lo tanto, puedo restringir su uso a un pequeño número de clases que se ocupan de los gráficos, en lugar de rociarlos en todo el proyecto. Por lo tanto, es bastante fácil cambiar las API más adelante si es necesario; En el caso de un registrador, el asunto se vuelve mucho más complicado.
Por lo tanto, diría que la decisión tiene mucho que ver con lo que está haciendo exactamente la biblioteca de terceros y cuánto dolor se asociaría con cambiarla. Si cambiar todas las llamadas a la API sería fácil independientemente, entonces probablemente no valga la pena hacerlo. Sin embargo, si cambiar la biblioteca más tarde sería realmente difícil, entonces probablemente lo envolvería ahora.
Más allá de eso, otras respuestas han cubierto muy bien la pregunta principal, así que solo quiero centrarme en esa última adición, sobre la inyección de dependencia y los objetos simulados. Por supuesto, depende de cómo funciona exactamente su marco de registro, pero en la mayoría de los casos no, eso no requeriría un contenedor (aunque probablemente se beneficiará de uno). Simplemente haga que la API para su objeto simulado sea exactamente la misma que la biblioteca de terceros y luego puede intercambiar fácilmente el objeto simulado para probarlo.
El factor principal aquí es si la biblioteca de terceros incluso se implementa a través de la inyección de dependencia (o un localizador de servicios o algún patrón de acoplamiento débil). Si se accede a las funciones de la biblioteca a través de un método singleton o estático o algo así, deberá envolverlo en un objeto con el que pueda trabajar en la inyección de dependencia.
fuente
Estoy firmemente en el campo final y no con la posibilidad de sustituir la biblioteca de terceros con la mayor prioridad (aunque eso es una ventaja). Mi razonamiento principal que favorece el envoltorio es simple
Y esto se manifiesta, típicamente, en la forma de un montón de duplicación de código, como los desarrolladores que escriben 8 líneas de código solo para crear
QButton
y diseñarlo de la forma en que debe verse para la aplicación, solo para que el diseñador quiera no solo el aspecto pero también la funcionalidad de los botones para cambiar completamente para todo el software que termina requiriendo volver y reescribir miles de líneas de código, o descubrir que la modernización de una canalización de renderizado requiere una reescritura épica porque la base de código roció ad-hoc de bajo nivel fijo canalice el código de OpenGL por todas partes en lugar de centralizar un diseño de renderizador en tiempo real y dejar el uso de OGL estrictamente para su implementación.Estos diseños no se adaptan a nuestras necesidades de diseño específicas. Tienden a ofrecer un superconjunto masivo de lo que realmente se necesita (y lo que no es parte de un diseño es tan importante, si no más, que lo que es), y sus interfaces no están diseñadas para satisfacer específicamente nuestras necesidades en un "alto nivel" pensamiento = una solicitud "que nos priva de todo control de diseño central si los usamos directamente. Si los desarrolladores terminan escribiendo un código de nivel mucho más bajo que el requerido para expresar lo que necesitan, a veces pueden terminar envolviéndolo ellos mismos de formas ad-hoc que lo hacen para que termines con docenas de escritos apresurados y groseros. envoltorios diseñados y documentados en lugar de uno bien diseñado y bien documentado.
Por supuesto, aplicaría fuertes excepciones a las bibliotecas donde los contenedores son traducciones casi individuales de lo que las API de terceros tienen para ofrecer. En ese caso, es posible que no se busque un diseño de nivel superior que exprese más directamente los requisitos comerciales y de diseño (tal podría ser el caso de algo que se parezca más a una biblioteca de "utilidad"). Pero si hay un diseño mucho más personalizado disponible que exprese nuestras necesidades de manera mucho más directa, entonces estoy firmemente en el campo de la envoltura, al igual que estoy firmemente a favor de usar una función de nivel superior y reutilizarla sobre el código de ensamblaje en línea por todo el lugar.
Curiosamente, he chocado con los desarrolladores en formas en que parecían tan desconfiados y tan pesimistas sobre nuestra capacidad de diseñar, por ejemplo, una función para crear un botón y devolverlo que preferirían escribir 8 líneas de código de nivel inferior centradas en microscópico detalles de la creación de botones (que terminaron necesitando cambios repetidos en el futuro) sobre el diseño y uso de dicha función. Ni siquiera veo el propósito de que intentemos diseñar algo en primer lugar si no podemos confiar en nosotros mismos para diseñar este tipo de envoltorios de manera razonable.
Dicho de otra manera, veo las bibliotecas de terceros como formas de ahorrar potencialmente un tiempo enorme en la implementación, no como sustitutos del diseño de sistemas.
fuente
Mi idea sobre bibliotecas de terceros:
Ha habido alguna discusión recientemente en la comunidad iOS sobre pros y contras (OK, en su mayoría contras) de la utilización de dependencias de terceros. Muchos de los argumentos que vi eran bastante genéricos: agrupaban todas las bibliotecas de terceros en una sola canasta. Sin embargo, como con la mayoría de las cosas, no es tan simple. Entonces, tratemos de enfocarnos en un solo caso
¿Deberíamos evitar el uso de bibliotecas de interfaz de usuario de terceros?
Razones para considerar bibliotecas de terceros:
Parece que hay dos razones principales por las que los desarrolladores consideran usar una biblioteca de terceros:
La mayoría de las bibliotecas de IU (¡ no todas! ) Tienden a caer en la segunda categoría. Esto no es ciencia espacial, pero lleva tiempo construirlo correctamente.
Existen casi dos tipos de controles / vistas:
UICollectionView
deUIKit
.UIPickerView
. Ej . La mayoría de las bibliotecas de terceros tienden a caer en la segunda categoría. Además, a menudo se extraen de una base de código existente para la que fueron optimizados.Suposiciones iniciales desconocidas
Muchos desarrolladores realizan revisiones de código de su código interno, pero pueden estar dando por sentado la calidad del código fuente de terceros. Vale la pena pasar un poco de tiempo simplemente navegando por el código de una biblioteca. Es posible que te sorprendas al ver algunas banderas rojas, por ejemplo, el uso de swizzling donde no es necesario.
No puedes ocultarlo
Debido a la forma en que UIKit está diseñado, lo más probable es que no pueda ocultar la biblioteca de UI de terceros, por ejemplo, detrás de un adaptador. Una biblioteca se entrelazará con su código de IU convirtiéndose en un hecho de su proyecto.
Costo de tiempo futuro
UIKit cambia con cada lanzamiento de iOS. Las cosas se romperán. Su dependencia de terceros no estará tan libre de mantenimiento como podría esperar.
Conclusión
Desde mi experiencia personal, la mayoría de los usos del código de interfaz de usuario de terceros se reducen a intercambiar una menor flexibilidad para ganar algo de tiempo.
Usamos código listo para enviar nuestro lanzamiento actual más rápido. Sin embargo, tarde o temprano, llegamos a los límites de la biblioteca y estamos ante una decisión difícil: ¿qué hacer a continuación?
fuente
Usar la biblioteca directamente es más amigable para el equipo de desarrolladores. Cuando un nuevo desarrollador se une, es posible que tenga plena experiencia con todos los marcos utilizados, pero no podrá contribuir de manera productiva antes de aprender su API local. Cuando un desarrollador más joven intenta progresar en su grupo, se vería obligado a aprender su API específica que no está presente en ningún otro lugar, en lugar de adquirir una competencia genérica más útil. Si alguien conoce características o posibilidades útiles de la API original, es posible que no pueda alcanzar la capa escrita por alguien que no estaba al tanto de ellas. Si alguien obtiene una tarea de programación mientras busca un trabajo, es posible que no pueda demostrar las cosas básicas que usó muchas veces, solo porque todas estas veces estaba accediendo a la funcionalidad necesaria a través de su contenedor.
Creo que estos problemas pueden ser más importantes que la posibilidad remota de usar una biblioteca completamente diferente más adelante. El único caso en el que usaría un contenedor es cuando la migración a otra implementación está definitivamente planificada o la API envuelta no está lo suficientemente congelada y sigue cambiando.
fuente