Ci sono dei principi OO che sono praticamente applicabili per Javascript?


79

Javascript è un linguaggio orientato agli oggetti basato su prototipo ma può diventare basato su classi in vari modi, sia:

  • Scrivere le funzioni da utilizzare come classi da soli
  • Utilizzare un sistema di classe elegante in un framework (come mootools Class.Class )
  • Generalo da Coffeescript

All'inizio tendevo a scrivere codice basato su classi in Javascript e facevo molto affidamento su di esso. Recentemente, tuttavia ho usato framework JavaScript e NodeJS , che va via da questa nozione di classi e si basano più sulla natura dinamica del codice come:

  • Programmazione asincrona, utilizzo e scrittura del codice di scrittura che utilizza callback / eventi
  • Caricamento dei moduli con RequireJS (in modo che non perdano nello spazio dei nomi globale)
  • Concetti di programmazione funzionale come comprensione delle liste (mappa, filtro, ecc.)
  • Tra l'altro

Ciò che ho raccolto finora è che la maggior parte dei principi e dei modelli OO che ho letto (come i modelli SOLID e GoF) sono stati scritti per linguaggi OO basati su classi in mente come Smalltalk e C ++. Ma ce ne sono alcuni applicabili per un linguaggio basato su prototipi come Javascript?

Esistono principi o schemi specifici di Javascript? Principi per evitare l' inferno della richiamata , la malvagia eval o qualsiasi altro anti-schema ecc.

Risposte:


116

Dopo molte modifiche, questa risposta è diventata un mostro di lunghezza. Mi scuso in anticipo.

Prima di tutto, eval()non è sempre male e può portare benefici in termini di prestazioni se usato nella valutazione pigra, per esempio. La valutazione lenta è simile al caricamento lento, ma essenzialmente si memorizza il codice all'interno di stringhe e quindi si utilizza evalo new Functionper valutare il codice. Se usi alcuni trucchi, diventerà molto più utile del male, ma se non lo fai, può portare a cose cattive. Puoi guardare il mio sistema di moduli che utilizza questo modello: https://github.com/TheHydroImpulse/resolve.js . Resolve.js utilizza eval anziché new Functionprincipalmente per modellare CommonJS exportse le modulevariabili disponibili in ciascun modulo e new Functionracchiude il codice all'interno di una funzione anonima, tuttavia, finisco per avvolgere ogni modulo in una funzione che lo faccio manualmente in combinazione con eval.

Ne leggi di più nei due articoli seguenti, i successivi si riferiscono anche al primo.

Generatori di armonia

Ora che i generatori sono finalmente atterrati in V8 e quindi in Node.js, sotto una bandiera ( --harmonyo --harmony-generators). Questi riducono notevolmente la quantità di inferno di callback che hai. Rende davvero eccezionale la scrittura di codice asincrono.

Il modo migliore per utilizzare i generatori è utilizzare una sorta di libreria del flusso di controllo. Ciò consentirà al flusso di continuare ad andare mentre si cede all'interno dei generatori.

Recap / Panoramica:

Se non hai familiarità con i generatori, è una pratica mettere in pausa l'esecuzione di funzioni speciali (chiamate generatori). Questa pratica si chiama cedere usando la yieldparola chiave.

Esempio:

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

Pertanto, ogni volta che si chiama questa funzione per la prima volta, verrà restituita una nuova istanza del generatore. Ciò consente di chiamare next()quell'oggetto per avviare o riprendere il generatore.

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

Continueresti a chiamare nextfino al doneritorno true. Questo significa che il generatore ha terminato completamente la sua esecuzione e non ci sono più yielddichiarazioni.

Controllo-Flow:

Come puoi vedere, il controllo dei generatori non è automatico. Devi continuare manualmente ciascuno. Ecco perché vengono utilizzate le librerie del flusso di controllo come co .

Esempio:

var co = require('co');

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

Ciò consente la possibilità di scrivere tutto in Node (e nel browser con Regenerator di Facebook che accetta, come input, il codice sorgente che utilizza generatori di armonia e divide il codice ES5 pienamente compatibile) con uno stile sincrono.

