Sviluppo di JavaScript basato sui test in pratica

TDD è un processo di sviluppo iterativo in cui ogni iterazione inizia scrivendo un test che fa parte delle specifiche che stiamo implementando. Le brevi iterazioni consentono un feedback più immediato sul codice che stiamo scrivendo e le cattive decisioni di progettazione sono più facili da individuare. Scrivendo i test prima di qualsiasi codice di produzione, una buona copertura del test unitario arriva con il territorio, ma questo è solo un effetto collaterale positivo.

Tutorial ripubblicato

Ogni poche settimane, rivisitiamo alcuni dei post preferiti del nostro lettore da tutta la cronologia del sito. Questo tutorial è stato pubblicato per la prima volta nel novembre del 2010.


Turning Development upside-down

Nella programmazione tradizionale, i problemi vengono risolti programmando finché un concetto non è completamente rappresentato nel codice. Idealmente, il codice segue alcune considerazioni generali sulla progettazione architettonica, anche se in molti casi, forse soprattutto nel mondo di JavaScript, questo non è il caso. Questo stile di programmazione risolve i problemi indovinando a quale codice è richiesto per risolverli, una strategia che può facilmente portare a soluzioni gonfiate e strettamente accoppiate. Se non ci sono anche test unitari, le soluzioni prodotte con questo approccio possono anche contenere codice che non viene mai eseguito, come la logica di gestione degli errori e la gestione degli argomenti "flessibile", oppure può contenere casi limite che non sono stati testati a fondo, se testati affatto.

Lo sviluppo guidato dai test trasforma il ciclo di sviluppo a testa in giù. Piuttosto che concentrarsi su quale codice è necessario per risolvere un problema, lo sviluppo basato sui test inizia definendo l'obiettivo. I test unitari costituiscono sia la specifica che la documentazione per le azioni supportate e contabilizzate. Certo, l'obiettivo di TDD non è il test e quindi non vi è alcuna garanzia che gestisca ad es. casi limite Tuttavia, poiché ogni riga di codice viene testata da un pezzo rappresentativo di codice di esempio, è probabile che TDD produca meno codice in eccesso e la funzionalità che viene considerata è probabilmente più robusta. Lo sviluppo corretto basato sui test assicura che un sistema non contenga mai codice che non viene eseguito.


Il processo

Il processo di sviluppo basato su test è un processo iterativo in cui ogni iterazione consiste dei seguenti quattro passaggi:

  • Scrivi un test
  • Esegui test, guarda il nuovo test fallire
  • Fai il test
  • Refactor per rimuovere la duplicazione

In ogni iterazione, il test è la specifica. Una volta che è stato scritto un codice di produzione sufficiente (e non più) per fare passare il test, abbiamo finito, e potremmo refactoring il codice per rimuovere la duplicazione e / o migliorare il design, purché i test continuino a passare.


TDD pratico: il modello di osservatore

Il pattern Observer (noto anche come Pubblica / Iscriviti o semplicemente PubSub) è un modello di progettazione che ci consente di osservare lo stato di un oggetto e di essere avvisati quando cambia. Il modello può fornire oggetti con punti di estensione potenti pur mantenendo un accoppiamento lento.

Ci sono due ruoli in The Observer: osservabile e osservatore. L'osservatore è un oggetto o una funzione che verrà notificato quando cambia lo stato dell'osservabile. L'osservabile decide quando aggiornare i suoi osservatori e quali dati fornirli. L'osservabile fornisce in genere almeno due metodi pubblici: PubSub, che notifica i suoi osservatori di nuovi dati, e PubSub che sottoscrive gli osservatori agli eventi.


The Observable Library

Lo sviluppo basato sui test ci consente di muoverci in piccoli passi quando necessario. In questo primo esempio del mondo reale inizieremo con il più piccolo dei passi. Man mano che acquisiamo sicurezza nel nostro codice e nel processo, aumenteremo gradualmente la dimensione dei nostri passaggi quando le circostanze lo consentiranno (ad esempio, il codice da implementare è abbastanza banale). Scrivere codice in piccole iterazioni frequenti ci aiuterà a progettare le nostre API pezzo per pezzo e ci aiuterà a commettere meno errori. Quando si verificano degli errori, saremo in grado di risolverli rapidamente poiché gli errori saranno facili da rintracciare quando eseguiremo i test ogni volta che aggiungiamo una manciata di righe di codice.


