web-dev-qa-db-fra.com

Y a-t-il des principes OO qui sont pratiquement applicables à Javascript?

Javascript est un langage orienté objet basé sur un prototype, mais peut devenir basé sur une classe de diverses manières, soit par:

  • Écrire les fonctions à utiliser comme classes par vous-même
  • Utilisez un système de classe astucieux dans un cadre (tel que mootools Class.Class )
  • Générez-le à partir de Coffeescript

Au début, j'écrivais du code basé sur des classes en Javascript et je m'en appuyais beaucoup. Récemment, cependant, j'utilise des frameworks Javascript, et NodeJS , qui s'éloignent de cette notion de classes et s'appuient davantage sur la nature dynamique du code tels que:

  • Programmation asynchrone, utilisation et écriture de code d'écriture utilisant des rappels/événements
  • Chargement des modules avec RequireJS (afin qu'ils ne fuient pas vers l'espace de noms global)
  • Concepts de programmation fonctionnelle tels que les compréhensions de liste (carte, filtre, etc.)
  • Entre autres

Ce que j'ai rassemblé jusqu'à présent, c'est que la plupart des principes et des modèles OO que j'ai lus (tels que les modèles SOLID et GoF) ont été écrits pour les langages OO basés sur les classes à l'esprit comme Smalltalk et C++. Mais y en a-t-il qui s'appliquent à un langage basé sur un prototype tel que Javascript?

Existe-t-il des principes ou des modèles spécifiques à Javascript? Principes pour éviter l'enfer de rappel , le mal eval , ou tout autre anti- motifs etc.

81
Spoike

Après de nombreuses modifications, cette réponse est devenue un monstre de longueur. Je m'excuse d'avance.

Tout d'abord, eval() n'est pas toujours mauvais, et peut apporter des avantages en termes de performances lorsqu'il est utilisé dans lazy-evaluation, par exemple. L'évaluation différée est similaire au chargement différé, mais vous stockez essentiellement votre code dans des chaînes, puis utilisez eval ou new Function Pour évaluer le code. Si vous utilisez des astuces, cela deviendra beaucoup plus utile que le mal, mais si vous ne le faites pas, cela peut conduire à de mauvaises choses. Vous pouvez regarder mon système de modules qui utilise ce modèle: https://github.com/TheHydroImpulse/resolve.js . Resolve.js utilise eval au lieu de new Function Principalement pour modéliser les variables CommonJS exports et module disponibles dans chaque module, et new Function Enveloppe votre code dans un anonyme fonction, cependant, je finis par envelopper chaque module dans une fonction que je fais manuellement en combinaison avec eval.

Vous en lirez plus dans les deux articles suivants, le dernier faisant également référence au premier.

Générateurs d'harmonie

Maintenant que les générateurs ont finalement atterri en V8 et donc en Node.js, sous un drapeau (--harmony Ou --harmony-generators). Ceux-ci réduisent considérablement le nombre d'enfer de rappel que vous avez. Cela rend l'écriture de code asynchrone vraiment géniale.

La meilleure façon d'utiliser les générateurs est d'utiliser une sorte de bibliothèque de flux de contrôle. Cela permettra au flux de continuer à avancer au fur et à mesure que vous céderez au sein des générateurs.

Récapitulation/Aperçu:

Si vous n'êtes pas familier avec les générateurs, c'est une pratique de suspendre l'exécution de fonctions spéciales (appelées générateurs). Cette pratique est appelée produisant à l'aide du mot clé yield.

Exemple:

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

Ainsi, chaque fois que vous appelez cette fonction la première fois, elle renvoie une nouvelle instance de générateur. Cela vous permet d'appeler next() sur cet objet pour démarrer ou reprendre le générateur.

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

Vous continueriez d'appeler next jusqu'à ce que done renvoie true. Cela signifie que le générateur a complètement terminé son exécution et qu'il n'y a plus d'instructions yield.

Contrôle-flux:

Comme vous pouvez le voir, le contrôle des générateurs n'est pas automatique. Vous devez continuer manuellement chacun. C'est pourquoi des bibliothèques de flux de contrôle comme co sont utilisées.

