Existe-t-il des principes OO applicables dans la pratique au Javascript?


79

Javascript est un langage orienté objet basé sur un prototype mais qui peut le devenir de différentes façons, soit:

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

Au début, j’avais tendance à écrire du code basé sur les classes en Javascript et à m’en faire largement confiance. 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 utilisant des rappels / événements
  • Chargement de modules avec RequireJS (afin qu'ils ne fuient pas vers l'espace de noms global)
  • Concepts de programmation fonctionnels tels que la compréhension de liste (carte, filtre, etc.)
  • Entre autres

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

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

Réponses:


116

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

Tout d'abord, ce eval()n'est pas toujours mauvais et peut apporter des avantages en termes de performances lorsqu'il est utilisé dans une évaluation paresseuse, par exemple. Lazy-evaluation est similaire à lazy-loading, mais vous stockez essentiellement votre code dans des chaînes, puis vous utilisez evalou new Functionpour évaluer le code. Si vous utilisez des astuces, cela deviendra beaucoup plus utile que le mal, mais si vous ne l'utilisez pas, cela peut conduire à de mauvaises choses. Vous pouvez regarder mon système de module qui utilise ce modèle: https://github.com/TheHydroImpulse/resolve.js . Resolve.js utilise eval plutôt que new Functionprincipalement pour modéliser le CommonJS exportset les modulevariables disponibles dans chaque module, et new Functionenveloppe votre code dans une fonction anonyme. Toutefois, je finis par envelopper chaque module dans une fonction que je fais manuellement en combinaison avec eval.

Vous en saurez 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 dans V8 et donc dans Node.js, sous un drapeau ( --harmonyou --harmony-generators). Ceux-ci réduisent considérablement la quantité de rappel que vous avez. L'écriture de code asynchrone est vraiment géniale.

Le meilleur moyen d’utiliser les générateurs est d’utiliser une sorte de bibliothèque de flux de contrôle. Cela permettra de continuer à circuler au fur et à mesure que vous cédez au sein de générateurs.

Récapitulation / aperçu:

Si vous n'êtes pas familier avec les générateurs, ils ont l'habitude de suspendre l'exécution de fonctions spéciales (appelées générateurs). Cette pratique s'appelle céder en utilisant le yieldmot - clé.

Exemple:

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

Ainsi, chaque fois que vous appelez cette fonction pour la première fois, une nouvelle instance de générateur est renvoyée. 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 continuerez à appeler nextjusqu'au doneretour true. Cela signifie que le générateur a complètement terminé son exécution et qu'il n'y a plus d' yieldinstructions.

Flux de contrôle:

Comme vous pouvez le constater, les générateurs de contrôle ne sont pas automatiques. Vous devez continuer manuellement chacun d'eux. C'est pourquoi les bibliothèques de contrôle-flux comme co sont utilisées.

Exemple:

var co = require('co');

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

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

Les générateurs sont encore assez récents et nécessitent donc Node.js> = v11.2. Au moment où j'écris ces lignes, la v0.11.x est toujours instable et de nombreux modules natifs sont donc cassés et le resteront 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 la POO en coulisse lorsque cela est nécessaire, 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 viewfonction vérifie simplement si la même vue existe déjà dans une carte locale. Si la vue n'existe pas, une nouvelle vue est créée et une nouvelle entrée est ajouté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 aussi 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 que je développe 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 "un rappel difficile". C'est une approche assez différente de JavaScript, et je n'en suis pas un grand fan, 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 grandement le code de l'application. Lors de la construction du framework TowerJS , beaucoup de problèmes ont été résolus, mais il est évident que vous aurez toujours un certain nombre 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" en cours de développement, bien que assez différent du "type de rail" traditionnel. Mais cet exemple est extrêmement puissant et minimise le nombre de rappels et rend les choses assez évidentes.

Le problème avec cette approche est que tout est abstrait. Rien ne fonctionne comme-est et nécessite un "cadre" derrière. Mais si ces types de fonctionnalités et ce style de codage sont implémentés dans un cadre, le gain est énorme.

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 vous trouvez dans un environnement JavaScript "pur", l'utilisation du prototype d'interface traditionnelle fonctionne à merveille:

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 la construction d'objets. Au lieu de construire chaque prototype de méthode indépendamment, vous utiliseriez une interface de type module.

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

Tous ces styles sont différents, mais 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 passe-partout.

Conception basée sur événement / composant

Les modèles basés sur les événements et sur les composants sont les gagnants IMO, ou le plus facile à utiliser, en particulier lorsque vous travaillez avec Node.js, qui possède un composant EventEmitter intégré, bien que l'implémentation de tels émetteurs soit triviale, c'est simplement un ajout intéressant. .

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