I generatori sono ancora piuttosto nuovi e quindi richiedono Node.js> = v11.2. Mentre scrivo, v0.11.x è ancora instabile e quindi molti moduli nativi sono rotti e lo saranno fino a v0.12, dove l'API nativa si calmerà.


Per aggiungere alla mia risposta originale:

Di recente ho preferito un'API più funzionale in JavaScript. La convenzione utilizza OOP dietro le quinte quando necessario, ma semplifica tutto.

Prendiamo ad esempio un sistema di visualizzazione (client o server).

view('home.welcome');

È molto più facile da leggere o seguire rispetto a:

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

La viewfunzione controlla semplicemente se la stessa vista esiste già in una mappa locale. Se la vista non esiste, creerà una nuova vista e aggiungerà una nuova voce alla mappa.

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

Estremamente semplice, vero? Trovo che semplifichi notevolmente l'interfaccia pubblica e ne renda più facile l'utilizzo. Impiego anche la capacità di catena ...

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

Tower, un framework che sto sviluppando (con qualcun altro) o sviluppando la prossima versione (0.5.0) utilizzerà questo approccio funzionale nella maggior parte delle sue interfacce di esposizione.

Alcune persone sfruttano le fibre come un modo per evitare "l'inferno di richiamata". È un approccio piuttosto diverso a JavaScript, e non ne sono un grande fan, ma molti framework / piattaforme lo usano; incluso Meteor, poiché trattano Node.js come una piattaforma thread / per connessione.

Preferirei usare un metodo astratto per evitare l'inferno di richiamata. Può diventare ingombrante, ma semplifica notevolmente il codice dell'applicazione reale. Aiutando a costruire il framework TowerJS , ha risolto molti dei nostri problemi, ma ovviamente avrai comunque un certo livello di callback, ma il nesting non è profondo.

// 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 esempio del nostro sistema di routing e "controller", attualmente in fase di sviluppo, sebbene abbastanza diverso dai tradizionali "rails-like". Ma l'esempio è estremamente potente e minimizza la quantità di callback e rende le cose abbastanza evidenti.

Il problema con questo approccio è che tutto è astratto. Nulla funziona così com'è e richiede un "framework" dietro di esso. Ma se questo tipo di funzionalità e stile di codifica è implementato in un framework, allora è una grande vittoria.

Per i modelli in JavaScript, dipende onestamente. L'ereditarietà è davvero utile solo quando si utilizza CoffeeScript, Ember o qualsiasi framework / infrastruttura di "classe". Quando ti trovi in ​​un ambiente JavaScript "puro", l'utilizzo della tradizionale interfaccia prototipo funziona come un incantesimo:

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

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

Ember.js ha iniziato, almeno per me, usando un approccio diverso alla costruzione di oggetti. Invece di costruire ogni metodo prototipo in modo indipendente, useresti un'interfaccia simile a un modulo.

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

Tutti questi sono diversi stili di "codifica", ma aggiungono alla tua base di codice.

Polimorfismo

Il polimorfismo non è ampiamente usato nel puro JavaScript, dove lavorare con l'ereditarietà e copiare il modello simile a "classe" richiede molto codice boilerplate.

Progettazione basata su eventi / componenti

I modelli basati su eventi e basati su componenti sono i vincitori IMO, o i più facili da lavorare, specialmente quando si lavora con Node.js, che ha un componente EventEmitter incorporato, sebbene l'implementazione di tali emettitori sia banale, è solo una bella aggiunta .

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

Solo un esempio, ma è un bel modello con cui lavorare. Soprattutto in un progetto orientato al gioco / ai componenti.

La progettazione dei componenti è un concetto separato di per sé, ma penso che funzioni estremamente bene in combinazione con i sistemi di eventi. I giochi sono tradizionalmente noti per la progettazione basata su componenti, in cui la programmazione orientata agli oggetti ti porta solo finora.

La progettazione basata su componenti ha i suoi usi. Dipende dal tipo di sistema dell'edificio. Sono sicuro che funzionerebbe con le app Web, ma funzionerebbe molto bene in un ambiente di gioco, a causa del numero di oggetti e di sistemi separati, ma sicuramente esistono altri esempi.

Pub / Sottotitoli

