¿Hay algún principio OO que sea prácticamente aplicable para Javascript?

79

Javascript es un lenguaje orientado a objetos basado en prototipos, pero puede convertirse en una clase de varias maneras, ya sea por:

  • Escribir las funciones para que las use usted mismo como clases
  • Utilice un sistema de clase ingenioso en un marco (como mootools Class.Class )
  • Generarlo desde Coffeescript

Al principio solía escribir código basado en clases en Javascript y confiaba mucho en él. Sin embargo, recientemente he estado usando frameworks Javascript y NodeJS , que se alejan de esta noción de clases y se basan más en la naturaleza dinámica del código, como:

  • Programación asíncrona, usando y escribiendo código de escritura que usa devoluciones de llamada / eventos
  • Carga de módulos con RequireJS (para que no se filtren al espacio de nombres global)
  • Conceptos de programación funcional como la comprensión de listas (mapa, filtro, etc.)
  • Entre otras cosas

Lo que he reunido hasta ahora es que la mayoría de los principios y patrones de OO que he leído (como los patrones SOLID y GoF) fueron escritos para lenguajes de OO basados ​​en clases en mente como Smalltalk y C ++. Pero, ¿hay alguno de ellos aplicable para un lenguaje basado en prototipos como Javascript?

¿Hay algún principio o patrón que sea específico de Javascript? Principios para evitar el infierno de devolución de llamada , la evaluación malvada o cualquier otro antipatrón, etc.

Spoike
fuente

Respuestas:

116

Después de muchas ediciones, esta respuesta se ha convertido en un monstruo de longitud. Me disculpo de antemano.

En primer lugar, eval()no siempre es malo y, por ejemplo, puede aportar beneficios en el rendimiento cuando se usa en la evaluación diferida. La evaluación diferida es similar a la carga diferida, pero esencialmente almacena el código dentro de cadenas y luego usa evalo new Functionpara evaluar el código. Si usa algunos trucos, será mucho más útil que el mal, pero si no lo hace, puede llevar a cosas malas. Puede ver mi sistema de módulos que usa este patrón: https://github.com/TheHydroImpulse/resolve.js . Resolve.js usa eval en lugar de new Functionprincipalmente para modelar CommonJS exportsy las modulevariables disponibles en cada módulo, y new Functionenvuelve su código dentro de una función anónima, sin embargo, termino envolviendo cada módulo en una función, lo hago manualmente en combinación con eval.

Usted lee más sobre esto en los siguientes dos artículos, el último también se refiere al primero.

Generadores de armonía

Ahora que los generadores finalmente han aterrizado en V8 y, por lo tanto, en Node.js, debajo de una bandera ( --harmonyo --harmony-generators). Estos reducen en gran medida la cantidad de llamadas que tienes. Hace que escribir código asincrónico sea realmente genial.

La mejor manera de utilizar generadores es emplear algún tipo de biblioteca de flujo de control. Esto permitirá que el flujo continúe a medida que cedes en los generadores.

Resumen / Resumen:

Si no está familiarizado con los generadores, son una práctica de pausar la ejecución de funciones especiales (llamadas generadores). Esta práctica se llama ceder el uso de la yieldpalabra clave.

Ejemplo:

function* someGenerator() {
  yield []; // Pause the function and pass an empty array.
}

Por lo tanto, cada vez que llame a esta función la primera vez, devolverá una nueva instancia de generador. Esto le permite llamar next()a ese objeto para iniciar o reanudar el generador.

var gen = someGenerator();
gen.next(); // { value: Array[0], done: false }

Seguiría llamando nexthasta que doneregrese true. Esto significa que el generador ha finalizado completamente su ejecución, y no hay más yielddeclaraciones.

Flujo de control:

Como puede ver, los generadores de control no son automáticos. Necesita continuar manualmente cada uno. Es por eso que se utilizan bibliotecas de flujo de control como co .

Ejemplo:

var co = require('co');

co(function*() {
  yield query();
  yield query2();
  yield query3();
  render();
});