Impostazione dell'ambiente

Questo esempio utilizza JsTestDriver per eseguire i test. Una guida di installazione è disponibile dal sito web ufficiale.

Il layout del progetto iniziale ha il seguente aspetto:

 chris @ laptop: ~ / projects / osservabile $ tree. | - jsTestDriver.conf | - src | '- observable.js' - test '- observable_test.js

Il file di configurazione è solo il minimo JsTestDriver configurazione:

 server: http: // localhost: 4224 carico: - lib / *. js - test / *. js

Aggiunta di osservatori

Daremo il via al progetto implementando un mezzo per aggiungere osservatori a un oggetto. Fare così ci porterà a scrivere il primo test, a guardarlo fallire, a passarlo nel modo più sporco possibile e infine a rifarlo in qualcosa di più sensato.


Il primo test

Il primo test tenterà di aggiungere un osservatore chiamando il addObserver metodo. Per verificare che funzioni, saremo schietti e supponiamo che osservabile memorizzi i suoi osservatori in una matrice e controlli che l'osservatore sia l'unico elemento in quella matrice. Il test appartiene a test / observable_test.js e assomiglia al seguente:

 TestCase ("ObservableAddObserverTest", "test dovrebbe memorizzare la funzione": function () var observable = new tddjs.Observable (); var observer = function () ; observable.addObserver (observer); assertEquals (observer, observable. osservatori [0]););

Esecuzione del test e controllo fallito

A prima vista, il risultato dell'esecuzione del nostro primo test è devastante:

 Totale 1 test (superato: 0; errore: 0; errori: 1) (0,00 ms) Firefox 3.6.12 Linux: eseguire 1 test (superato: 0; errore: 0; errori 1) (0,00 ms) ObservableAddObserverTest.test deve memorizzare errore di funzione (0,00 ms): \ tddjs non è definito /test/observable_test.js:3 Test non riusciti.

Fare il test di prova

Non aver paura! Il fallimento è in realtà una buona cosa: ci dice dove concentrare i nostri sforzi. Il primo problema serio è che tddjs non esiste. Aggiungiamo l'oggetto namespace in src / observable.js:

 var tddjs = ;

L'esecuzione dei test genera nuovamente un nuovo errore:

 E Totale 1 test (superato: 0; errore: 0; errori: 1) (0,00 ms) Firefox 3.6.12 Linux: eseguire 1 test (superato: 0; errore: 0; errori 1) (0,00 ms) ObservableAddObserverTest.test dovrebbe Errore funzione archivio (0,00 ms): \ tddjs.Observable non è un costruttore /test/observable_test.js:3 Test non riusciti.

Possiamo risolvere questo nuovo problema aggiungendo un costruttore Observable vuoto:

 var tddjs = ; (function () function Observable ()  tddjs.Observable = Observable; ());

Eseguire il test ancora una volta ci porta direttamente al problema successivo:

 E Totale 1 test (superato: 0; errore: 0; errori: 1) (0,00 ms) Firefox 3.6.12 Linux: eseguire 1 test (superato: 0; errore: 0; errori 1) (0,00 ms) ObservableAddObserverTest.test dovrebbe Errore funzione archivio (0,00 ms): \ observable.addObserver non è una funzione /test/observable_test.js:6 Test non riusciti.

Aggiungiamo il metodo mancante.

 function addObserver ()  Observable.prototype.addObserver = addObserver;

Con il metodo in atto, il test ora fallisce al posto di un array di osservatori mancante.

 E Totale 1 test (superato: 0; errore: 0; errori: 1) (0,00 ms) Firefox 3.6.12 Linux: eseguire 1 test (superato: 0; errore: 0; errori 1) (0,00 ms) ObservableAddObserverTest.test dovrebbe Errore funzione archivio (0,00 ms): \ observable.observers è indefinito /test/observable_test.js:8 Test non riusciti.

Per quanto possa sembrare strano, definirò l'array di osservatori all'interno di PubSub metodo. Quando un test fallisce, TDD ci istruisce a fare la cosa più semplice che possa funzionare, non importa quanto sia sporco. Avremo la possibilità di rivedere il nostro lavoro una volta superato il test.

 function addObserver (observer) this.observers = [observer];  Successo! Il test ora passa:. Totale 1 test (superato: 1; errore: 0; errori: 0) (1,00 ms) Firefox 3.6.12 Linux: eseguire 1 test (superato: 1; errore: 0; errori 0) (1,00 ms)