Event-binding e pub / sub è simile. Il modello pub / sub brilla davvero nelle applicazioni Node.js a causa del linguaggio unificante, ma può funzionare in qualsiasi lingua. Funziona molto bene in applicazioni in tempo reale, giochi, ecc.

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

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

Osservatore

Questo potrebbe essere soggettivo, poiché alcune persone scelgono di pensare al modello Observer come pub / sub, ma hanno le loro differenze.

"L'Observer è un modello di progettazione in cui un oggetto (noto come soggetto) mantiene un elenco di oggetti a seconda di esso (osservatori), notificandoli automaticamente di eventuali cambiamenti di stato." - Il modello di osservatore

Il modello di osservatore è un passo oltre i tipici pub / sottosistemi. Gli oggetti hanno relazioni strette o metodi di comunicazione tra loro. Un oggetto "Soggetto" manterrebbe un elenco di dipendenti "Osservatori". L'argomento manterrebbe aggiornati gli osservatori.

Programmazione reattiva

La programmazione reattiva è un concetto più piccolo e più sconosciuto, specialmente in JavaScript. Esiste un framework / libreria (di cui sono a conoscenza) che espone un'API di facile utilizzo per utilizzare questa "programmazione reattiva".

Risorse sulla programmazione reattiva:

Fondamentalmente, sta avendo una serie di dati di sincronizzazione (siano essi variabili, funzioni, ecc.).

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

 a = 2;

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

Credo che la programmazione reattiva sia notevolmente nascosta, specialmente nei linguaggi imperativi. È un paradigma di programmazione incredibilmente potente, specialmente in Node.js. Meteor ha creato il proprio motore reattivo su cui si basa sostanzialmente il framework. Come funziona la reattività di Meteor dietro le quinte? è un'ottima panoramica di come funziona internamente.

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

Questo verrà eseguito normalmente, visualizzando il valore di name, ma se lo cambiamo

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

Ri-output la visualizzazione console.log Hello Bob. Un esempio di base, ma è possibile applicare questa tecnica a modelli di dati e transazioni in tempo reale. È possibile creare sistemi estremamente potenti dietro questo protocollo.

Meteor di ...

Il pattern reattivo e il modello Observer sono abbastanza simili. La differenza principale è che il modello di osservatore descrive comunemente il flusso di dati con interi oggetti / classi e la programmazione reattiva descrive invece il flusso di dati verso proprietà specifiche.

Meteor è un ottimo esempio di programmazione reattiva. Il suo runtime è un po 'complicato a causa della mancanza di eventi nativi di modifica del valore in JavaScript (i proxy Harmony lo cambiano). Anche altri framework lato client, Ember.js e AngularJS utilizzano la programmazione reattiva (in parte).

I due quadri successivi utilizzano il modello reattivo, in particolare sui loro modelli (ovvero l'aggiornamento automatico). Angular.js utilizza una semplice tecnica di controllo sporco. Non chiamerei questa programmazione esattamente reattiva, ma è vicina, poiché il controllo sporco non è in tempo reale. Ember.js utilizza un approccio diverso. Uso della brace set()e get()metodi che consentono loro di aggiornare immediatamente in base ai valori. Con il loro runloop è estremamente efficiente e consente valori più dipendenti, dove angolare ha un limite teorico.

promesse

Non è una soluzione ai callback, ma elimina alcuni rientri e riduce al minimo le funzioni nidificate. Aggiunge anche una bella sintassi 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
});

È inoltre possibile diffondere le funzioni di callback in modo che non siano in linea, ma questa è un'altra decisione di progettazione.

Un altro approccio sarebbe quello di combinare eventi e promesse in cui si avrebbe una funzione per inviare gli eventi in modo appropriato, quindi le funzioni funzionali reali (quelle che hanno la vera logica al loro interno) si legerebbero a un evento particolare. Passeresti quindi il metodo dispatcher all'interno di ogni posizione di callback, tuttavia, dovresti elaborare alcuni nodi che verrebbero in mente, come parametri, sapendo a quale funzione spedire, ecc ...

Funzione singola funzione

Invece di avere un enorme casino di callback infernale, mantieni una singola funzione per un singolo compito e svolgi bene quel compito. A volte puoi anticipare te stesso e aggiungere più funzionalità all'interno di ciascuna funzione, ma chiediti: può diventare una funzione indipendente? Assegna un nome alla funzione e questo pulisce il rientro e, di conseguenza, elimina il problema dell'inferno di richiamata.