Exemple:

var co = require('co');

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

Cela permet de tout écrire en Node (et le navigateur avec Facebook's Regenerator qui prend en entrée le code source qui utilise des générateurs d'harmonie et divise ES5 entièrement compatible) code) avec un style synchrone.

Les générateurs sont encore assez nouveaux et nécessitent donc Node.js> = v11.2. Au moment où j'écris ceci, la v0.11.x est toujours instable et donc de nombreux modules natifs sont cassés et seront jusqu'à la v0.12, où l'API native se calmera.


Pour ajouter à ma réponse originale:

J'ai récemment préféré une API plus fonctionnelle en JavaScript. La convention utilise OOP en arrière-plan en cas de besoin mais elle simplifie tout.

Prenons par exemple un système de visualisation (client ou serveur).

view('home.welcome');

Est beaucoup plus facile à lire ou à suivre que:

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

La fonction view vérifie simplement si la même vue existe déjà dans une carte locale. Si la vue n'existe pas, elle créera une nouvelle vue et ajoutera une nouvelle entrée à la carte.

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 = {};

Extrêmement basique, non? Je trouve que cela simplifie considérablement l'interface publique et la rend plus facile à utiliser. J'emploie également la capacité de chaîne ...

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

Tower, un framework que je développe (avec quelqu'un d'autre) ou développant la prochaine version (0.5.0) utilisera cette approche fonctionnelle dans la plupart de ses interfaces d'exposition.

Certaines personnes profitent des fibres pour éviter "l'enfer du rappel". C'est une approche assez différente de JavaScript, et je ne suis pas un grand fan de celui-ci, mais de nombreux frameworks/plateformes l'utilisent; y compris Meteor, car ils traitent Node.js comme un thread/par plate-forme de connexion.

Je préfère utiliser une méthode abstraite pour éviter l'enfer de rappel. Cela peut devenir lourd, mais cela simplifie considérablement le code d'application réel. En aidant à la construction du framework TowerJS , cela a résolu beaucoup de nos problèmes, cependant, vous aurez évidemment toujours un certain niveau de rappels, mais l'imbrication n'est pas profonde.

// 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 exemple de notre système de routage et de nos "contrôleurs", actuellement en cours de développement, quoique assez différent du "Rails-like" traditionnel. Mais l'exemple est extrêmement puissant et minimise la quantité de rappels et rend les choses assez apparentes.

Le problème avec cette approche est que tout est abstrait. Rien ne fonctionne tel quel et nécessite un "cadre" derrière lui. Mais si ces types de fonctionnalités et de style de codage sont mis en œuvre dans un cadre, c'est une énorme victoire.

Pour les modèles en JavaScript, cela dépend honnêtement. L'héritage n'est vraiment utile que lorsque vous utilisez CoffeeScript, Ember ou tout autre framework/infrastructure de "classe". Lorsque vous êtes dans un environnement JavaScript "pur", l'utilisation de l'interface prototype traditionnelle fonctionne comme un charme:

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

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

Ember.js a commencé, du moins pour moi, à utiliser une approche différente pour construire des objets. Au lieu de construire indépendamment chaque méthode prototype, vous utiliseriez une interface de type module.

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

Tous ces styles de codage sont différents, mais ils s'ajoutent à votre base de code.

Polymorphisme

Le polymorphisme n'est pas largement utilisé en JavaScript pur, où travailler avec l'héritage et copier le modèle de type "classe" nécessite beaucoup de code standard.

Conception basée sur les événements/composants

Les modèles basés sur les événements et les composants sont les gagnants de l'OMI ou les plus faciles à travailler, en particulier lorsque vous travaillez avec Node.js, qui a un composant EventEmitter intégré, bien que la mise en œuvre de tels émetteurs soit triviale, c'est juste un ajout sympa .

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

Juste un exemple, mais c'est un joli modèle avec lequel travailler. Surtout dans un projet orienté jeu/composant.