Esto permite la posibilidad de escribir todo en Node (y el navegador con Regenerator de Facebook que toma, como entrada, el código fuente que utiliza generadores de armonía y divide el código ES5 totalmente compatible) con un estilo sincrónico.

Los generadores siguen siendo bastante nuevos y, por lo tanto, requiere Node.js> = v11.2. Mientras escribo esto, v0.11.x todavía es inestable y, por lo tanto, muchos módulos nativos están rotos y serán hasta v0.12, donde la API nativa se calmará.


Para agregar a mi respuesta original:

Recientemente he estado prefiriendo una API más funcional en JavaScript. La convención usa OOP detrás de escena cuando es necesario, pero simplifica todo.

Tomemos por ejemplo un sistema de vista (cliente o servidor).

view('home.welcome');

Es mucho más fácil de leer o seguir que:

var views = {};
views['home.welcome'] = new View('home.welcome');

La viewfunción simplemente verifica si la misma vista ya existe en un mapa local. Si la vista no existe, creará una nueva vista y agregará una nueva entrada al mapa.

function view(name) {
  if (!name) // Throw an error

  if (view.views[name]) return view.views[name];

  return view.views[name] = new View({
    name: name
  });
}

// Local Map
view.views = {};

Extremadamente básico, ¿verdad? Creo que simplifica drásticamente la interfaz pública y hace que sea más fácil de usar. También empleo la capacidad de la cadena ...

view('home.welcome')
   .child('menus')
   .child('auth')

Tower, un marco que estoy desarrollando (con alguien más) o desarrollando la próxima versión (0.5.0) utilizará este enfoque funcional en la mayoría de sus interfaces de exposición.

Algunas personas aprovechan las fibras como una forma de evitar el "infierno de devolución de llamada". Es un enfoque bastante diferente a JavaScript, y no soy un gran admirador de él, pero muchos frameworks / plataformas lo usan; incluyendo Meteor, ya que tratan a Node.js como una plataforma de conexión / subproceso.

Prefiero usar un método abstracto para evitar el infierno de devolución de llamada. Puede volverse engorroso, pero simplifica enormemente el código de aplicación real. Al ayudar a construir el marco de TowerJS , resolvió muchos de nuestros problemas, sin embargo, obviamente todavía tendrá cierto nivel de devoluciones de llamada, pero el anidamiento no es profundo.

// app/config/server/routes.js
App.Router = Tower.Router.extend({
  root: Tower.Route.extend({
    route: '/',
    enter: function(context, next) {
      context.postsController.page(1).all(function(error, posts) {
        context.bootstrapData = {posts: posts};
        next();
      });
    },
    action: function(context, next) {
      context.response.render('index', context);
      next();
    },
    postRoutes: App.PostRoutes
  })
});

Un ejemplo de nuestro, actualmente en desarrollo, sistema de enrutamiento y "controladores", aunque bastante diferente de los "rieles" tradicionales. Pero el ejemplo es extremadamente poderoso y minimiza la cantidad de devoluciones de llamada y hace que las cosas sean bastante aparentes.

El problema con este enfoque es que todo está abstraído. Nada se ejecuta tal cual y requiere un "marco" detrás de él. Pero si este tipo de características y estilo de codificación se implementa dentro de un marco, entonces es una gran victoria.

Para patrones en JavaScript, honestamente depende. La herencia solo es realmente útil cuando se utiliza CoffeeScript, Ember o cualquier infraestructura / marco de "clase". Cuando estás dentro de un entorno de JavaScript "puro", usar la interfaz de prototipo tradicional funciona de maravilla:

function Controller() {
    this.resource = get('resource');
}

Controller.prototype.index = function(req, res, next) {
    next();
};

Ember.js comenzó, al menos para mí, usando un enfoque diferente para construir objetos. En lugar de construir cada prototipo de forma independiente, usaría una interfaz similar a un módulo.

Ember.Controller.extend({
   index: function() {
      this.hello = 123;
   },
   constructor: function() {
      console.log(123);
   }
});

