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:
public interface ImageService {
Bitmap load(String url);
}
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:
public class PicassoImageService implements ImageService {
private final Context mContext;
public PicassoImageService(Context context) {
mContext = context;
}
@Override
public Bitmap load(String url) {
return Picasso.with(mContext).load(url).get();
}
}
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 nuestra ImageService
, 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 una ImageService
y, cuando necesita una imagen, llama load()
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:
public class UniversalImageLoaderImageService implements ImageService {
private final ImageLoader mImageLoader;
public UniversalImageLoaderImageService(Context context) {
DisplayImageOptions defaultOptions = new DisplayImageOptions.Builder()
.cacheInMemory(true)
.cacheOnDisk(true)
.build();
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
.defaultDisplayImageOptions(defaultOptions)
.build();
mImageLoader = ImageLoader.getInstance();
mImageLoader.init(config);
}
@Override
public Bitmap load(String url) {
return mImageLoader.loadImageSync(url);
}
}
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 su load()
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 el ImageService
, puede fácilmente load()
devolver una estática Bitmap
utilizada para las pruebas unitarias implementando un simulacro ImageService
:
public class MockImageService implements ImageService {
private final Bitmap mMockBitmap;
public MockImageService(Bitmap mockBitmap) {
mMockBitmap = mockBitmap;
}
@Override
public Bitmap load(String url) {
return mMockBitmap;
}
}
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.