refactoring

Durante lo sviluppo della soluzione attuale abbiamo intrapreso il percorso più veloce possibile per superare un test. Ora che la barra è verde, possiamo esaminare la soluzione ed eseguire qualsiasi refactoring che riteniamo necessario. L'unica regola in questo ultimo passo è mantenere la barra verde. Ciò significa che dovremo effettuare il refactoring anche in piccoli passaggi, assicurandoci di non rompere accidentalmente nulla.

L'attuale implementazione ha due problemi che dovremmo affrontare. Il test formula ipotesi dettagliate sull'implementazione di Observable e il addObserver l'implementazione è codificata per il nostro test.

Affronteremo prima l'hard-coding. Per esporre la soluzione codificata, aumenteremo il test per aggiungere due osservatori invece di uno.

 "test dovrebbe memorizzare funzione": function () var observable = new tddjs.Observable (); var observers = [function () , function () ]; observable.addObserver (osservatori [0]); observable.addObserver (osservatori [1]); assertEquals (observers, observable.observers); 

Come previsto, il test ora fallisce. Il test si aspetta che le funzioni aggiunte come osservatori si impilino come qualsiasi elemento aggiunto ad un PubSub. Per ottenere ciò, sposteremo l'istanziazione dell'array nel costruttore e semplicemente delegheremo addObserver al schieramento metodo push:

 function Observable () this.observers = [];  function addObserver (observer) this.observers.push (observer); 

Con questa implementazione in atto, il test passa nuovamente, dimostrando che ci siamo occupati della soluzione codificata. Tuttavia, il problema dell'accesso a una proprietà pubblica e di formulare ipotesi selvagge sull'implementazione di Observable è ancora un problema. Un osservabile PubSub dovrebbe essere osservabile da un numero qualsiasi di oggetti, ma non interessa agli estranei come o dove li memorizza l'osservabile. Idealmente, vorremmo essere in grado di verificare con l'osservabile se un determinato osservatore è registrato senza brancolare intorno al suo interno. Prendiamo nota dell'odore e andiamo avanti. Più tardi, torneremo per migliorare questo test.


Controllo per gli osservatori

Aggiungeremo un altro metodo a Observable, hasObserver, e usarlo per rimuovere parte della confusione che abbiamo aggiunto durante l'implementazione addObserver.


Il test

Un nuovo metodo inizia con un nuovo test e il successivo comportamento desiderato per hasObserver metodo.

 TestCase ("ObservableHasObserverTest", "test dovrebbe restituire true quando ha observer": function () var observable = new tddjs.Observable (); var observer = function () ; observable.addObserver (observer); assertTrue (osservabile .hasObserver (observer)););

Ci aspettiamo che questo test fallisca di fronte a una mancanza hasObserver, che fa.


Fare il test di prova

Ancora una volta, utilizziamo la soluzione più semplice che potrebbe superare il test corrente:

 function hasObserver (observer) return true;  Observable.prototype.hasObserver = hasObserver;

Anche se sappiamo che questo non risolverà i nostri problemi a lungo termine, mantiene i test verdi. Cercare di fare una revisione e un refactoring ci lascia a mani vuote perché non ci sono punti ovvi in ​​cui possiamo migliorare. I test sono i nostri requisiti e al momento richiedono solo hasObserver per restituire vero Per risolvere il problema, introdurremo un altro test che si aspetta hasObserver a restituisce falso per un osservatore inesistente, che può aiutare a forzare la vera soluzione.

 "test dovrebbe restituire false quando non ci sono osservatori": function () var observable = new tddjs.Observable (); assertFalse (observable.hasObserver (function () )); 

Questo test fallisce miseramente, dato che hasObserver sempre ritorna vero, costringendoci a produrre la vera implementazione. Controllare se un osservatore è registrato è una semplice questione di controllare che l'array this.observers contenga l'oggetto originariamente passato addObserver:

 function hasObserver (observer) return this.observers.indexOf (observer)> = 0; 

Il Array.prototype.indexOf metodo restituisce un numero inferiore a 0 se l'elemento non è presente nel schieramento, quindi controllando che restituisca un numero uguale o maggiore di 0 ci dirà se l'osservatore esiste.


Risoluzione delle incompatibilità del browser