La conception des composants est un concept distinct en soi, mais je pense qu'elle fonctionne extrêmement bien en combinaison avec des systèmes d'événements. Les jeux sont traditionnellement connus pour la conception basée sur les composants, où la programmation orientée objet ne vous emmène que jusqu'à présent.

La conception basée sur les composants a ses utilisations. Cela dépend du type de système de votre bâtiment. Je suis sûr que cela fonctionnerait avec des applications Web, mais cela fonctionnerait extrêmement bien dans un environnement de jeu, en raison du nombre d'objets et de systèmes séparés, mais d'autres exemples existent sûrement.

Motif Pub/Sub

La liaison d'événements et pub/sub est similaire. Le modèle pub/sub brille vraiment dans les applications Node.js en raison de la langue unificatrice, mais il peut fonctionner dans n'importe quelle langue. Fonctionne très bien dans les applications en temps réel, les jeux, etc.

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

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

Observateur

Cela peut être subjectif, car certaines personnes choisissent de considérer le modèle Observer comme un pub/sub, mais elles ont leurs différences.

"L'observateur est un modèle de conception dans lequel un objet (connu sous le nom de sujet) maintient une liste d'objets en fonction (observateurs), les informant automatiquement de tout changement d'état." - Le modèle d'observateur

Le modèle d'observation est une étape au-delà des systèmes pub/sous-types. Les objets ont des relations ou des méthodes de communication strictes entre eux. Un objet "Sujet" conserverait une liste des "Observateurs" dépendants. Le sujet tiendrait ses observateurs à jour.

Programmation réactive

La programmation réactive est un concept plus petit et plus inconnu, en particulier en JavaScript. Il y a un framework/bibliothèque (que je sache) qui expose une API facile à utiliser pour utiliser cette "programmation réactive".

Ressources sur la programmation réactive:

Fondamentalement, c'est d'avoir un ensemble de données de synchronisation (que ce soit des variables, des fonctions, etc.).

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

 a = 2;

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

Je crois que la programmation réactive est considérablement cachée, en particulier dans les langages impératifs. C'est un paradigme de programmation incroyablement puissant, en particulier dans Node.js. Meteor a créé son propre moteur réactif dans lequel le framework est fondamentalement basé. Comment fonctionne la réactivité de Meteor dans les coulisses? est un excellent aperçu de son fonctionnement interne.

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

Cela s'exécutera normalement, affichant la valeur de name, mais si nous la modifions

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

Il affichera à nouveau le fichier console.log affichant Hello Bob. Un exemple de base, mais vous pouvez appliquer cette technique aux modèles de données et aux transactions en temps réel. Vous pouvez créer des systèmes extrêmement puissants derrière ce protocole.

Meteor ...

Le modèle réactif et le modèle d'observateur sont assez similaires. La principale différence est que le modèle d'observateur décrit généralement le flux de données avec des objets/classes entiers, tandis que la programmation réactive décrit plutôt le flux de données vers des propriétés spécifiques.

Meteor est un excellent exemple de programmation réactive. Son exécution est un peu compliquée en raison du manque d'événements de changement de valeur native de JavaScript (les proxys Harmony changent cela). D'autres frameworks côté client, Ember.js et AngularJS utilisent également une programmation réactive (dans une certaine mesure).

Les deux derniers frameworks utilisent le modèle réactif notamment sur leurs modèles (c'est-à-dire la mise à jour automatique). Angular.js utilise une technique simple de vérification sale. Je n'appellerais pas cela une programmation exactement réactive, mais c'est proche, car une vérification sale n'est pas en temps réel. Ember.js utilise une approche différente. Ember utilisez les méthodes set() et get() qui leur permettent de mettre à jour immédiatement les valeurs dépendantes. Avec leur boucle d'exécution, il est extrêmement efficace et permet des valeurs plus dépendantes, où angular a une limite théorique.

Promesses

Pas un correctif pour les rappels, mais supprime une indentation et maintient les fonctions imbriquées au minimum. Il ajoute également une syntaxe agréable au problème.

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
});

Vous pouvez également répartir les fonctions de rappel afin qu'elles ne soient pas en ligne, mais c'est une autre décision de conception.

