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 al JavaScript. El primero de ellos es: SOLID JavaScript: The Single Responsibility Principle.

De la misma serie: SOLID JavaScript

 

Esta es la primera entrega de la serie “SOLID JavaScript” que explorará los principios de diseño SOLID dentro del contexto del lenguaje JavaScript. En esta primera entrega, vamos a echar un vistazo a los principios de diseño SOLID y discutiremos el primero de ellos: el principio de responsabilidad única.

Los principios de diseño SOLID y JavaScript

SOLID es un acrónimo nemotécnico que se refiere a un conjunto de principios de diseño que se desarrolló entorno a la comunidad de programadores de lenguajes orientados a objetos y que fueron popularizados por los escritos de Robert C. Martin. Estos principios son los siguientes:

  • El principio de responsabilidad única
  • El principio abierto/cerrado
  • El principio de sustitución Liskov
  • El principio de segregación de iterfaz
  • El principio de inversión de dependencias

Estos principios se discuten a menudo en el contexto de lenguajes clásicos, de tipado estático, y orientados a objetos, y aunque JavaScript es un lenguaje basado en prototipos y de tipado dinámico, tiene mezcla de ambos paradigmas, el orientado a objetos y el funcional, por lo que los programadores aún puede cosechar los beneficios de la aplicación de estos principios a JavaScript. Este artículo cubre el primero de estos principios: el principio de responsabilidad única.

El principio de responsabilidad única

El principio de responsabilidad única se refiere a la relación funcional de los elementos de un módulo. El principio declara:

Una clase debe tener sólo una razón para cambiar

Esta descripción puede ser un poco engañosa, ya que parece indicar que un objeto sólo debe hacer una cosa. Sin embargo, lo que quiere decir esta afirmación es que un objeto debe tener un conjunto coherente de comportamientos entorno a una única responsabilidad de tal forma que si esta responsabilidad cambia implica cambiar la definición del objeto. De una forma más simple, la definición de un objeto sólo debería tener que ser modificada debido a cambios en una única responsabilidad dentro del sistema.

La adhesión al principio de responsabilidad única ayuda a mejorar la mantenibilidad limitando las responsabilidades de un objeto a únicamente aquellas que cambien por razones relacionadas. Cuando un objeto encapsula múltiples responsabilidades, los cambios en una de las responsabilidades del objeto puede afectar negativamente a las otras. Al desacoplar esas responsabilidades, podemos crear código que es más resistente al cambio.

Pero, ¿cómo identificar si un determinado conjunto de comportamientos constituye una responsabilidad única? ¿Agrupar la manipulación de strings en un único objeto es una responsabilidad única? ¿Y agrupar todas las llamadas a servicios de una aplicación? Sin un enfoque establecido para hacer estas determinaciones, la adhesión al principio de responsabilidad única puede ser un poco desconcertante.

Los roles estereotipados de objetos

Un enfoque que puede ayudar en la organización de las funciones dentro de un sistema es el uso de los roles estereotipados de objetos. Los roles estereotipados de objetos son un conjunto de roles generales y preestablecidos que comúnmente aparecen en arquitecturas orientadas a objetos. Mediante el uso de un conjunto de roles estereotipados, los desarrolladores pueden dotarse ellos mismos de un conjunto de plantillas que pueden utilizar a medida que avanzan a través del ejercicio mental de descomponer las funciones en componentes coherentes.

El concepto de los roles estereotipados de objetos se trata en el libro Object Design: Roles, Responsibilies, and Collaborations de Rebecca Wirfs-Brock y McKean Alan. El libro presenta los siguientes roles estereotipados:

  • Propietario de la información – un objeto diseñado para conocer determinada información y proporcionar esa información a los otros objetos.
  • Estructurador – un objeto que mantiene las relaciones entre los objetos y la información sobre esas relaciones.
  • Proveedor de servicios – un objeto que realiza un trabajo específico y ofrece servicios a otros bajo demanda.
  • Controlador – un objeto diseñado para tomar decisiones y controlar una tarea compleja.
  • Coordinador – un objeto que no toma muchas decisiones, pero, de un modo memorístico o mecánico, delega el trabajo en otros objetos.
  • Interfaz – un objeto que transforma la información o peticiones entre distintas partes de un sistema.

Aunque no es preceptivo, este conjunto de estereotipos proporciona un excelente marco mental para ayudar en el proceso de diseño de software. Una vez establecidos un conjunto de estereotipos para trabajar con ellos, veremos que es más fácil agrupar comportamientos en grupos de responsabilidades cohesionados relacionadas con el papel previsto para un objeto.

Ejemplo: El principio de responsabilidad única