Eseguire il test in più di un browser produce risultati un po 'sorprendenti:

 chris @ laptop: ~ / projects / observable $ jstestdriver --tests all ... E Total 4 test (superato: 3; Fails: 0; Errors: 1) (11,00 ms) Firefox 3.6.12 Linux: Esegui 2 test (superato: 2 ; Errore: 0; Errori 0) (2,00 ms) Microsoft Internet Explorer 6.0 Windows: Esegui 2 test \ (Passato: 1; Errore: 0; Errori 1) (0,00 ms) ObservableHasObserverTest.test dovrebbe restituire true quando ha errore observer \ ( 0,00 ms): l'oggetto non supporta questa proprietà o metodo Test non riusciti.

Le versioni di Internet Explorer 6 e 7 non hanno superato il test con i loro messaggi di errore più generici: "L'oggetto non supporta questa proprietà o metodo ". Questo può indicare un numero qualsiasi di problemi:

  • stiamo chiamando un metodo su un oggetto che è null
  • stiamo chiamando un metodo che non esiste
  • stiamo accedendo a una proprietà che non esiste

Fortunatamente, TDD-in pochi passi, sappiamo che l'errore deve riguardare la chiamata aggiunta di recente a indice di sui nostri osservatori schieramento. Come risulta, IE 6 e 7 non supportano il metodo JavaScript 1.6 Array.prototype.indexOf (per il quale non possiamo davvero biasimarlo, solo di recente è stato standardizzato con ECMAScript 5, dicembre 2009). A questo punto, abbiamo tre opzioni:

  • Evita l'uso di Array.prototype.indexOf in hasObserver, duplicando efficacemente la funzionalità nativa nei browser di supporto.
  • Implementare Array.prototype.indexOf per i browser non di supporto. In alternativa, implementa una funzione di supporto che fornisce la stessa funzionalità.
  • Utilizzare una libreria di terze parti che fornisce il metodo mancante o un metodo simile.

Quale di questi approcci è più adatto a risolvere un determinato problema dipenderà dalla situazione: tutti hanno i loro pro e contro. Nell'interesse di mantenere l'osservabile autosufficiente, implementeremo semplicemente hasObserver in termini di un ciclo al posto del indice di chiama, efficacemente aggirando il problema. Per inciso, anche questa sembra essere la cosa più semplice che potrebbe funzionare a questo punto. Se dovessimo imbattersi in una situazione simile più avanti, saremmo invitati a riconsiderare la nostra decisione. L'aggiornato hasObserver sembra come segue:

 function hasObserver (observer) for (var i = 0, l = this.observers.length; i < l; i++)  if (this.observers[i] == observer)  return true;   return false; 

refactoring

Con la barra di nuovo verde, è tempo di rivedere i nostri progressi. Ora abbiamo tre test, ma due sembrano stranamente simili. Il primo test che abbiamo scritto per verificare la correttezza di addObserver fondamentalmente prova per le stesse cose del test che abbiamo scritto per verificare refactoring . Ci sono due differenze chiave tra i due test: il primo test è stato precedentemente dichiarato maleodorante, in quanto accede direttamente alla matrice di osservatori all'interno dell'oggetto osservabile. Il primo test aggiunge due osservatori, assicurando che siano entrambi aggiunti. Ora possiamo partecipare ai test in uno che verifica che tutti gli osservatori aggiunti all'osservabile siano effettivamente aggiunti:

 "test dovrebbe memorizzare funzioni": function () var observable = new tddjs.Observable (); var observers = [function () , function () ]; observable.addObserver (osservatori [0]); observable.addObserver (osservatori [1]); assertTrue (observable.hasObserver (osservatori [0])); assertTrue (observable.hasObserver (osservatori [1])); 

Osservatori Notificanti

Aggiungere osservatori e verificare la loro esistenza è bello, ma senza la possibilità di notificarli di cambiamenti interessanti, Observable non è molto utile. È ora di implementare il metodo di notifica.


Assicurare che gli osservatori siano chiamati

L'attività più importante notificata esegue chiama tutti gli osservatori. Per fare ciò, abbiamo bisogno di un modo per verificare che un osservatore sia stato chiamato dopo il fatto. Per verificare che una funzione sia stata chiamata, possiamo impostare una proprietà sulla funzione quando viene chiamata. Per verificare il test possiamo verificare se la proprietà è impostata. Il seguente test utilizza questo concetto nel primo test per la notifica.

 TestCase ("ObservableNotifyTest", "test dovrebbe chiamare tutti gli osservatori": function () var observable = new tddjs.Observable (); var observer1 = function () observer1.called = true;; var observer2 = function () observer2.called = true;; observable.addObserver (observer1); observable.addObserver (observer2); observable.notify (); assertTrue (observer1.called); assertTrue (observer2.called););

