Patrones fundamentales de diseño de objetos en JavaScript

Diseño efectivo de objetos en cuatro formas

Foto de Dominik Scythe en Unsplash

Como desarrollador de JavaScript, gran parte del código que escribirá tratará con objetos. Creamos objetos para ayudar a organizar el código, reducir la redundancia y razonar sobre los problemas utilizando técnicas orientadas a objetos. Los beneficios del diseño orientado a objetos son evidentes, pero reconocer la utilidad de los objetos es solo el primer paso. Una vez que haya decidido utilizar una estructura orientada a objetos en su código, el siguiente paso es decidir cómo hacerlo. En JavaScript, esto no es tan simple como diseñar una clase e instanciar objetos a partir de ella (ya que JavaScript no tiene clases verdaderas, pero esa es una pregunta para otra publicación de blog). Hay muchos patrones de diseño diferentes para crear objetos similares, y hoy estamos Vamos a explorar algunos de los más comunes. Cada patrón tiene sus propios pros y contras, y esperamos que al final de esta publicación en el blog esté listo para decidir cuál de estas opciones es la adecuada para usted.

Cada desarrollador tiene sus propias preferencias, pero ofrecería los siguientes criterios a tener en cuenta al decidir sobre un patrón de diseño de objeto apropiado para su código.

  1. Legibilidad: como todo buen código, el código orientado a objetos debe ser legible no solo para usted, sino también para otros desarrolladores. Algunos patrones de diseño son más fáciles de interpretar que otros y siempre debe tener en cuenta la legibilidad. Si tiene dificultades para comprender lo que está haciendo su código, entonces otros desarrolladores seguramente no tendrán idea.
  2. Repetición: uno de los principales beneficios del código orientado a objetos es que reduce la redundancia. Si es probable que su código tenga muchos objetos del mismo tipo, entonces un diseño orientado a objetos es casi seguro apropiado. Sin embargo, algunos patrones reducen la redundancia más que otros. Tenga esto en cuenta, al mismo tiempo que considera que una mayor reducción de redundancia puede provocar la pérdida (o al menos una implementación más difícil) de ciertas opciones de personalización.
  3. Estructura jerárquica: como mencionamos anteriormente, JavaScript no tiene clases verdaderas, y es mejor no pensar en sus objetos de esta manera; sin embargo, hay opciones para delegar comportamientos similares a varios conjuntos y subconjuntos de objetos. Esto se realiza mediante la delegación de prototipos, en la que un objeto buscará en toda su cadena de prototipos una propiedad determinada. De esta manera, es posible crear una estructura jerárquica de objetos donde un objeto de un tipo inferior en la estructura puede delegar el comportamiento en su cadena de prototipo (por ejemplo, un objeto Chicken que delega un comportamiento layEgg a un objeto Bird prototipo superior) .) Antes de seleccionar un patrón de diseño, tómese un momento para considerar si espera que sea necesaria una estructura jerárquica y, de ser así, qué comportamientos se deben colocar en qué tipos de objetos.

Y con esas pocas recomendaciones breves completas, veamos nuestra revisión de los patrones de diseño más comunes que es probable que encuentre.

Patrón de creación de objetos de fábrica

El patrón de creación de objetos de fábrica, o simplemente el patrón de fábrica, utiliza las llamadas "funciones de fábrica" ​​para crear objetos de un tipo similar. Cada objeto creado por dicha función tiene las mismas propiedades, para incluir tanto el estado como el comportamiento. Tome por ejemplo lo siguiente:

Aquí, tenemos una función, makeRobot (), que toma dos parámetros (nombre y trabajo) y los usa para asignar el estado a un objeto literal dentro de la función, que luego devuelve. Además, la función define un método, introducir (), en el mismo objeto. En este ejemplo, instanciamos dos objetos de robot, los cuales tienen las mismas propiedades (aunque con valores diferentes). Si quisiéramos, podríamos crear miles de robots más exactamente de la misma manera y predecir de manera confiable cuáles serían sus propiedades cada vez.

Aunque el patrón de fábrica es útil para crear objetos similares, tiene dos inconvenientes principales. Primero, no hay forma de verificar si un determinado objeto fue creado por una determinada fábrica. No podemos, por ejemplo, decir algo como bender instanceof makeRobot para averiguar cómo se creó bender. En segundo lugar, el patrón de fábrica no comparte comportamientos, sino que simplemente crea nuevas versiones de un comportamiento cada vez que se llama y los agrega al objeto que se está creando. Como resultado, los métodos se repiten de nuevo en cada objeto creado por la función de fábrica, ocupando un espacio valioso. En un programa grande, esto podría resultar extremadamente lento y derrochador.

Patrón de constructor

Una forma de abordar algunas de las debilidades del patrón de fábrica es usar el llamado Patrón de constructor. En este patrón, usamos una "función de constructor", que en realidad es solo una función regular que se llama usando la nueva palabra clave. Al usar la nueva palabra clave, le estamos diciendo a JavaScript que ejecute la función de una manera especial, y sucederán cuatro cosas clave:

  1. La función creará inmediatamente un nuevo objeto.
  2. El contexto de ejecución de la función (esto) se establecerá como el nuevo objeto.
  3. El código de función se ejecutará dentro del contexto de ejecución del nuevo objeto.
  4. La función devolverá implícitamente el nuevo objeto, en ausencia de algún otro retorno explícito.

Modifiquemos nuestro ejemplo anterior e intentemos hacer algunos robots usando el patrón constructor.

