28 de febrero de 2013

Principios SOLID con JavaScript: El principio de responsabilidad única (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 al JavaScript. El primero de ellos es: SOLID JavaScript: The Single Responsibility Principle.


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:

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  <title>Single Responsibility Principle Example - 
    Version 1 - jsFiddle demo by derekgreer</title>
  <script type='text/javascript'
      src='http://code.jquery.com/jquery-1.7.1.js'></script>
  <script type='text/javascript'>//<![CDATA[ 
    $(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></li>')
                    .html(product.getDescription())
                    .attr('id-cart', product.getId())
                    .appendTo("#cart");
            }
            products.forEach(function(product) {
                var newItem = $('<li></li>')
                    .html(product.getDescription())
                    .attr('id', product.getId())
                    .dblclick(addToCart)
                    .appendTo("#products");
            });
        })();
    });//]]>  
  </script>
  <style type='text/css'>
    ul {
        border: 1px solid black;
        width: 200px;
        min-height: 100px;
    }
    .cart-wrapper {
        float: left;
        margin-left: 20px;
    }
   .products-wrapper {
        float: left;
    }
  </style>
</head>
<body>
  <div class="products-wrapper">
    <h2>Products</h2>
    <ul id="products"></ul>
  </div>
  <div class="cart-wrapper">
    <h2>Cart</h2>
    <ul id="cart"></ul>
  </div>
</body>
</html>

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:

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  <title>Single Responsibility Principle Example - 
    Version 2 - jsFiddle demo by derekgreer</title>
  <script type='text/javascript' 
      src='http://code.jquery.com/jquery-1.7.1.js'></script>
    <script type='text/javascript'>//<![CDATA[ 
    $(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></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></li>')
                    .html(product.getDescription())
                    .attr('id', product.getId())
                    .dblclick(onProductSelected)
                    .appendTo("#products");
            });
        })();
    });//]]>  
  </script>
  <style type='text/css'>
    ul {
        border: 1px solid black;
        width: 200px;
        min-height: 100px;
    }
    .cart-wrapper {
        float: left;
        margin-left: 20px;
    }
   .products-wrapper {
        float: left;
    }
  </style>
</head>
<body>
  <div class="products-wrapper">
    <h2>Products</h2>
    <ul id="products"></ul>
  </div>
  <div class="cart-wrapper">
    <h2>Cart</h2>
    <ul id="cart"></ul>
  </div>
</body>

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.

5 comentarios:

  1. Gracias por la traducción! Aunque me he acordado de la madre autor, Derek Greek... Me ha llevado una tarde entender el ejemplito del final, con el nuevo diseño.

    ResponderEliminar
    Respuestas
    1. A mí no me eches la culpa, yo simplemente he traducido y he puesto los ejemplos originales del autor. Es verdad que estos ejemplos no son muy didácticos.

      Mi intención es darle una vuelta y hacer mi propia serie de entradas sobre los principios SOLID con JavaScript, con ejemplos un poco más didácticos.

      Eliminar
    2. Que va! mi intención no era echar la culpa :) Aunque ahora que leo mi comentario de nuevo, puede parecerlo, jeje

      Al contrario, se agradece mucho la traducción! Y en el fondo, descifrando este código uno aprende también.

      Sigo con el resto de post relacionados con SOLID. Saludos!

      Eliminar
    3. ¡Ey! ¡Qué yo no me he sentido ofendido! Aunque es verdad también que leyendo mi comentario de nuevo, se podría haber entendido lo contrario. Le han faltado una par de smiles del tipo :)

      Por favor, sigue con el resto de posts relacionados, y cualquier comentario será bienvenido. La verdad es que saber que alguien lo está leyendo, te da fuerzas para seguir. Por cierto a ver si me pongo con los 2 artículos que me faltan por traducir...

      Eliminar
  2. Buenas! me alegro haber encontrado esta serie de artículos, voy a mirármelos.

    Creo que el ejemplo está hecho a mala leche :P, mezcla varios diseños de patrones JS diferentes y conceptos: Revelado, prototipado, autoejecutable...

    Por ejemplo la función Cart tiene un método público con un método público usando this. Supongo que no usa prototype por que solo va a instanciarse una vez, oki, pero mala leche. El prototype se usa para evitar que a cada new Cart se creen de nuevo estos métodos públicos que no cambian para nada de nuevo.

    Después la función "eventAggregator" utiliza el patrón de exposición de métodos, diferente a la de "Cart", a través del return, pero bueno, como también se trata de una función autollamada... se crea una sola vez...

    A lo que me refiero es que habría quedado todo mas claro usando un solo patrón de creación de módulos, por que aquí lo que se intenta explicar es como identificar responsabilidades únicas supongo :)

    Me ha gustado el ejemplo, explica como dividir, e incluso, dentro de cada módulo cada una de las funciones a modo un poco MVC (El Model no se pone del todo claro pero tb se podría hacer). Creo se podría refactorizar un poco incluso creo, pero tiene miga de estudio!

    ResponderEliminar