19 de septiembre de 2013

Principios SOLID con JavaScript: El principio de segregación de interfaz (Traducción del Inglés)


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 cuarto de ellos es: SOLID JavaScript: The Interface Segregation Principle.


Esta es la cuarta entrega de la serie "SOLID JavaScript" que explora los principios de diseño SOLID dentro del contexto del lenguaje JavaScript. En esta entrega, vamos a explicar el principio de segregación de la interfaz.

El principio de segregación de la interfaz


El principio de segregación de la interfaz se refiere a la cohesión de las interfaces dentro de un sistema. El principio establece:
Los clientes no deben ser forzados a depender de los métodos que no utilizan.
Cuando los clientes dependen de objetos que contienen métodos utilizados solamente por otros clientes, o se ven forzados a implementar métodos no utilizados con funcionalidad degradada (que puede conducir a una incumplimiento del principio de substitución de Liskov), esto puede producir código frágil. Esto ocurre cuando un objeto se utiliza como implementación de una interfaz poco cohesionada.

El principio de segregación de la interfaz es similar al principio de responsabilidad única puesto que ambos se centran en la cohesión de responsabilidades. De hecho, el ISP (Interface Segregation Principle) puede ser entendido como la aplicación del SRP (Single Responsibility Principle) para la interfaz pública de un objeto.

Interfaces de JavaScript


Así que, ¿qué tiene que ver este principio con JavaScript? Después de todo, JavaScript no tiene interfaces, ¿verdad? Si por interfaces nos referimos a algún tipo abstracto proporcionado por el lenguaje para establecer contratos y permitir desacoplamiento, entonces, tal afirmación sería correcta. Sin embargo, JavaScript tiene otro tipo de interfaz. En el libro Design Patterns: Elements of Reusable Object-Oriented Software por Gamma y otro, nos encontramos con la siguiente definición de una interfaz:
Todas las operaciones declaradas por un objeto se definen por su nombre, los objetos que cogen como parámetros y su valor de retorno. Esto es conocido como firma de la operación. El conjunto de todas las firmas definidas por las operaciones de un objeto se le denomina interfaz del objeto. La interfaz de un objeto engloba el conjunto de solicitudes que se le pueden enviar al objeto.
Independientemente de si un lenguaje proporciona una construcción separada para la representación de las interfaces o no, todos los objetos tienen una interfaz implícita compuesta por el conjunto de propiedades y métodos públicos del objeto. Por ejemplo, considere la siguiente biblioteca:

var exampleBinder = {};
exampleBinder.modelObserver = (function() {
    /* private variables */
    return {
        observe: function(model) {
            /* code */
            return newModel;
        },
        onChange: function(callback) {
            /* code */
        }
    }
})();

exampleBinder.viewAdaptor = (function() {
    /* private variables */
    return {
        bind: function(model) {
            /* code */
        }
    }
})();

exampleBinder.bind = function(model) {
    /* private variables */
    exampleBinder.modelObserver.onChange(/* callback */);
    var om = exampleBinder.modelObserver.observe(model);
    exampleBinder.viewAdaptor.bind(om);
    return om;
};

Esta ejemplo muestra una biblioteca denominada exampleBinder cuyo propósito es facilitar una doble vía de enlace de datos (data-binding). La interfaz pública de la biblioteca está representada por el método bind. Las responsabilidades de notificación de cambio y de interacción con la vista se han separado en los objetos modelObserver y viewAdaptor respectivamente para permitir implementaciones alternativas. Estos objetos representan las implementaciones de las interfaces esperadas por el método bind. Cualquier objeto que se adhiera a la semántica de comportamiento representada por estas interfaces puede ser sustituido por las implementaciones por defecto.

Aunque JavaScript como lenguaje no proporcione interfaces para ayudar en la especificación del contrato de un objeto, la interfaz implícita del objeto todavía sirve como contrato para el código cliente dentro de una aplicación.

ISP y JavaScript


Las siguientes secciones analizan algunas de las consecuencias de incumplir el principio de segregación de la interfaz en JavaScript. Como verás, a la vez que el ISP es aplicable al diseño de aplicaciones JavaScript, las características del lenguaje ofrecen un poco más de resistencia a las interfaces no cohesivas que en los lenguajes de tipado estático.

Degenerado Implementaciones