Todos estos son diferentes estilos de "codificación", pero se agregan a su base de código.

Polimorfismo

El polimorfismo no se usa ampliamente en JavaScript puro, donde trabajar con herencia y copiar el modelo tipo "clase" requiere mucho código repetitivo.

Diseño basado en eventos / componentes

Los modelos basados ​​en eventos y basados ​​en componentes son los IMO ganadores, o son los más fáciles de trabajar, especialmente cuando se trabaja con Node.js, que tiene un componente EventEmitter incorporado, aunque implementar estos emisores es trivial, es solo una buena adición .

event.on("update", function(){
    this.component.ship.velocity = 0;
    event.emit("change.ship.velocity");
});

Solo un ejemplo, pero es un buen modelo para trabajar. Especialmente en un proyecto orientado a juegos / componentes.

El diseño de componentes es un concepto separado en sí mismo, pero creo que funciona extremadamente bien en combinación con los sistemas de eventos. Los juegos son tradicionalmente conocidos por el diseño basado en componentes, donde la programación orientada a objetos lo lleva solo hasta cierto punto.

El diseño basado en componentes tiene sus usos. Depende de qué tipo de sistema tenga su edificio. Estoy seguro de que funcionaría con aplicaciones web, pero funcionaría extremadamente bien en un entorno de juego, debido a la cantidad de objetos y sistemas separados, pero seguramente existen otros ejemplos.

Pub / Sub Pattern

El enlace de eventos y pub / sub es similar. El patrón pub / sub realmente brilla en las aplicaciones Node.js debido al lenguaje unificador, pero puede funcionar en cualquier idioma. Funciona extremadamente bien en aplicaciones en tiempo real, juegos, etc.

model.subscribe("message", function(event){
    console.log(event.params.message);
});

model.publish("message", {message: "Hello, World"});

Observador

Esto podría ser subjetivo, ya que algunas personas optan por pensar en el patrón Observador como pub / sub, pero tienen sus diferencias.

"El observador es un patrón de diseño en el que un objeto (conocido como sujeto) mantiene una lista de objetos dependiendo de él (observadores), notificándoles automáticamente cualquier cambio de estado". - El patrón de observador

El patrón de observación es un paso más allá de los típicos sistemas pub / subs. Los objetos tienen relaciones estrictas o métodos de comunicación entre ellos. Un objeto "Sujeto" mantendría una lista de dependientes "Observadores". El tema mantendría a sus observadores actualizados.

Programación Reactiva

La programación reactiva es un concepto más pequeño y desconocido, especialmente en JavaScript. Hay un marco / biblioteca (que yo sepa) que expone una API fácil de trabajar para usar esta "programación reactiva".

Recursos sobre programación reactiva:

Básicamente, tiene un conjunto de datos de sincronización (ya sean variables, funciones, etc.).

 var a = 1;
 var b = 2;
 var c = a + b;

 a = 2;

 console.log(c); // should output 4

Creo que la programación reactiva está considerablemente oculta, especialmente en lenguajes imperativos. Es un paradigma de programación increíblemente poderoso, especialmente en Node.js. Meteor ha creado su propio motor reactivo en el que se basa básicamente el marco. ¿Cómo funciona la reactividad de Meteor detrás de escena? es una excelente descripción general de cómo funciona internamente.

Meteor.autosubscribe(function() {
   console.log("Hello " + Session.get("name"));
});

Esto se ejecutará normalmente, mostrando el valor de name, pero si lo cambiamos

Session.set ('nombre', 'Bob');

Volverá a mostrar la visualización de console.log Hello Bob. Un ejemplo básico, pero puede aplicar esta técnica a modelos de datos y transacciones en tiempo real. Puede crear sistemas extremadamente potentes detrás de este protocolo.

Meteorito ...

El patrón reactivo y el patrón de observador son bastante similares. La principal diferencia es que el patrón de observador describe comúnmente el flujo de datos con objetos / clases completos frente a la programación reactiva que describe el flujo de datos a propiedades específicas.

