En mi afán de aprender bien JavaScript, y ya de paso un poquito de inglés y un poquito de Ingeniería del Software, me he propuesto traducir una serie de artículos sobre los principios SOLID aplicados a JavaScript. El quinto de ellos es: SOLID JavaScript: The Dependency Inversion Principle.
De la misma serie: SOLID JavaScript
El principio de responsabilidad única
El principio abierto/cerrado
El principio de sustitución de Liskov
El principio de segregación de iterfaz
El principio de inversión de dependencias
El principio de responsabilidad única
El principio abierto/cerrado
El principio de sustitución de Liskov
El principio de segregación de iterfaz
El principio de inversión de dependencias
Esta es la quinta y última de la serie "SOLID JavaScript" que explora los principios de diseño SOLID dentro del contexto del lenguaje JavaScript. En esta última entrega, vamos a examinar el principio de inversión de dependencias.
El principio de inversión de dependencias
El principio de inversión de dependencias se refiere a la estabilidad y la capacidad de reutilización de los componentes de alto nivel dentro de una aplicación. El principio establece:
A. Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones.
B. Las abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones.
La principal preocupación del principio de inversión de dependencias es garantizar que los principales componentes de una aplicación o framework permanecen desacoplados de los componentes auxiliares que suministran los detalles de implementación de bajo nivel. Esto asegura que las partes importantes de una aplicación o framework no se vean afectados cuando los componentes de bajo nivel tengan que cambiar.
La primera parte del principio se refiere al método de acoplamiento entre los módulos de alto nivel y los módulos de bajo nivel. Con arquitectura de capas tradicionales, los módulos de alto nivel (los componentes que encapsulan la lógica de negocio de la aplicación) tienen dependencias en módulos de bajo nivel (componentes que proporcionan temas de infraestructura). Cuando se adhiere al principio de inversión de dependencias, esta relación se invierte. En vez de que sean los módulos de alto nivel los que estén acoplados a los módulos de bajo nivel, deberían ser los módulos de bajo nivel los que se acoplen a las interfaces declaradas por los módulos de alto nivel. Por ejemplo, supongamos una aplicación con temas de persistencia, pues en un diseño tradicional puede pasar que un módulo principal esté basado en el API definido por un módulo de persistencia. Si refactorizamos para que se ajuste al principio de inversión de dependencias, el módulo de persistencia debería modificarse para que se ajustase a una interfaz definida por el módulo principal.
La segunda parte del principio de inversión de dependencias se refiere a la relación adecuada entre abstracciones y detalles. Para entender esta parte del principio, es útil considerar su aplicación en el lenguaje en donde el principio fue concebido: el lenguaje C++.
A diferencia de algunos lenguajes de tipado estático, C++ no proporciona a nivel del lenguaje la construcción de definición de interfaces. Lo que sí que proporciona es la separación de la definición de la clase de su implementación. En C++, las clases se definen mediante un archivo cabecera que enumera los métodos y variables miembros de una clase, junto con un archivo de código fuente que contiene la implementación de cualquier método miembro. Dado que las variables miembro y los métodos privados se declaran en el archivo de cabecera, es posible que las clases destinadas a ser usadas como abstracciones se vuelvan dependientes de los detalles de implementación de la clase. Esto se supera definiendo clases que sólo contienen métodos abstractos (conocido en C++ como clases base abstractas puras) para servir como interfaces para la implementación de clases.
DIP y JavaScript
Como es un lenguaje dinámico, JavaScript no requiere el uso de abstracciones para facilitar el desacoplamiento. Por lo tanto, la estipulación de que las abstracciones no deben depender de los detalles no es particularmente relevante para aplicaciones de JavaScript. Sin embargo, la estipulación de que los módulos de alto nivel no deben depender de módulos de bajo nivel si que lo es.
Al hablar del principio de inversión de dependencias en el contexto de lenguajes de tipado estático, la preocupación de acoplamiento es a la vez semántica y física. Es decir, si un módulo de alto nivel está acoplado a un módulo de bajo nivel, este está acoplado tanto a la interfaz de semántica, así como a la definición física de la interfaz definida en el módulo de bajo nivel. Esto implica que las dependencias del módulo de alto nivel deben invertirse tanto para las dependencias sobre bibliotecas de terceros, como las de módulos nativos de bajo nivel.
Para explicarlo, considera una aplicación .Net que puede encapsular módulos útiles de alto nivel que tienen una dependencia de un módulo de bajo nivel que proporciona temas de persistencia. Aunque es probable que el autor haya expuesto un API similar a la interfaz de persistencia, ya sea respetado el DIP o no, el módulo de alto nivel no podrá ser reutilizado en otra aplicación sin llevar consigo la dependencia del módulo de bajo nivel en donde se define la interfaz de persistencia.
En JavaScript, la aplicación del principio de inversión de dependencias sólo es relevante para el acoplamiento semántico entre módulos de alto nivel y módulos de bajo nivel. Así pues, la adhesión al DIP puede lograrse simplemente expresando la interfaz semántica en términos de necesidades de la aplicación y no mediante el acoplamiento de la interfaz implícita definida por alguna de las implementaciones elegidas para un módulo de bajo nivel .
Para ilustrarlo, consideremos el siguiente ejemplo:
$.fn.trackMap = function(options) {
var defaults = {
/* defaults */
};
options = $.extend({}, defaults, options);
var mapOptions = {
center: new google.maps.LatLng(options.latitude,options.longitude),
zoom: 12,
mapTypeId: google.maps.MapTypeId.ROADMAP
},
map = new google.maps.Map(this[0], mapOptions),
pos = new google.maps.LatLng(options.latitude,options.longitude);
var marker = new google.maps.Marker({
position: pos,
title: options.title,
icon: options.icon
});
marker.setMap(map);
options.feed.update(function(latitude, longitude) {
marker.setMap(null);
var newLatLng = new google.maps.LatLng(latitude, longitude);
marker.position = newLatLng;
marker.setMap(map);
map.setCenter(newLatLng);
});
return this;
};
var updater = (function() {
// private properties
return {
update: function(callback) {
updateMap = callback;
}
};
})();
$("#map_canvas").trackMap({
latitude: 35.044640193770725,
longitude: -89.98193264007568,
icon: 'http://bit.ly/zjnGDe',
title: 'Tracking Number: 12345',
feed: updater
});
En esta fragmento, tenemos una pequeña biblioteca que convierte un determinado div en un mapa mostrando los datos de la ubicación actual, obtenidos estos de un elemento que se está observando. La función trackMap tiene dos dependencias: el API de terceros de Google Maps y un feed de la ubicación. La responsabilidad del objeto feed es simplemente invocar un callback (suministrada durante el proceso de inicialización) con unas nuevas latitud y longitud cuando la ubicación del icono se actualiza. La API de Google Maps se utiliza para hacer el render del mapa de la pantalla.
Mientras que la interfaz del objeto feed puede o no haber sido diseñada en relación a la función trackMap, el hecho de que su funcionalidad sea simple y concisa hace que sea fácil de sustituir por diferentes implementaciones. No es así con la dependencia de Google Maps. Dado que la función trackMap está semánticamente acoplada a la API de Google Maps, cambiar a un proveedor de mapas diferente requeriría que la función trackMap fuese reescrita o que escribiéramos un adaptador que adaptase otro proveedor de mapas a la interfaz específica de Google.
Para invertir el acoplamiento semántico de la biblioteca de Google Maps, se tiene que rediseñar la función trackMap para que tenga un acoplamiento semántico a una interfaz implícita abstracta que represente la funcionalidad necesaria por un proveedor de mapas. Entonces tenemos que implementar un objeto que se adapte a la interfaz del API de Google Maps. A continuación se muestra esta versión alternativa de la función trackMap:
$.fn.trackMap = function(options) {
var defaults = {
/* defaults */
};
options = $.extend({}, defaults, options);
options.provider.showMap(
this[0],
options.latitude,
options.longitude,
options.icon,
options.title);
options.feed.update(function(latitude, longitude) {
options.provider.updateMap(latitude, longitude);
});
return this;
};
$("#map_canvas").trackMap({
latitude: 35.044640193770725,
longitude: -89.98193264007568,
icon: 'http://bit.ly/zjnGDe',
title: 'Tracking Number: 12345',
feed: updater,
provider: trackMap.googleMapsProvider
});
En esta versión, hemos rediseñado la función trackMap para expresar sus necesidades en relación a una interfaz genérica de proveedores de mapas proveedor y hemos trasladado los detalles de implementación hacia un componente googleMapsProvider separado que puede ser incluido como un módulo separado JavaScript. Aquí está nuestra aplicación googleMapsProvider:
trackMap.googleMapsProvider = (function() {
var marker, map;
return {
showMap: function(element, latitude, longitude, icon, title) {
var mapOptions = {
center: new google.maps.LatLng(latitude, longitude),
zoom: 12,
mapTypeId: google.maps.MapTypeId.ROADMAP
},
pos = new google.maps.LatLng(latitude, longitude);
map = new google.maps.Map(element, mapOptions);
marker = new google.maps.Marker({
position: pos,
title: title,
icon: icon
});
marker.setMap(map);
},
updateMap: function(latitude, longitude) {
marker.setMap(null);
var newLatLng = new google.maps.LatLng(latitude,longitude);
marker.position = newLatLng;
marker.setMap(map);
map.setCenter(newLatLng);
}
};
})();
Con estos cambios, nuestra función trackMap es ahora más resistente a los cambios que puedan producirse en la API de Google Maps y es susceptible de ser reutilizado con otro proveedor de mapas. Por supuesto, siempre y cuando la API se pueda adaptar a las necesidades de nuestra aplicación.
¿Adónde la inyección de dependencias?
Aunque no está totalmente relacionado, el concepto de inyección de dependencias se confunde a menudo con el principio de inversión de dependencias debido a la similitud en la terminología. Por esta razón, una discusión de las diferencias entre los dos conceptos puede resultar útil para algunos.
La inyección de dependencias es una forma específica de inversión de control en donde la preocupación de como un componente obtiene sus dependencias está invertida. Cuando se utiliza la inyección de dependencias, las dependencias se suministran al componente en vez de que sea este el que la obtenga mediante la creación de una instancia de dicha dependencia, solicitando la dependencia a través de un Factory, de un Service Locator, o mediante cualquier otro medio de iniciación del componente en sí. Tanto el principio de inversión de dependencias como la inyección de dependencias tienen que ver con las dependencias y ambos utilizan la noción de inversión para contrastar un enfoque alternativo con el presuntamente enfoque estándar. Sin embargo, el principio de inversión de dependencias no se ocupa de cómo los componentes obtienen sus dependencias, sino del desacoplamiento de los componentes de alto nivel con respecto a los componentes de bajo nivel. En cierto sentido, el principio de inversión de dependencias podría decirse que es otra forma de inversión de control donde la preocupación del módulo que define la interfaz está invertida.
Conclusión
Esto nos lleva al final de nuestra serie. En el transcurso de nuestro examen, a la par que hemos visto variaciones en como los principios de diseño SOLID aplican a JavaScript con respecto a otros lenguajes, también hemos visto como cada uno de estos principios han demostrado tener un cierto grado de aplicabilidad en el desarrollo de JavaScript.