Para ilustrar la aplicación del principio de responsabilidad única, consideremos el siguiente ejemplo donde se facilita el movimiento de unidades de producto hasta un carrito de la compra:


    $(window).load(function(){
    
        function Product(id, description) {
            this.getId = function() {
                return id;
            };
            this.getDescription = function() {
                return description;
            };
        }
    
        function Cart(eventAggregator) {
            var items = [];

            this.addItem = function(item) {
                items.push(item);
            };
        }
    
        var products = [
            new Product(1, "Star Wars Lego Ship"),
            new Product(2, "Barbie Doll"),
            new Product(3, "Remote Control Airplane")];
    
        var cart = new Cart();
    
        (function() {
            function addToCart() {
                var productId = $(this).attr('id');
                var product = $.grep(products, function(x) {
                    return x.getId() == productId;
                })[0];
                cart.addItem(product);

                var newItem = $('li')
                    .html(product.getDescription())
                    .attr('id-cart', product.getId())
                    .appendTo("#cart");
            }
            products.forEach(function(product) {
                var newItem = $('li')
                    .html(product.getDescription())
                    .attr('id', product.getId())
                    .dblclick(addToCart)
                    .appendTo("#products");
            });
        })();
    });

Aunque no es excesivamente complejo, este ejemplo ilustra una serie de responsabilidades no relacionadas que se agrupan dentro de una función anónima única. Vamos a considerar cada responsabilidad:

En primer lugar, tenemos un comportamiento definido para añadir un producto al objeto Cart cuando se hace doble clic en un elemento.

En segundo lugar, tenemos un comportamiento definido para añadir un producto a la vista del carrito cuando se hace doble clic en un elemento.

Tercero, tenemos un comportamiento definido para rellenar la vista de los productos con el conjunto inicial de productos.

Vamos a romper estas tres responsabilidades hacia sus propios objetos:


    $(window).load(function() {
        function Event(name) {
            this._handlers = [];
            this.name = name;
        }
        Event.prototype.addHandler = function(handler) {
            this._handlers.push(handler);
        };
        Event.prototype.removeHandler = function(handler) {
            for (var i = 0; i < handlers.length; i++) {
                if (this._handlers[i] == handler) {
                    this._handlers.splice(i, 1);
                    break;
                }
            }
        };
        Event.prototype.fire = function(eventArgs) {
            this._handlers.forEach(function(h) {
                h(eventArgs);
            });
        };

        var eventAggregator = (function() {
            var events = [];

            function getEvent(eventName) {
                return $.grep(events, function(event) {
                    return event.name === eventName;
                })[0];
            }

            return {
                publish: function(eventName, eventArgs) {
                    var event = getEvent(eventName);

                    if (!event) {
                        event = new Event(eventName);
                        events.push(event);
                    }
                    event.fire(eventArgs);
                },

                subscribe: function(eventName, handler) {
                    var event = getEvent(eventName);

                    if (!event) {
                        event = new Event(eventName);
                        events.push(event);
                    }

                    event.addHandler(handler);
                }
            };
        })();

        function Cart() {
            var items = [];

            this.addItem = function(item) {
                items.push(item);
                eventAggregator.publish("itemAdded", item);
            };
        }

        var cartView = (function() {
            eventAggregator.subscribe("itemAdded", function(eventArgs) {
                var newItem = $('li')
                    .html(eventArgs.getDescription())
                    .attr('id-cart', eventArgs.getId())
                    .appendTo("#cart");
            });
        })();

        var cartController = (function(cart) {
            eventAggregator.subscribe(
                "productSelected", 
                function(eventArgs) {
                    cart.addItem(eventArgs.product);
                });
        })(new Cart());

        function Product(id, description) {
            this.getId = function() {
                return id;
            };
            this.getDescription = function() {
                return description;
            };
        }

        var products = [
            new Product(1, "Star Wars Lego Ship"),
            new Product(2, "Barbie Doll"),
            new Product(3, "Remote Control Airplane")];

        var productView = (function() {
            function onProductSelected() {
                var productId = $(this).attr('id');
                var product = $.grep(products, function(x) {
                    return x.getId() == productId;
                })[0];
                eventAggregator.publish("productSelected", {
                    product: product
                });
            }

            products.forEach(function(product) {
                var newItem = $('li')
                    .html(product.getDescription())
                    .attr('id', product.getId())
                    .dblclick(onProductSelected)
                    .appendTo("#products");
            });
        })();
    });

En nuestro diseño revisado, hemos eliminado la función anónima y la hemos sustituido por objetos que coordinan cada uno de los conjuntos separados de responsabilidades. Se ha introducido el objeto cartView para coordinar los productos que se muestran en el carrito, se ha introducido el objeto cartController para coordinar los productos que se añaden al objeto Cart, y se ha introducido el objeto ProductView para coordinar los productos que se muestran en pantalla. También se ha introducido un agregador de eventos para facilitar la comunicación entre objetos mediante acoplamiento flexible. Aunque este diseño da como resultado un mayor número de objetos, cada objeto se centra ahora en el cumplimiento de una función específica dentro de la orquestación general con acoplamiento mínimo entre los objetos.

La próxima vez, vamos a discutir el siguiente principio en el acrónimo SOLID: el principio abierto / cerrado.

Anuncios