Meteor es un gran ejemplo de programación reactiva. Su tiempo de ejecución es un poco complicado debido a la falta de eventos de cambio de valor nativo de JavaScript (los proxys de Harmony cambian eso). Otros frameworks del lado del cliente, Ember.js y AngularJS también utilizan programación reactiva (hasta cierto punto).

Los dos marcos posteriores usan el patrón reactivo más notablemente en sus plantillas (es decir, la actualización automática). Angular.js utiliza una técnica simple de verificación sucia. No llamaría a esto exactamente programación reactiva, pero está cerca, ya que la verificación sucia no es en tiempo real. Ember.js usa un enfoque diferente. Uso de ascuas set()y get()métodos que les permiten actualizar inmediatamente los valores dependientes. Con su runloop es extremadamente eficiente y permite valores más dependientes, donde angular tiene un límite teórico.

Promesas

No es una solución para las devoluciones de llamada, pero elimina algunas sangrías y mantiene las funciones anidadas al mínimo. También agrega una buena sintaxis al problema.

fs.open("fs-promise.js", process.O_RDONLY).then(function(fd){
  return fs.read(fd, 4096);
}).then(function(args){
  util.puts(args[0]); // print the contents of the file
});

También podría difundir las funciones de devolución de llamada para que no estén en línea, pero esa es otra decisión de diseño.

Otro enfoque sería combinar eventos y promesas donde tendría una función para distribuir eventos de manera apropiada, luego las funciones funcionales reales (las que tienen la lógica real dentro de ellas) se unirían a un evento particular. Luego pasaría el método de despachador dentro de cada posición de devolución de llamada, sin embargo, tendría que resolver algunos problemas que se le ocurrirían, como parámetros, saber a qué función enviar, etc.

Función única función

En lugar de tener un gran lío de devolución de llamadas, mantenga una sola función en una sola tarea y haga bien esa tarea. A veces puede adelantarse y agregar más funcionalidades dentro de cada función, pero pregúntese: ¿Puede esto convertirse en una función independiente? Nombra la función, y esto limpia tu sangría y, como resultado, limpia el problema del infierno de devolución de llamada.

Al final, sugeriría desarrollar o usar un pequeño "marco", básicamente solo una columna vertebral para su aplicación, y tomarse el tiempo para hacer abstracciones, decidir sobre un sistema basado en eventos o un "montón de pequeños módulos que son sistema independiente ". He trabajado con varios proyectos de Node.js donde el código era extremadamente complicado con el infierno de devolución de llamadas en particular, pero también una falta de pensamiento antes de que comenzaran a codificar. Tómese su tiempo para pensar en las diferentes posibilidades en términos de API y sintaxis.

Ben Nadel ha hecho algunas publicaciones de blog realmente buenas sobre JavaScript y algunos patrones bastante estrictos y avanzados que pueden funcionar en su situación. Algunas buenas publicaciones que destacaré:

Inversión de control

Aunque no está exactamente relacionado con el infierno de devolución de llamada, puede ayudarlo con la arquitectura general, especialmente en las pruebas unitarias.

Las dos subversiones principales de la inversión de control son Inyección de dependencias y Localizador de servicios. Considero que el Localizador de servicios es el más fácil dentro de JavaScript, a diferencia de la Inyección de dependencias. ¿Por qué? Principalmente porque JavaScript es un lenguaje dinámico y no existe escritura estática. Java y C #, entre otros, son "conocidos" por la inyección de dependencias porque puede detectar tipos, y tienen interfaces, clases, etc. incorporados. Esto hace las cosas bastante fáciles. Sin embargo, puede volver a crear esta funcionalidad dentro de JavaScript, aunque no será idéntica y un poco confusa, prefiero usar un localizador de servicios dentro de mis sistemas.