Per superare il test, è necessario eseguire il loop della matrice di osservatori e chiamare ciascuna funzione:

 function notify () for (var i = 0, l = this.observers.length; i < l; i++)  this.observers[i]();   Observable.prototype.notify = notify;

Passando argomenti

Attualmente vengono chiamati gli osservatori, ma non vengono dati loro dati. Sanno che qualcosa è successo - ma non necessariamente cosa. Faremo notifica prendere un numero qualsiasi di argomenti, semplicemente passandoli insieme a ciascun osservatore:

 "test dovrebbe passare attraverso argomenti": function () var observable = new tddjs.Observable (); var effettivo; observable.addObserver (function () actual = arguments;); observable.notify ("String", 1, 32); assertEquals (["String", 1, 32], actual); 

Il test confronta gli argomenti ricevuti e passati assegnando gli argomenti ricevuti ad una variabile locale al test. L'osservatore che abbiamo appena creato è in realtà una spia di test manuale molto semplice. Esecuzione del test conferma che fallisce, il che non è sorprendente dato che attualmente non stiamo toccando gli argomenti all'interno di notify.

Per superare il test possiamo usare apply quando si chiama l'osservatore:

 function notify () for (var i = 0, l = this.observers.length; i < l; i++)  this.observers[i].apply(this, arguments);  

Con questa semplice correzione, i test tornano al verde. Nota che abbiamo inviato questo come primo argomento da applicare, nel senso che gli osservatori saranno chiamati con l'osservabile come questo.


Gestione degli errori

A questo punto Osservabile è funzionale e abbiamo test che ne verificano il comportamento. Tuttavia, i test verificano solo che gli osservabili si comportino correttamente in risposta all'input previsto. Cosa succede se qualcuno tenta di registrare un oggetto come osservatore al posto di una funzione? Cosa succede se uno degli osservatori esplode? Queste sono domande a cui abbiamo bisogno che i nostri test rispondano. Assicurare un comportamento corretto nelle situazioni previste è importante - questo è ciò che i nostri oggetti faranno la maggior parte del tempo. Almeno così potremmo sperare. Tuttavia, un comportamento corretto anche quando il cliente si comporta male è altrettanto importante per garantire un sistema stabile e prevedibile.


Aggiunta di osservatori del bogus

L'attuale implementazione accetta ciecamente qualsiasi tipo di argomento per addObserver. Sebbene la nostra implementazione possa utilizzare qualsiasi funzione come osservatore, non può gestire alcun valore. Il seguente test si aspetta che l'osservabile lanci un'eccezione quando si tenta di aggiungere un osservatore che non è richiamabile.

 "il test deve essere lanciato per l'osservatore non controllabile": function () var observable = new tddjs.Observable (); assertException (function () observable.addObserver ();, "TypeError"); 

Lanciando un'eccezione già quando si aggiungono gli osservatori non è necessario preoccuparsi di dati non validi in seguito quando si notificano gli osservatori. Se avessimo programmato per contratto, potremmo dire che una precondizione per il addObserver il metodo è che l'input deve essere chiamabile. Il postcondizione è che l'osservatore è aggiunto all'osservabile ed è garantito per essere chiamato una volta che le chiamate osservabili notificano.

Il test fallisce, quindi spostiamo l'attenzione per riportare la barra verde il più rapidamente possibile. Sfortunatamente, non c'è modo di simulare l'implementazione: lanciare un'eccezione su ogni chiamata a addObserver fallirà tutti gli altri test. Fortunatamente, l'implementazione è abbastanza banale:

 function addObserver (observer) if (typeof observer! = "function") lanciare un nuovo TypeError ("observer is not function");  this.observers.push (observer); 

addObserver ora controlla che l'osservatore sia effettivamente una funzione prima di aggiungerla alla lista. Eseguendo i test si ottiene quella dolce sensazione di successo: tutto verde.


Osservatori inadeguati

