Per imparare con facilità ad usare le classi in Node.js occorre pensare ad un'applicazione che ci permetta di condensare i fondamenti di questa feature in modo completo.

Creeremo il gioco MasterMind in cui l'utente dovrà indovinare un numero compreso tra 100 e 999. In caso di errore mostreremo le cifre che l'utente ha indovinato e quelle errate.

Di quante classi abbiamo bisogno? Il dubbio riguarda l'opportunità di creare una classe monolitica o applicare la delegazione usando più classi.

Dato che in genere un progetto può evolversi nel tempo con l'aggiunta di nuove feature, l'approccio monolitico non è abbastanza flessibile.

Cominceremo quindi col creare una classe che genera il numero segreto.

'use strict';

class MMUtils {
    static randomInt(min = 100, max = 999) {
        let minimum = Math.ceil(min);
        let maximum = Math.floor(max);
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }

    static generateSecretCode() {
        return this.randomInt();

    }
};

module.exports = MMUtils;

Dato che questa è una classe di utility, definiremo dei metodi statici da usare nella classe principale. Um metodo statico ha un comportamento analogo a quello che abbiamo già visto con gli oggetti letterali: in breve, non è necessario istanziare la classe per usare questi metodi.

Nella classe principale dobbiamo inglobare la logica dell'applicazione. Innanzitutto, quando si lancia l'app dalla riga di comando, questa deve aver già generato il numero segreto. Quindi useremo il costruttore della classe per questo scopo.

'use strict';

const MMUtils = require('./MMUtils');

class MM {
    constructor() {
        this.secretCode = MMUtils.generateSecretCode();
    }
    //...
};

module.exports = MM;    

Ora implementiamo i metodi più semplici, ossia quello che verifica se l'utente ha indovinato e quello che mostra all'utente il numero corretto.

hasWon(playerCode) {
        return (this.secretCode === playerCode);
    }

    displayCode() {
        console.log(`Node code: ${this.secretCode}`);
    }

A questo punto dobbiamo trovare i numeri corretti inseriti dall'utente. Dobbiamo convertire il numero intero in un'array di tre caratteri per operare il confronto con il numero inserito dall'utente.

getCorrectNumbers(playerCode) {
        let output = [];
        let n = this.secretCode.toString().split('');
        let p = playerCode.toString().split('');
        for (let i = 0; i < n.length; i++) {
            for (let j = 0; j < p.length; j++) {
                if (n[i] === p[j]) {
                    output.push(n[i]);
                }
            }
        }
        return (output.length > 0) ? output.join('') : '';
    }

Ora dobbiamo procedere con l'operazione inversa, ossia individuare le cifre che non corrispondono al numero segreto. La logica della trasformazione in array di caratteri resta la stessa.

getWrongNumbers(playerCode) {
        let output = [];
        let n = this.secretCode.toString().split('');
        let p = playerCode.toString().split('');

        for (let i = 0; i < n.length; i++) {
            if (p[i] !== n[i]) {
                output.push(p[i]);
            }
        }
        return (output.length > 0) ? output.join('') : '';
    }

Dato che l'utente inserirà il numero da riga di comando, dobbiamo leggere e validare l'argomento che corrisponde al numero inserito.

getPlayerCode() {
        let arg = process.argv[2];
        if (!/^\d{3}$/.test(arg)) {
            throw new Error('Integer required!');
        } else {
            return arg;
        }
    }

Infine, eseguiamo le nostre routine nel metodo principale della nostra app.

run() {

        let playerCode = parseInt(this.getPlayerCode(), 10);

        if (this.hasWon(playerCode)) {
            console.log('You won!');
        } else {
            let correctNums = this.getCorrectNumbers(playerCode);
            let wrongNums = this.getWrongNumbers(playerCode);

            console.log(`Your code: ${playerCode}`);
            console.log(`Number(s) correct: ${correctNums}`);
            console.log(`Number(s) wrong: ${wrongNums}`);

            this.displayCode();
        }
    }

Il file principale della nostra app mostrerà quindi un codice compatto e coeso.

'use strict';

const MM = require('./lib/MM');

const app = new MM();

app.run();

Invece di avere un codice pieno di funzioni racchiuse in un unico file e di difficile lettura, abbiamo un'app divisa logicamente e un codice ordinato e dal design flessibile.