Code, Learn

D.D.D in TypeScript: Model, Ubiquitous Language e Value Object

7 Aprile 2020 - 7 minuti di lettura

Nelle ultime settimane Gabor Heves è rimasto affascinato dall’approccio Domain-Driven Design, o D.D.D. di Eric Evans guardando alcune presentazioni su Youtube e leggendo articoli sull’argomento. Notando che la maggior parte degli esempi mostrano implementazioni in Java, sorge una domanda: funziona anche con TypeScript?

TypeScript è un linguaggio molto simile a Java in quanto dipende fortemente da tipizzazioni, interfacce, classi e oggetti ben definiti anche se il prodotto finale dopo la compilazione del codice sarà JavaScript. Molto probabilmente in rete si trovano esempi anche per TypeScript, ma Gabor ha evitato di proposito di cercarne.

Per la rubrica “Cosa abbiamo imparato?” del mese di Marzo, Gabor in questo articolo parlerà di D.D.D. in TypeScript, toccando i concetti di Model, Ubiquitous Language  e Value Object utili per modellare una classe.

D.D.D – facciamo chiarezza sui concetti

Iniziamo con spiegare ognuno dei concetti precedentemente introdotti (fonti dalla pagina D.D.D di Wikipedia).

Model

Un sistema di astrazioni che descrive aspetti selezionati di un dominio e può essere utilizzato per risolvere problemi relativi a quel dominio.

Ubiquitous Language

Un linguaggio strutturato attorno al modello di dominio e utilizzato da tutti i membri del team per collegare tutte le attività del team con il software.

Value Object

Un oggetto che contiene attributi ma non ha identità concettuale. Dovrebbero essere trattati come immutabili. Inoltre, un Value Object dovrebbe essere in grado di auto-validarsi.

D.D.D. in TypeScript – il caso d’uso dei numeri IBAN

Scegliamo un modello su cui vogliamo lavorare. Mi piacerebbe applicare il concetto di Value Object su qualcosa di cui non ho conoscenze da esperto di dominio, o domain expert. Prendo come esempio i numeri IBAN. Ne ho sentito parlare, li ho usati per inviare e ricevere denaro ma non ho mai approfondito l’argomento.

Non avendo a disposizione un esperto di dominio che sappia cos’è un IBAN, ricorro all’uso di Wikipedia.
Ho controllato la pagina italiana ma non è stato sufficiente per descrivere il modello così com’è, mancano alcune informazioni che torneranno utili. La prima cosa che  ho imparato è: usare direttamente la pagina Wikipedia in lingua inglese per ottenere informazioni sul dominio.

Prendendo spunto dalla pagina Wikipedia,  IBAN è in realtà un’abbreviazione di International Bank Account Number. E’ plausibile pensare di usare un numero per il modello? Non proprio. Leggendo un po’ più a fondo dove viene spiegata la struttura, scopro che un IBAN ha un’anatomia ben definita, composta da tre parti: codice paese, cifre di controllo e un numero di conto bancario di base (BBAN). Di seguito alcuni esempi:

  • Belgio: BE71 0961 2345 6769
  • Francia: FR76 3000 6000 0112 3456 7890 189
  • Regno Unito: GB98 MIDL 0700 9312 3456 78

Ok, non posso usare un numero, forse una stringa? Non proprio. Gli esempi sopra riportati sono in effetti stringhe, ma sono la rappresentazione in stringhe dell’oggetto, il valore di ritorno della funzione iban.toString(). Se avessi un oggetto. Quindi creo qualcosa che descriva questo oggetto.

D.D.D in TypeScript – iniziamo a modellare la classe IBAN

Ho due opzioni, creare un’interfaccia o una classe. L’interfaccia sarebbe facile da implementare, ma conterrebbe solo le tre definizioni basiche per l’oggetto: il codice paese, le cifre di controllo e il BBAN. Inoltre, vale la pena di calcolare che un IBAN è esattamente qualcosa che possiamo usare Value Object. Immutabile? Sì, se una qualunque modifica al valore di un IBAN porta ad un IBAN diverso. Dovrebbe essere (auto) validato? Sì. Ok, allora creo una classe per il Value Object.

export class Iban {
 constructor() {
 }
}

Bene, ho creato una classe che si chama Iban. Avrei potuto scegliere InternationalBankAccountNumber come nome, ma Iban è sufficientemente descrittivo. Abbastanza sicuro che un domain expert potrebbe capirebbe (ricordiamoci, Ubiquitous language).

Ricordiamo i tre valori che descrivono l’oggetto IBAN (li riporto in inglese, dopotutto si programma in lingua inglese): country code, check digits e BBAN. Tornando alla pagina Wikipedia, il country code è composto da due lettere che si riferiscono al paese dell’IBAN in un formato specifico (ISO-3166). Ok, aggiorniamo il codice:

private readonly _countryCode: string;
public get countryCode(): string {
  return this._countryCode;
}

Un paio di considerazioni prima di procedere.
Perché readonly? Perché ricordiamolo, il Value Object è immutabile, quindi il country code settato non verrà mai modificato.
Perché un tipo string, perché non usare un enum di nome Iso3166Alpha2? Per alcuni potrebbe essere eccessivo, sebbene io lo userei. Per ora, rimaniamo su string, potremo poi sempre ricorrere al refactoring e cambiare.

Ora riportiamo il codice per check digits:

private readonly _checkDigits: string;
public get checkDigits(): string {
 return this._checkDigits;
}

Perché una stringa e non un numero? Vero, potrei usare anche un integer, teniamo presente che un IBAN ha una rappresentazione in stringa e potrebbe iniziare con uno zero. In questo caso la stringa potrebbe rivelarsi un’opzione migliore e più semplice.