Este fragmento se parece mucho al anterior, excepto que esta vez usamos la palabra clave this dentro de la función para hacer referencia a un nuevo objeto, establecer algunos estados y propiedades en él, y luego regresar implícitamente cuando la función termina de ejecutarse. En aras de la convención (no una razón sintáctica real), hemos llamado a nuestra función simplemente Robot con una "R" mayúscula. Y, a diferencia del patrón de fábrica, incluso podemos verificar si un objeto dado fue construido por la función Robot dentro de la posición de.

Es posible que sienta la tentación de pensar en esto como si hubiéramos creado una "clase" de Robot, pero es importante recordar que no estamos creando copias de Robot, ya que podríamos estar en un lenguaje de clase real. Más bien, estamos explotando un vínculo que se crea entre el prototipo del objeto recién instanciado y el prototipo de su función constructora correspondiente, lo que facilita la delegación de prototipos. Sin embargo, realmente no hemos aprovechado esa funcionalidad en el fragmento anterior, ya que todavía estamos creando un nuevo método de introducción () en cada nuevo robot. Veamos si podemos arreglar eso.

Patrón pseudoclásico

Hasta ahora no hemos explorado realmente la delegación prototípica aparte de mencionar brevemente que existe. Ahora es el momento de verlo en acción y eliminar algo de redundancia de código al mismo tiempo. Los prototipos de objetos y su comportamiento de delegación son dignos de una publicación de blog completa, pero podemos obtener al menos una imagen básica aquí. En esencia, cuando se invoca una determinada propiedad en un determinado objeto, por ejemplo someRobot.introduce (), primero busca esa propiedad en sí misma. Si no existe tal propiedad, entonces mira las propiedades disponibles para su objeto prototipo, que a su vez mira su objeto prototipo si es necesario, y así hasta el Object.prototype de nivel superior. La cadena de prototipos permite la delegación de comportamiento, en el que no tenemos que definir algún método compartido en objetos de nivel inferior del mismo tipo. En cambio, podemos definir el comportamiento en cualquier prototipo que todos compartan y, por lo tanto, eliminar la redundancia definiendo solo el código una vez. Aquí está en acción con nuestros robots.

Al igual que en el patrón de constructor, estamos usando la nueva palabra clave para crear un nuevo objeto, asignar un estado y luego devolver ese objeto implícitamente. Sin embargo, en este caso no definimos el método de introducción () en cada uno de nuestros robots. Más bien, lo definimos en el objeto Robot.prototype, que como hemos visto, actúa como el prototipo de cada nuevo objeto creado por la función de constructor Robot. Cuando intentamos llamar, por ejemplo, wallE.introduce (), el objeto wallE ve que no tiene dicho método y busca en su cadena de prototipos, encontrando rápidamente un método con ese nombre en Robot.prototype. De hecho, si verificamos el prototipo de wallE usando Object.getPrototypeOf (), podemos ver que de hecho es Robot.prototype.

Este patrón de diseño, conocido como el patrón pseudo-clásico, resuelve los dos problemas que vimos inicialmente en el patrón de fábrica; sin embargo, todavía nos presenta la ilusión algo incómoda de un sistema basado en clases. Esto puede conducir a algunos desvíos desafortunados en nuestro modelo mental de cómo funciona realmente JavaScript, y algunas trampas inesperadas en la ejecución real del programa. Una solución a este problema, popularizada por Kyle Simpson, autor de You Don't Know JS, es el patrón de objeto vinculado a otro objeto (OLOO), que exploraremos a continuación.

Objeto vinculado a otro patrón de objeto

Si el patrón pseudo-clásico es una combinación tentativa del patrón del constructor y la delegación prototípica, entonces OLOO podría considerarse como un abrazo completo del sistema prototipo de JavaScript. En este patrón, no usamos una función para crear objetos. En su lugar, definimos un tipo de objeto plano, que luego utilizamos explícitamente como prototipo para cualquier objeto individual que necesitemos. Podemos ver esto en acción con un último conjunto de robots.

En este fragmento, primero definimos un objeto Robot, que servirá como prototipo para todos los robots futuros. El objeto Robot contiene todos los comportamientos que esperamos de nuestros robots; sin embargo, no establece ningún estado. Más bien, definimos un método init () en Robot, que usaremos para establecer el estado en cualquier futuro robot. Hablando de futuros robots, en lugar de crearlos con una función, lo hacemos utilizando el método Object.create (), que acepta un prototipo como argumento. Al pasar Robot al método Object.create (), nos aseguramos de que el objeto resultante tenga Robot para su prototipo. Luego llamamos al método init () en nuestros robots individuales para establecer el estado necesario. Incluso podemos verificar si un objeto determinado es de cierto tipo utilizando el práctico método Object.getPrototypeOf (), como lo hicimos en fragmentos anteriores.

OLOO nos permite compartir comportamientos similares y verificar el tipo de objetos individuales, todo mientras esquivamos las ilusiones de clase inherentes al constructor y los patrones pseudo-clásicos. Para muchos desarrolladores, este método es preferido porque proporciona un código fácil de entender que también es eficiente y limpio.

Los patrones de creación de objetos que elija finalmente dependen de usted, pero con suerte esta ha sido una buena introducción a algunas de las opciones disponibles. Los objetos en JavaScript son increíblemente poderosos, especialmente cuando se combinan con el uso efectivo de prototipos de objetos, y ni siquiera hemos comenzado a explorar las opciones disponibles al explotar completamente varios pasos en la cadena de prototipos. Eso, sin embargo, es un tema para otro día. Hasta entonces, ¡feliz codificación!