En los lenguajes de tipado estático, una cuestión que surge al incumplir el ISP es la necesidad de crear implementaciones degradadas. En lenguajes como Java y C#, todos los métodos declarados en una interfaz deben de ser implementados. Para los casos en los que se requiere una interfaz en particular, pero sólo un subconjunto de la conducta es relevante para un escenario de uso dado, los métodos no utilizados generalmente se rellenan con implementaciones vacías o con implementaciones que simplemente lanzan una excepción que indica que el método realmente no está implementado . En JavaScript, los casos en los que se utiliza sólo un subconjunto de la interfaz de un objeto no terminan planteando las mismas cuestiones puesto que un objeto sustituto sólo necesita ofrecer las propiedades esperadas para ajustarse a la parte consumida de la interfaz del objeto. Sin embargo, estas implementaciones pueden todavía incumplir el principio de sustitución de Liskov.

Por ejemplo, considere el siguiente ejemplo:


var rectangle = {
    area: function() { 
        /* code */
    },
    draw: function() { 
        /* code */
    }
};

var geometryApplication = {
    getLargestRectangle: function(rectangles) { 
        /* code */
    }
};

var drawingApplication = {
    drawRectangles: function(rectangles) {
       /* code */
    }
};

Mientras que un rectángulo sustituto podría ser creado con un sólo método area() con el fin de satisfacer las necesidades de método getLargestRectangle de geometryApplication, tal implementación representaría un incumplimiento LSP con respecto al método drawRectangles de drawingApplication.

Acoplamiento estático


Otra cuestión que se plantea en lenguajes de tipado estático es el tema del acoplamiento estático. En lenguajes de tipado estático, las interfaces juegan un papel importante en el diseño de aplicaciones de acoplamiento débil. Tanto en lenguajes estáticos como dinámicos, hay momentos en los que un objeto puede necesitar colaborar con varios clientes (por ejemplo, en los casos en donde puede ser necesario compartir el estado). Para lenguajes de tipado estático, se considera una buena práctica que los clientes interactúen con objetos que utilizan Rol Interfaces. Esto permite a los clientes interactuar con un objeto que puede requerir la intersección de múltiples funciones como detalle de implementación sin acoplar al cliente a un comportamiento no utilizado. En JavaScript, esto no es un problema ya que los objetos están desacoplados en virtud de las capacidades dinámicas del lenguaje.

Acoplamiento semántico


Una consecuencia que es igualmente relevante en JavaScript y en lenguajes de tipado estático es el tema del acoplamiento semántico. El acoplamiento semántico es la interdependencia que se produce cuando una parte del comportamiento de un objeto está acoplado con otra. Cuando la interfaz de un objeto especifica responsabilidades no cohesivos, los cambios para adaptarse a las cambiantes necesidades de un cliente pueden afectar inadvertidamente a otro cliente como resultado de los cambios realizados en el estado o comportamiento compartido. Esta es también una de las posibles consecuencias de incumplir el principio de responsabilidad única, aunque esto suele ser visto desde la perspectiva del impacto que tiene el comportamiento no cohesivo sobre objetos que colaboran entre sí. Desde la perspectiva del ISP, esta consecuencia se extiende a través de la herencia o de la sustitución del objeto.

Extensibilidad


Otro papel importante que las interfaces desempeñan en aplicaciones JavaScript se encuentra en la zona de extensibilidad. El ejemplo más común de esto se puede ver en el uso de callbacks donde una función conforme a la interfaz esperada se pasa a una biblioteca que se invoca en algún momento en el futuro (por ejemplo, en una petición exitosa de AJAX). El principio de segregación de la interfaz se vuelve relevante cuando dichas interfaces requieren la implementación de un objeto con múltiples propiedades y/o métodos. Cuando una interfaz comienza a requerir la implementación de un número significativo de métodos, esto hace el trabajo con la biblioteca utilizada más difícil y es probablemente una indicación de que las interfaces representan responsabilidades no cohesivas. Esto es a menudo descrito como interfaces "fat".

En resumen, las capacidades dinámicas de JavaScript permiten que la aparición de interfaces no cohesivas tengan menos consecuencias que en lenguajes de tipado estático, pero no obstante el principio de segregación de la interfaz tiene su lugar en el diseño de aplicaciones de JavaScript.

La próxima vez, vamos a discutir el siguiente principio en el acrónimo SOLID: el principo de inversión de dependencias.

0 comentarios:

Publicar un comentario