Gestiamo l’ultimo dei tre valori, BBAN.

private readonly _bban: string;
public get bban(): string {
 return this._bban;
}

Nessun dubbio qui. Deve essere una stringa in quanto questa informazione contiene caratteri alfanumerici.

D.D.D in TypeScript – implementiamo il costruttore della classe IBAN

Pensiamo ora al costruttore della classe. Al momento è vuoto, quali parametri dovremmo passare affinché la nostra classe possa restituire un oggetto IBAN?. Se provenite dal mondo Java, potreste dire che potremmo passare i tre valori come parametri che impostano i valori all’interno del costruttore:

constructor(countryCode: string, checkDigits: string, bban: string) {
 this._countryCode = countryCode;
 this._checkDigits = checkDigits;
 this._bban = bban;
}

Può andare, ma non è così efficiente dal mio punto di vista. Passassi questi tre valori: “ciao”, “mondo”, “!” verrebbe creato un IBAN ben formattato? Sicuramente no. Consideriamo un altro approccio: se passassimo solo una stringa che presumibilmente sarebbe la rappresentazione di stringa di un oggetto IBAN?

constructor(iban: string) {
}

Sì, potrebbe essere. Ma potrebbe non essere valido. Potremmo convalidare la stringa iban utilizzando le regular expression, o RegExp.

D.D.D. in TypeScript – Regular expression e gestione degli errori

export const IBAN_REGEX: RegExp = new RegExp(
 '^([A-Z]{2})([0-9]{2})([A-Z0-9]{1,30})$',
 'i'
);

Non posso dire che questo servirà da proiettile d’argento, ma per il momento usiamolo:

constructor(iban: string) {
 const match: string[] = iban.match(IBAN_REGEX);
 if (match) {
  … set the variables depending on the regex group ...
 } else {
  … something is wrong with the string ...
 }
}

Ora il costruttore decide la validità dell’oggetto, mappa la stringa e imposta i valori. Apportiamo un’ulteriore modifica per gestire i casi in cui la stringa IBAN contenga spazi o caratteri minuscoli:

const match: string[] = iban.toUpperCase().replace(new RegExp('[ ]+', 'g'), '').match(IBAN_REGEX);

Aggiorniamo il costruttore:

constructor(iban: string) {
 const match: string[] = iban.toUpperCase().replace(new RegExp('[ ]+', 'g'), '').match(IBAN_REGEX);
 if (match) {
  this._countryCode = match[1];
  this._checkDigits = match[2];
  this._bban = match[3];
 } else {
  throw new Error('Not a well formatted IBAN number: ' + iban);
 }
}

Ciò che accade è che se il parametro di tipo string corrisponde alla regular expression allora vengono impostati i valori dell’oggetto prendendo l’elemento corretto dall’array contenuto in match, per effetto della regular expression. In caso contrario viene generato il messaggio di errore “IBAN non ben formattato”. Poco elegante come soluzione, ma funziona. Ovviamente, quando viene creato un nuovo IBAN, il codice deve essere incapsulato all’interno di un’istruzione try/catch, altrimenti potremmo finire con avere diverse linee rosse nella console. Cosa ho imparato? lanciare errori ovunque sia necessario in TypeScript/JavaScript non è poi così male.

D.D.D in Typescript – new Iban(…) oppure Iban.valueOf(…)?

Per spiegarmi meglio, vorrei che provaste a rispondere alla seguente domanda.

Quale delle seguenti righe di codice è più descrittiva quando si deve crea un nuovo oggetto IBAN?
La prima fa uso del classico new, la seconda utilizza uno static factory method (non ancora implementato nella nostra classe)?

  • const iban: Iban = new Iban('BE71 0961 2345 6769');
  • const iban: Iban = Iban.valueOf('BE71 0961 2345 6769');

Entrambe potrebbero andare bene. Ma se provassimo a tradurle in italiano, suonerebbero così:

  • (La) costante iban – che è un Iban – equivale a un nuovo (new) Iban BE71 0961 2345 6769.
  • (La) costante iban – che è un Iban – equivale al valore Iban di (Iban.valueOf(…)) BE71 0961 2345 6769.

Credo che la seconda opzione sia la migliore. Non dobbiamo creare/deifinire un nuovo IBAN, ma modellarne uno già esistente il cui valore è BE71 0961 2345 6769. Scegliamo quindi la seconda strada e utilizziamo lo static factory method e rendiamo privato il costruttore dell’Iban:

private constructor(iban: string) {
 ...
}

public static valueOf(iban: string): Iban {
 return new Iban(iban);
}

Non stilisticamente il meglio,mi rendo conto. Però è utile, pulito e soprattutto funzionante. Utilizzare static factory method portano ad avere un codice molto più pulito. Questa è la terza cosa che ho imparato.

D.D.D. in TypeScript – gli ultimi ritocchi per il nostro Value Object

Abbiamo creato uno scheletro per modellare il Value Object di un IBAN. Ciò che manca sono i metodi toString() e equals() (come il buon caro e vecchio Java).

public equals(iban: Iban): boolean {
 if (!!iban) {
   return this.countryCode === iban.countryCode &&
     this.checkDigits === iban.checkDigits &&
     this.bban === iban.bban;
 }
 return false;
}

public toString(): string {
 return this.countryCode + this.checkDigits + this.bban;
}

Conclusioni

In questo articolo abbiamo dimostrato che alcuni aspetti di D.D.D. (probabilmente anche altri) possono essere effettivamente utilizzati in TypeScript per creare Value Object auto-validanti quasi identici a come li implementeremmo in Java.
Nel prossimo articolo scaveremo più a fondo nel dominio attraverso la pagina Wikipedia e scriveremo una vera validazione per il nostro Value Object.

Articolo scritto da