L'osservabile ora garantisce che ogni osservatore aggiunto addObserver è chiamabile Tuttavia, la notifica può ancora fallire in modo orribile se un osservatore lancia un'eccezione. Il prossimo test si aspetta che tutti gli osservatori vengano chiamati anche se uno di loro lancia un'eccezione.

 "il test dovrebbe notificare tutto anche quando alcuni falliscono": function () var observable = new tddjs.Observable (); var observer1 = function () throw new Error ("Oops"); ; var observer2 = function () observer2.called = true; ; observable.addObserver (Observer1); observable.addObserver (observer2); observable.notify (); assertTrue (observer2.called); 

L'esecuzione del test rivela che l'attuale implementazione esplode insieme al primo osservatore, causando il non richiamo del secondo osservatore. In effetti, la notifica sta infrangendo la sua garanzia che chiamerà sempre tutti gli osservatori una volta che sono stati aggiunti con successo. Per rettificare la situazione, il metodo deve essere preparato al peggio:

 function notify () for (var i = 0, l = this.observers.length; i < l; i++)  try  this.observers[i].apply(this, arguments);  catch (e)   

L'eccezione viene silenziosamente scartata. È responsabilità dell'osservatore assicurarsi che ogni errore sia gestito correttamente, l'osservatore stia semplicemente respingendo gli osservatori che si comportano male..


Documentare l'ordine delle chiamate

Abbiamo migliorato la robustezza del modulo Osservabile dandogli la corretta gestione degli errori. Il modulo è ora in grado di fornire garanzie di funzionamento finché riceve un buon input ed è in grado di recuperare se un osservatore non riesce a soddisfare i suoi requisiti. Tuttavia, l'ultimo test che abbiamo aggiunto fa un'ipotesi sulle caratteristiche non documentate dell'osservabile: presuppone che gli osservatori siano chiamati nell'ordine in cui sono stati aggiunti. Attualmente questa soluzione funziona perché abbiamo utilizzato un array per implementare l'elenco degli osservatori. Se dovessimo decidere di cambiare questo, tuttavia, i nostri test potrebbero interrompersi. Quindi dobbiamo decidere: facciamo il refactoring del test per non assumere l'ordine delle chiamate, o semplicemente aggiungiamo un test che si aspetta l'ordine delle chiamate - documentando così l'ordine delle chiamate come una caratteristica? L'ordine di chiamata sembra una caratteristica sensata, quindi il nostro prossimo test farà in modo che Observable mantenga questo comportamento.

 "test dovrebbe chiamare osservatori nell'ordine in cui sono stati aggiunti": function () var observable = new tddjs.Observable (); var calls = []; var observer1 = function () calls.push (observer1); ; var observer2 = function () calls.push (observer2); ; observable.addObserver (Observer1); observable.addObserver (observer2); observable.notify (); assertEquals (observer1, chiama [0]); assertEquals (observer2, chiama [1]); 

Poiché l'implementazione utilizza già un array per gli osservatori, questo test ha esito positivo.


Osservazione di oggetti arbitrari

Nei linguaggi statici con ereditarietà classica, gli oggetti arbitrari sono resi osservabili da sottoclassi la classe osservabile. La motivazione dell'ereditarietà classica in questi casi deriva dal desiderio di definire la meccanica del modello in un posto e di riutilizzare la logica attraverso vaste quantità di oggetti non correlati. In JavaScript, abbiamo diverse opzioni per il riutilizzo del codice tra gli oggetti, quindi non dobbiamo limitarci a un'emulazione del modello di ereditarietà classica.

Nell'interesse di liberarsi dall'emulazione classica fornita dai costruttori, si consideri i seguenti esempi che assumono che tddjs.observable sia un oggetto piuttosto che un costruttore:

Notare la tddjs.extend il metodo viene introdotto altrove nel libro e copia semplicemente le proprietà da un oggetto a un altro.

 // Creazione di un singolo oggetto osservabile var observable = Object.create (tddjs.util.observable); // Estensione di un singolo oggetto tddjs.extend (giornale, tddjs.util.observable); // Un costruttore che crea oggetti osservabili funzione Newspaper () / * ... * / Newspaper.prototype = Object.create (tddjs.util.observable); // Estensione di un prototipo esistente tddjs.extend (Newspaper.prototype, tddjs.util.observable);

Semplicemente implementando l'osservabile come un singolo oggetto offre una grande flessibilità. Per arrivarci abbiamo bisogno di rifattorizzare la soluzione esistente per sbarazzarsi del costruttore.


Rendere il Costruttore obsoleto