Juste un exemple, mais c’est un bon 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 que cela fonctionne extrêmement bien en combinaison avec les systèmes d'événements. Les jeux sont traditionnellement connus pour la conception à base de composants, où la programmation orientée objet vous emmène jusque-là.

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

Motif Pub / Sub

La liaison d'événement et pub / sub est similaire. Le modèle pub / sub brille vraiment dans les applications Node.js à cause du langage unificateur, mais il peut fonctionner dans n’importe quel langage. Fonctionne extrêmement 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 style pub / sub, mais elles ont leurs différences.

"Observer est un modèle de conception dans lequel un objet (appelé sujet) maintient une liste d'objets en fonction de celui-ci (observateurs), en le notifiant automatiquement de tout changement d'état." - Le modèle d'observateur

Le modèle d'observateur est une étape au-delà des systèmes pub / sub classiques. Les objets ont des relations ou des méthodes de communication strictes les uns avec les autres. Un objet "Sujet" conserverait une liste de personnes à charge "Observateurs". Le sujet tiendra ses observateurs à jour.

Programmation Réactive

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

Ressources sur la programmation réactive:

En gros, il s’agit d’un ensemble de données de synchronisation (variables, 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 cadre est essentiellement basé. Comment la réactivité de Meteor fonctionne-t-elle en coulisse? est un excellent aperçu de la façon dont cela fonctionne en interne.

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

Ceci s’exécutera normalement, affichant la valeur de name, mais si nous changeons le

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

Il ré-affichera le fichier console.log Hello Bob. 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 ...

Les schémas réactif et observateur sont assez similaires. La principale différence est que le modèle d'observateur décrit couramment 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 dans des propriétés spécifiques.

Meteor est un excellent exemple de programmation réactive. Son exécution est un peu compliquée à cause du manque d'événements de changement de valeur natifs dans JavaScript (les mandataires Harmony le changent). D' autres cadres côté client, ember.js et AngularJS utilisent également la programmation réactive (dans une certaine mesure).

Les deux derniers cadres 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 les vérifications en profondeur ne se font pas en temps réel. Ember.js utilise une approche différente. Ember utiliser set()et get()méthodes qui leur permettent de mettre à jour immédiatement les valeurs dépendantes. Avec leur runloop, il est extrêmement efficace et permet de définir davantage de valeurs dépendantes, où angular a une limite théorique.

Promesses

Pas un correctif pour les rappels, mais prend une certaine indentation, et garde les fonctions imbriquées au minimum. Cela ajoute aussi une belle syntaxe 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 des événements et des promesses dans lesquels vous auriez une fonction pour répartir les événements de manière appropriée, puis les fonctions fonctionnelles réelles (celles qui contiennent la logique réelle) seraient liées à un événement particulier. Vous passeriez alors la méthode de 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, savoir à quelle fonction envoyer, etc.

Fonction à fonction unique

Au lieu d'avoir un énorme bazar de callbacks, gardez une seule fonction pour une seule tâche et faites-la bien. Parfois, vous pouvez prendre de l'avance et ajouter plus de fonctionnalités dans chaque fonction, mais posez-vous la question suivante: cette fonction peut-elle devenir indépendante? Nommez la fonction, cela nettoie votre mise en retrait 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", essentiellement une épine dorsale de votre application, et de prendre du temps pour créer des abstractions, choisir un système basé sur les événements, ou un "lot de petits modules système "indépendant". J'ai travaillé avec plusieurs projets Node.js où le code était extrêmement compliqué, notamment en callback, mais aussi par manque de réflexion avant de commencer à coder. Prenez le temps de réfléchir aux différentes possibilités en termes d’API et de syntaxe.

Ben Nadel a publié de très bons blogs sur JavaScript et des modèles assez stricts et avancés qui peuvent fonctionner dans votre situation. Quelques bons messages que je vais souligner:

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 principales versions d’inversion de contrôle sont Injection de dépendance et Localisateur de service. Je trouve que Service Locator est le plus simple en JavaScript, par opposition à Dependency Injection. Pourquoi? Principalement parce que JavaScript est un langage dynamique et qu’il n’existe pas de typage statique. Java et C #, entre autres, sont "connus" pour l'injection de dépendances parce que vous êtes capable de détecter des types, et qu'ils ont des interfaces, des classes, etc. intégrées. Cela rend les choses assez faciles. Vous pouvez, cependant, recréer cette fonctionnalité dans JavaScript, bien que cela ne soit pas identique et un peu compliqué, je préfère utiliser un localisateur de services dans mes systèmes.

N'importe quel type d'inversion de contrôle découplera votre code de façon spectaculaire en modules distincts qui peuvent être fictifs ou simulés à tout moment. 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 serveurs mandataires Harmony, bien qu’ils ne puissent être utilisés efficacement que dans Node.js. Ils fournissent une API plus agréable, plutôt que d’utiliser Service.get('render');et de remplacer Service.render. Je travaille actuellement sur ce type de système: https://github.com/TheHydroImpulse/Ettore .

Bien que l’absence de typage statique (le typage statique soit une raison possible des utilisations effectives de l’injection de dépendances en Java, C #, PHP - ce n’est pas du type statique, mais des astuces de type.) Peut être considéré comme un point négatif. vraiment en faire 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 associer chaque composant / module / classe / instance à 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. Dans un monde réel, avec une utilisation efficace, vous devrez approfondir ce concept, mais il pourrait aider à découpler votre système si vous souhaitez réellement 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 modèle le plus évident et le plus utilisé sur le Web. Il y a quelques années, JQuery était à la mode et les plugins JQuery sont nés. Vous n'avez pas besoin d'un framework complet côté client, utilisez simplement jquery et quelques plugins.

Maintenant, il y a une énorme guerre d'infrastructure 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 mis en œuvre de la même manière.

Si vous utilisez les interfaces prototypiques traditionnelles, vous aurez peut-être du mal à obtenir un sucre syntaxique ou une belle API lorsque vous travaillerez avec MVC, à moins que vous ne souhaitiez effectuer un travail manuel. Ember.js résout ce problème en créant un système "classe" / objet. Un contrôleur pourrait ressembler à ceci:

 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 (en devenant des vues).


Nouvelles fonctionnalités de JavaScript:

Cela ne sera efficace que si vous utilisez Node.js, mais néanmoins, c'est inestimable. Cette conférence sur NodeConf de 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 liés à l'imbrication de fonctions et apportera des performances légèrement meilleures en raison du manque de charge de la fonction.

Je ne suis pas sûr que V8 prenne cela en charge de manière native. La dernière fois que j'ai vérifié, vous deviez activer certains indicateurs, mais cela fonctionne dans un port de Node.js qui utilise SpiderMonkey .

Ressources supplémentaires:


2
Belle écriture. Personnellement, je n'ai aucune utilité pour le MV? bibliothèques. Nous avons tout ce dont nous avons besoin pour organiser notre code pour des applications plus grandes et plus complexes. Ils me rappellent tous trop que Java et C # essaient de jeter leurs propres rideaux de merde sur ce qui se passait réellement dans la communication serveur-client. Nous avons un DOM. Nous avons eu la délégation de l'événement. Nous avons OOP. Je peux lier mes propres événements aux modifications de données tyvm.
Erik Reppen

2
"Au lieu d'avoir un énorme désastre de callback, gardez une seule fonction pour une seule tâche et faites-la bien." - Poésie.
CuriousWebDeveloper

1
Javascript à l’époque du début au milieu des années 2000, alors que très peu de gens comprenaient comment écrire pour les utiliser avec de grandes applications. Comme @ErikReppen le dit, si vous trouvez que votre application JS ressemble à une application Java ou C #, vous vous trompez.
Backpackcoder

3

Ajout à la réponse de Daniels:

Valeurs observables / composants

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 qu'un changement se produit dans une valeur ou un objet, il met automatiquement à jour tous les observateurs. Il s’agit en gros du modèle d’observateur implémenté en Javascript, et de la manière dont la plupart des frameworks pub / sous sont implémentés, la "clé" est le sujet lui-même et non 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 cela au lieu de pub / sub est notable si vous devez modifier beaucoup le code car il est plus facile de supprimer des sujets lors d’une étape de refactoring. Je veux dire cela parce que, une fois que vous supprimez un sujet, toutes les personnes qui en dépendent échoueront. Si le code échoue rapidement, vous savez où supprimer les références restantes. Ceci est en contraste avec le sujet complètement découplé (comme avec une clé de chaîne dans un motif pub / sub) et a plus de chance de rester dans le code, surtout si des clés dynamiques ont été utilisées et que le programmeur de maintenance n’a pas été mis au courant le code dans la programmation de maintenance est un problème ennuyeux).

En programmation de jeu, cela réduit le besoin de votre ancien modèle de boucle de mise à jour et davantage dans un idiome de programmation événementielle / réactive, car dès que quelque chose est changé, le sujet mettra automatiquement à jour tous les observateurs du changement, sans avoir à attendre la boucle de mise à jour. éxécuter. Il y a des utilisations pour la boucle de mise à jour (pour les éléments devant être synchronisés avec le temps de jeu écoulé), mais il est parfois préférable de ne 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 manipuler des tableaux en javascript et le motif de l' 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 réalisé une implémentation de l'objet observable dans JsFiddle qui continue avec l'observation de composants et la possibilité de supprimer des abonnés. N'hésitez pas à expérimenter le JsFiddle.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.