Une autre approche consisterait à combiner les événements et les promesses à l'endroit où vous auriez une fonction pour répartir les événements de manière appropriée, puis les fonctions fonctionnelles réelles (celles qui ont la vraie logique en elles) se lieraient à un événement particulier. Vous passeriez ensuite la méthode du répartiteur à l'intérieur de chaque position de rappel, cependant, vous auriez à résoudre certains problèmes qui vous viendraient à l'esprit, tels que les paramètres, sachant à quelle fonction envoyer, etc.

Fonction à fonction unique

Au lieu d'avoir un énorme gâchis d'enfer de rappel, gardez une seule fonction pour une seule tâche, et faites bien cette tâche. Parfois, vous pouvez prendre de l'avance sur vous-même et ajouter plus de fonctionnalités dans chaque fonction, mais demandez-vous: Cela peut-il devenir une fonction indépendante? Nommez la fonction, et cela nettoie votre indentation et, par conséquent, nettoie le problème de l'enfer de rappel.

En fin de compte, je suggérerais de développer ou d'utiliser un petit "framework", fondamentalement juste une épine dorsale pour votre application, et de prendre le temps de faire des abstractions, de décider d'un système basé sur les événements, ou de "charges de petits modules qui sont indépendant ". J'ai travaillé avec plusieurs projets Node.js où le code était extrêmement compliqué avec l'enfer de rappel en particulier, mais aussi un manque de réflexion avant de commencer à coder. Prenez votre temps pour réfléchir aux différentes possibilités en termes d'API et de syntaxe.

Ben Nadel a fait de très bons articles de blog sur JavaScript et quelques modèles assez stricts et avancés qui peuvent fonctionner dans votre situation. Quelques bons articles sur lesquels je vais insister:

Inversion de contrôle

Bien que n'étant pas exactement lié à l'enfer de rappel, il peut vous aider à l'architecture globale, en particulier dans les tests unitaires.

Les deux sous-versions principales de l'inversion de contrôle sont l'injection de dépendance et le localisateur de service. Je trouve que Service Locator est le plus simple dans JavaScript, par opposition à l'injection de dépendance. Pourquoi? Principalement parce que JavaScript est un langage dynamique et qu'aucune saisie statique n'existe. Java et C #, entre autres, sont "connus" pour l'injection de dépendances car ils sont capables de détecter des types, et ils ont intégré des interfaces, des classes, etc ... Cela rend les choses assez faciles. Vous peut, cependant, recréer cette fonctionnalité dans JavaScript, cependant, cela ne va pas être identique et un peu hacky, je préfère utiliser un localisateur de service dans mes systèmes.

Tout type d'inversion de contrôle découplera considérablement votre code en modules distincts qui peuvent être moqués ou truqués à tout moment. Vous avez conçu une deuxième version de votre moteur de rendu? Génial, remplacez simplement l'ancienne interface par la nouvelle. Les localisateurs de services sont particulièrement intéressants avec les nouveaux proxys Harmony, cependant, uniquement utilisables efficacement dans Node.js, ils fournissent une API plus agréable, plutôt que d'utiliser Service.get('render'); et à la place Service.render. Je travaille actuellement sur ce type de système: https://github.com/TheHydroImpulse/Ettore .

Bien que le manque de typage statique (le typage statique étant une raison possible des utilisations efficaces de l'injection de dépendances en Java, C #, PHP - Ce n'est pas du typage statique, mais il a des indices de type.) Pourrait être considéré comme un point négatif, vous pouvez certainement le transformer en un point fort. Parce que tout est dynamique, vous pouvez concevoir un "faux" système statique. En combinaison avec un localisateur de service, vous pouvez avoir chaque composant/module/classe/instance lié à un type.

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 exemple simpliste. Pour une utilisation réelle et efficace, vous devrez aller plus loin dans ce concept, mais cela pourrait aider à découpler votre système si vous voulez vraiment une injection de dépendance traditionnelle. Vous devrez peut-être jouer un peu avec ce concept. Je n'ai pas beaucoup réfléchi à l'exemple précédent.

Modèle Vue Contrôleur