Alla fine, suggerirei di sviluppare o utilizzare un piccolo "framework", fondamentalmente solo una spina dorsale per la tua applicazione, e prendere tempo per fare astrazioni, decidere su un sistema basato su eventi o un "sacco di piccoli moduli che sono sistema "indipendente". Ho lavorato con diversi progetti Node.js in cui il codice era estremamente disordinato con l'inferno di callback in particolare, ma anche una mancanza di pensiero prima che iniziassero a scrivere codice. Prenditi il ​​tuo tempo per riflettere sulle diverse possibilità in termini di API e sintassi.

Ben Nadel ha pubblicato dei post sul blog davvero validi su JavaScript e alcuni schemi piuttosto severi e avanzati che potrebbero funzionare nella tua situazione. Alcuni buoni post che sottolineerò:

Inversione di controllo

Sebbene non sia esattamente correlato all'inferno del callback, può aiutarti nell'architettura generale, specialmente nei test unitari.

Le due sotto-versioni principali di inversione di controllo sono Iniezione delle dipendenze e Localizzatore di servizi. Trovo Service Locator come il più semplice in JavaScript, al contrario di Iniezione delle dipendenze. Perché? Principalmente perché JavaScript è un linguaggio dinamico e non esiste alcuna digitazione statica. Java e C #, tra gli altri, sono "conosciuti" per l'iniezione di dipendenze perché sei in grado di rilevare i tipi e hanno interfacce, classi, ecc. Integrate. Questo rende le cose abbastanza facili. Puoi, tuttavia, ricreare questa funzionalità all'interno di JavaScript, tuttavia, non sarà identica e un po 'confusa, preferisco usare un localizzatore di servizi all'interno dei miei sistemi.

Qualsiasi tipo di inversione di controllo disaccoppia drasticamente il codice in moduli separati che possono essere derisi o falsificati in qualsiasi momento. Hai progettato una seconda versione del tuo motore di rendering? Fantastico, basta sostituire la vecchia interfaccia con quella nuova. I localizzatori di servizi sono particolarmente interessanti con i nuovi Harmony Proxies, tuttavia, utilizzabili in modo efficace solo all'interno di Node.js, forniscono un'API più bella, anziché utilizzare Service.get('render');e invece Service.render. Attualmente sto lavorando su quel tipo di sistema: https://github.com/TheHydroImpulse/Ettore .