Cualquier tipo de inversión de control desacoplará dramáticamente su código en módulos separados que se pueden burlar o falsificar en cualquier momento. ¿Diseñó una segunda versión de su motor de renderizado? Impresionante, simplemente sustituya la interfaz anterior por la nueva. Sin embargo, los localizadores de servicios son especialmente interesantes con los nuevos Proxy Harmony, que solo se pueden usar de manera efectiva en Node.js, proporcionan una API más agradable, en lugar de usar Service.get('render');y en su lugar Service.render. Actualmente estoy trabajando en ese tipo de sistema: https://github.com/TheHydroImpulse/Ettore .

Aunque la falta de tipeo estático (el tipeo estático es una posible razón para los usos efectivos en la inyección de dependencia en Java, C #, PHP: no es tipeado estático, pero tiene sugerencias de tipo) podría considerarse como un punto negativo, puede Definitivamente convertirlo en un punto fuerte. Como todo es dinámico, puede diseñar un sistema estático "falso". En combinación con un localizador de servicios, puede vincular cada componente / módulo / clase / instancia a un tipo.

var Service, componentA;

function Manager() {
  this.instances = {};
}

Manager.prototype.get = function(name) {
  return this.instances[name];
};

Manager.prototype.set = function(name, value) {
  this.instances[name] = value;
};

Service = new Manager();
componentA = {
  type: "ship",
  value: new Ship()
};

Service.set('componentA', componentA);

// DI
function World(ship) {
  if (ship === Service.matchType('ship', ship))
    this.ship = new ship();
  else
    throw Error("Wrong type passed.");
}

// Use Case:
var worldInstance = new World(Service.get('componentA'));

Un ejemplo simplista. Para un uso efectivo del mundo real, necesitará llevar este concepto más allá, pero podría ayudar a desacoplar su sistema si realmente desea la inyección de dependencia tradicional. Es posible que deba jugar un poco con este concepto. No he pensado mucho en el ejemplo anterior.

Modelo-Vista-Controlador

El patrón más obvio y el más utilizado en la web. Hace unos años, JQuery estaba de moda, y así, nacieron los complementos de JQuery. No necesitaba un marco completo en el lado del cliente, solo use jquery y algunos complementos.

Ahora, hay una gran guerra de JavaScript del lado del cliente. La mayoría de los cuales usan el patrón MVC, y todos lo usan de manera diferente. MVC no siempre se implementa de la misma manera.

Si está utilizando las interfaces prototípicas tradicionales, es posible que tenga dificultades para obtener un azúcar sintáctico o una buena API cuando trabaje con MVC, a menos que desee hacer un trabajo manual. Ember.js resuelve esto creando un sistema de "clase" / objeto ". Un controlador podría verse así:

 var Controller = Ember.Controller.extend({
      index: function() {
        // Do something....
      }
 });

La mayoría de las bibliotecas del lado del cliente también extienden el patrón MVC al introducir vistas de ayuda (que se convierten en vistas) y plantillas (que se convierten en vistas).


Nuevas características de JavaScript:

Esto solo será efectivo si está utilizando Node.js, pero no obstante, es invaluable. Esta charla en NodeConf por Brendan Eich trae algunas características nuevas y geniales. La sintaxis de la función propuesta, y especialmente la biblioteca Task.js js.

Esto probablemente solucionará la mayoría de los problemas con el anidamiento de funciones y traerá un rendimiento ligeramente mejor debido a la falta de sobrecarga de funciones.

No estoy muy seguro de si V8 admite esto de forma nativa, la última vez que verifiqué que necesitabas habilitar algunas banderas, pero esto funciona en un puerto de Node.js que usa SpiderMonkey .

Recursos extra:

Daniel
fuente
2
Buen artículo. Yo personalmente no tengo uso para el MV? bibliotecas Tenemos todo lo que necesitamos para organizar nuestro código para aplicaciones más grandes y complejas. Todos me recuerdan demasiado a Java y C # tratando de lanzar sus propias cortinas de basura sobre lo que realmente estaba sucediendo en la comunicación servidor-cliente. Tenemos un DOM. Tenemos delegación del evento. Tenemos POO. Puedo vincular mis propios eventos a los cambios de datos tyvm.
Erik Reppen
2
"En lugar de tener un gran desastre de devolución de llamadas, mantenga una sola función en una sola tarea y haga bien esa tarea". - Poesía
CuriousWebDeveloper
1
Javascript cuando estaba en una era muy oscura a principios y mediados de la década de 2000, cuando pocos entendían cómo escribir grandes aplicaciones usándolo. Como dice @ErikReppen, si encuentra que su aplicación JS se parece a una aplicación Java o C #, lo está haciendo mal.
backpackcoder
3

Agregando a la respuesta de Daniels:

Valores / componentes observables

Esta idea está tomada del marco MVVM Knockout.JS ( ko.observable ), con la idea de que los valores y los objetos pueden ser sujetos observables, y una vez que se produce el cambio en un valor u objeto, actualizará automáticamente a todos los observadores. Básicamente es el patrón de observador implementado en Javascript y, en cambio, cómo se implementan la mayoría de los marcos de pub / sub, la "clave" es el sujeto en lugar de un objeto arbitrario.

El uso es el siguiente:

// the subjects
// plain old javascript object with observable values
var shipComponent = {
    velocity : observable(0)
};

// the observer, a player user interface
// implemented with revealing module pattern
var playerUi = (function(ship) {

  var module = {
    setVelocity: function (x) { 
      // ... sets the velocity on the player user interface
    },

    // only called once
    init: function() {

      // subscribe to changes on the velocity value
      // using the module's function as callback
      module.velocity.onChange(playerUi.setVelocity);
    }
  };

  return module;
})(shipComponent).init();

// the player ui will change when the velocity value is changed
shipComponent.velocity.set(10);

La idea es que los observadores usualmente sepan dónde está el sujeto y cómo suscribirse a él. La ventaja de esto en lugar del pub / sub es notable si tiene que cambiar mucho el código, ya que es más fácil eliminar temas como un paso de refactorización. Quiero decir esto porque una vez que eliminas un tema, todos los que dependían de él fallarán. Si el código falla rápidamente, entonces sabe dónde eliminar las referencias restantes. Esto está en contraste con el tema completamente desacoplado (como con una clave de cadena en el patrón pub / sub) y tiene una mayor probabilidad de permanecer en el código, especialmente si se usaron claves dinámicas y el programador de mantenimiento no se dio cuenta de ello (muerto el código en la programación de mantenimiento es un problema molesto).

En la programación de juegos, esto reduce la necesidad de un antiguo patrón de bucle de actualización y más en un lenguaje de programación igualado / reactivo, porque tan pronto como se cambia algo, el sujeto actualizará automáticamente a todos los observadores en el cambio, sin tener que esperar el bucle de actualización ejecutar. Hay usos para el ciclo de actualización (para cosas que deben sincronizarse con el tiempo transcurrido del juego), pero a veces simplemente no desea saturarlo cuando los componentes mismos pueden actualizarse automáticamente con este patrón.

La implementación real de la función observable es en realidad sorprendentemente fácil de escribir y comprender (especialmente si sabe cómo manejar matrices en javascript y el patrón de observación ):

var observable = function(v) {
    var val = v, subscribers = [];

    // the observable object,
    // as revealing module
    var output = {

        // subscribes to event
        onChange : function(func) {
            // idiomatic JS to add object to the
            // subscribers array
            subscribers.push(func);

            return output: // enables chaining
        },

        // the method that changes the observable object
        // and emits the event
        set : function(v) {
            var i;
            val = v;
            for (i = 0, i < subscribers.length; i++) {
                // this is hardly fault tolerant but as long
                // as subscribers are functions it'll work
                subscribers[i](v);
            }

            return output;
        }

    };

    return output;
};

Hice una implementación del objeto observable en JsFiddle que continúa con la observación de componentes y la eliminación de suscriptores. Siéntase libre de experimentar el JsFiddle.

Spoike
fuente