Le motif le plus évident et le plus utilisé sur le web. Il y a quelques années, JQuery était à la mode, et donc, les plugins JQuery sont nés. Vous n'aviez pas besoin d'un framework complet côté client, utilisez simplement jquery et quelques plugins.

Maintenant, il y a une énorme guerre de framework JavaScript côté client. La plupart d'entre eux utilisent le modèle MVC et ils l'utilisent tous différemment. MVC n'est pas toujours implémenté de la même manière.

Si vous utilisez les interfaces prototypiques traditionnelles, vous pourriez avoir du mal à obtenir un sucre syntaxique ou une API Nice lorsque vous travaillez avec MVC, sauf si vous souhaitez effectuer un travail manuel. Ember.js résout ce problème en créant un système "classe"/objet ". Un contrôleur pourrait ressembler à:

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

La plupart des bibliothèques côté client étendent également le modèle MVC en introduisant des aides à la vue (devenant des vues) et des modèles (devenant des vues).


Nouvelles fonctionnalités JavaScript:

Cela ne sera efficace que si vous utilisez Node.js, mais néanmoins, c'est inestimable. Cette conférence à NodeConf par Brendan Eich apporte de nouvelles fonctionnalités intéressantes. La syntaxe de fonction proposée, et en particulier la bibliothèque Task.js js.

Cela résoudra probablement la plupart des problèmes d'imbrication de fonctions et apportera des performances légèrement meilleures en raison du manque de surcharge de fonction.

Je ne sais pas trop si V8 prend en charge cela nativement, j'ai vérifié que vous aviez besoin d'activer certains indicateurs, mais cela fonctionne dans un port de Node.js qui utilise SpiderMonkey .

Ressources supplémentaires:

116
Daniel

Ajout à la réponse de Daniels:

Valeurs/composants observables

Cette idée est empruntée au framework MVVM Knockout.JS ( ko.observable ), avec l'idée que les valeurs et les objets peuvent être des sujets observables, et une fois que le changement se produit dans une valeur ou un objet, il mettra automatiquement à jour tous les observateurs. C'est fondamentalement le modèle d'observateur implémenté en Javascript, et à la place de la façon dont la plupart des frameworks pub/sub sont implémentés, la "clé" est le sujet lui-même au lieu d'un objet arbitraire.

L'utilisation est la suivante:

// 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);

L'idée est que les observateurs savent généralement où se trouve le sujet et comment s'y abonner. L'avantage de ceci au lieu du pub/sub est perceptible si vous devez beaucoup changer le code car il est plus facile de supprimer des sujets comme étape de refactoring. Je veux dire cela parce qu'une fois que vous supprimez un sujet, tous ceux qui en dépendent échouent. Si le code échoue rapidement, vous savez où supprimer les références restantes. Cela contraste avec le sujet complètement découplé (comme avec une clé de chaîne dans le modèle pub/sub) et a plus de chances de rester dans le code, surtout si des clés dynamiques ont été utilisées et que le programmeur de maintenance n'en a pas été informé (mort dans la programmation de maintenance est un problème gênant).

Dans la programmation de jeux, cela réduit le besoin de ye olde mettre à jour le modèle de boucle et plus en un idiome de programmation événementiel/réactif, car dès que quelque chose est changé, le Le sujet mettra automatiquement à jour tous les observateurs sur le changement, sans avoir à attendre l'exécution de la boucle de mise à jour. Il y a des utilisations pour la boucle de mise à jour (pour les choses qui doivent être synchronisées avec le temps de jeu écoulé), mais parfois vous ne voulez simplement pas l'encombrer lorsque les composants eux-mêmes peuvent se mettre à jour automatiquement avec ce modèle.

L'implémentation réelle de la fonction observable est en fait étonnamment facile à écrire et à comprendre (surtout si vous savez comment gérer les tableaux en javascript et le modèle d'observateur ):

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;
};

J'ai fait ne implémentation de l'objet observable dans JsFiddle qui continue avec des composants d'observation et la possibilité de supprimer des abonnés. N'hésitez pas à expérimenter le JsFiddle.

3
Spoike