Sebbene la mancanza di tipizzazione statica (la tipizzazione statica sia una possibile ragione per gli usi efficaci nell'iniezione di dipendenza in Java, C #, PHP - Non è tipizzata staticamente, ma ha suggerimenti di tipo.) Potrebbe essere considerata un punto negativo, è possibile sicuramente trasformarlo in un punto di forza. Poiché tutto è dinamico, è possibile progettare un sistema statico "falso". In combinazione con un localizzatore di servizi, è possibile associare ciascun componente / modulo / classe / istanza 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 esempio semplicistico. Per un mondo reale, un utilizzo efficace, è necessario portare avanti questo concetto, ma potrebbe aiutare a disaccoppiare il sistema se si desidera davvero l'iniezione di dipendenza tradizionale. Potrebbe essere necessario giocherellare un po 'con questo concetto. Non ho pensato molto all'esempio precedente.

Model-View-Controller

Il modello più evidente e il più utilizzato sul web. Qualche anno fa, JQuery era di gran moda, e così sono nati i plugin JQuery. Non era necessario un framework completo sul lato client, basta usare jquery e alcuni plugin.

Ora, c'è una grande guerra di framework JavaScript lato client. La maggior parte dei quali utilizza il modello MVC e tutti lo usano in modo diverso. MVC non è sempre implementato allo stesso modo.

Se stai usando le interfacce prototipo tradizionali, potresti avere difficoltà a ottenere uno zucchero sintattico o una bella API quando lavori con MVC, a meno che tu non voglia fare un po 'di lavoro manuale. Ember.js risolve questo problema creando un sistema "class" / object ". Un controller potrebbe apparire come:

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

La maggior parte delle librerie lato client estende anche il modello MVC introducendo view-helpers (diventando view) e template (diventando view).


Nuove funzionalità JavaScript:

Questo sarà efficace solo se stai usando Node.js, ma è comunque prezioso. Questo discorso a NodeConf di Brendan Eich porta alcune nuove fantastiche funzionalità. La sintassi della funzione proposta, e in particolare la libreria js Task.js.

Questo probabilmente risolverà la maggior parte dei problemi con l'annidamento delle funzioni e porterà prestazioni leggermente migliori a causa della mancanza di sovraccarico della funzione.

Non sono troppo sicuro se V8 lo supporta in modo nativo, per ultimo ho verificato che era necessario abilitare alcuni flag, ma questo funziona in una porta di Node.js che utilizza SpiderMonkey .

Risorse extra:


2
Bel writeup. Personalmente non ho alcuna utilità per la MV? librerie. Abbiamo tutto il necessario per organizzare il nostro codice per app più grandi e complesse. Tutti mi ricordano troppo di Java e C # che cercavano di gettare le loro varie tende di merda su ciò che stava realmente accadendo nella comunicazione server-client. Abbiamo un DOM. Abbiamo delegazione di eventi. Abbiamo OOP. Posso associare i miei eventi ai cambiamenti di dati tyvm.
Erik Reppen,

2
"Invece di avere un enorme casino di callback infernale, mantieni una singola funzione per un singolo compito e svolgi bene quel compito." - Poesia.
CuriousWebDeveloper

1
Javascript ha attraversato un'era molto buia all'inizio della metà degli anni 2000, quando pochi hanno capito come scrivere applicazioni di grandi dimensioni utilizzandolo. Come dice @ErikReppen, se trovi un'applicazione JS che assomiglia a un'applicazione Java o C #, stai sbagliando.
Backpackcoder

3

Aggiungendo alla risposta Daniels:

Valori / componenti osservabili

Questa idea è presa in prestito dal framework MVVM Knockout.JS ( ko.observable ), con l'idea che valori e oggetti possono essere soggetti osservabili, e una volta che il cambiamento avviene in un valore o oggetto, aggiornerà automaticamente tutti gli osservatori. È fondamentalmente il modello di osservatore implementato in Javascript, e invece come viene implementata la maggior parte dei framework di pub / sub, la "chiave" è il soggetto stesso anziché un oggetto arbitrario.

L'utilizzo è il seguente:

// 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'idea è che gli osservatori di solito sanno dove si trova l'argomento e come si iscrivono ad esso. Il vantaggio di questo invece del pub / sub è evidente se si deve cambiare molto il codice in quanto è più facile rimuovere i soggetti da una fase di refactoring. Intendo questo perché una volta rimosso un argomento, tutti coloro che dipendevano da esso falliranno. Se il codice fallisce rapidamente, sai dove rimuovere i riferimenti rimanenti. Ciò è in contrasto con il soggetto completamente disaccoppiato (ad esempio con una chiave di stringa in pub / modello secondario) e ha una maggiore possibilità di rimanere nel codice soprattutto se sono stati utilizzati tasti dinamici e il programmatore di manutenzione non ne è stato informato (morto il codice nella programmazione della manutenzione è un problema fastidioso).

Nella programmazione del gioco, ciò riduce la necessità del vecchio ciclo di aggiornamento e altro ancora in un linguaggio di programmazione eventivo / reattivo, poiché non appena qualcosa viene cambiato il soggetto aggiornerà automaticamente tutti gli osservatori sulla modifica, senza dover attendere il ciclo di aggiornamento eseguire. Ci sono usi per il ciclo di aggiornamento (per cose che devono essere sincronizzate con il tempo di gioco trascorso), ma a volte non vuoi semplicemente ingombrarlo quando i componenti stessi possono aggiornarsi automaticamente con questo modello.

L'implementazione effettiva della funzione osservabile è in realtà sorprendentemente facile da scrivere e comprendere (specialmente se sai come gestire le matrici in javascript e il modello di osservatore ):

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

Ho fatto un'implementazione dell'oggetto osservabile in JsFiddle che continua su questo con l'osservazione componenti e di essere in grado di rimuovere gli abbonati. Sentiti libero di sperimentare il JsFiddle.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.