Per eliminare il costruttore, dovremmo prima effettuare il refactoring Observable in modo tale che il costruttore non lavori. Fortunatamente, il costruttore inizializza solo l'array di osservatori, che non dovrebbe essere troppo difficile da rimuovere. Tutti i metodi su Observable.prototype accedono all'array, quindi dobbiamo assicurarci che tutti possano gestire il caso in cui non è stato inizializzato. Per testare questo, abbiamo semplicemente bisogno di scrivere un test per metodo che chiama il metodo in questione prima di fare qualsiasi altra cosa.

Dato che abbiamo già test che chiamano addObserver e hasObserver prima di fare qualsiasi altra cosa, ci concentreremo sul metodo di notifica. Questo metodo viene testato solo dopo addObserver è stato chiamato. I nostri prossimi test si aspettano che sia possibile chiamare questo metodo prima di aggiungere qualsiasi osservatore.

 "test non dovrebbe fallire se non ci sono osservatori": function () var observable = new tddjs.Observable (); assertNoException (function () observable.notify ();); 

Con questo test in atto possiamo svuotare il costruttore:

 funzione Observable () 

L'esecuzione dei test mostra che tutti tranne uno ora stanno fallendo, tutti con lo stesso messaggio: "this.observers non è definito". Ci occuperemo di un metodo alla volta. Il primo è addObserver metodo:

function addObserver (observer)
se (! this.observers)
this.observers = [];

/ * ... * /

Eseguendo nuovamente i test si rivela che l'aggiornamento addObserver il metodo corregge tutti tranne i due test che non iniziano chiamandolo. In seguito, ci assicureremo di restituire il falso direttamente da hasObserver se la matrice non esiste.

 function hasObserver (observer) if (! this.observers) return false;  / * ... * /

Possiamo applicare la stessa esatta soluzione per notificare:

 function notify (observer) if (! this.observers) return;  / * ... * /

Sostituzione del Costruttore con un oggetto

Ora che il costruttore non fa nulla che possa essere rimosso in sicurezza. Quindi aggiungeremo tutti i metodi direttamente a tddjs.observable oggetto, che può quindi essere usato con ad es. Object.create o tddjs.extend per creare oggetti osservabili. Nota che il nome non è più in maiuscolo poiché non è più un costruttore. L'implementazione aggiornata segue:

 (function () function addObserver (observer) / * ... * / function hasObserver (observer) / * ... * / function notify () / * ... * / tddjs.observable = addObserver: addObserver, hasObserver : hasObserver, notify: notify; ());

Sicuramente, la rimozione del costruttore causa la rottura di tutti i test. Ripararli è facile, tuttavia. Tutto quello che dobbiamo fare è sostituire la nuova affermazione con una chiamata a Object.create. Tuttavia, la maggior parte dei browser non supporta Object.create ancora, così possiamo farlo a pezzi. Poiché non è possibile emulare perfettamente il metodo, forniremo la nostra versione su tddjs oggetto:

 (function () function F ()  tddjs.create = function (object) F.prototype = object; return new F ();; / * L'implementazione osservabile va qui ... * / ());

Con lo shim sul posto, possiamo aggiornare i test in una questione che funzionerà anche nei vecchi browser. La suite di test finale segue:

 TestCase ("ObservableAddObserverTest", setUp: function () this.observable = tddjs.create (tddjs.observable);, "test dovrebbe memorizzare funzioni": function () var observers = [function () , function () ]; this.observable.addObserver (observers [0]); this.observable.addObserver (observers [1]); assertTrue (this.observable.hasObserver (observers [0])); assertTrue (this.observable .hasObserver (observers [1]));); TestCase ("ObservableHasObserverTest", setUp: function () this.observable = tddjs.create (tddjs.observable);, "test dovrebbe restituire false quando nessun osservatore": function () assertFalse (this.observable.hasObserver ( funzione () ));  ); TestCase ("ObservableNotifyTest", setUp: function () this.observable = tddjs.create (tddjs.observable);, "test dovrebbe chiamare tutti gli osservatori": function () var observer1 = function () observer1.called = true;; var observer2 = function () observer2.called = true;; this.observable.addObserver (observer1); this.observable.addObserver (observer2); this.observable.notify (); assertTrue (observer1. chiamato); assertTrue (observer2.called);, "il test deve passare attraverso gli argomenti": function () var actual; this.observable.addObserver (function () actual = arguments;); this.observab