01 - Fondamenti e TicketHub

Fondamenti e TicketHub

1. Come funziona il corso

Il corso dura 40 ore, ed è costruito attorno a un progetto: TicketHub, una biglietteria per concerti ed eventi. È un'applicazione che funziona, ma che ha qualche anno sulle spalle e parecchi problemi. Per tutto il corso la cambierete un pezzo alla volta, e ogni cambiamento sarà l'occasione per incontrare un design pattern.

Le giornate

Giornata Tema
G1 Fondamenti, code smell e antipattern, primo contatto con TicketHub
G2 Creare oggetti e gestire le dipendenze
G3 Integrare servizi esterni
G4 Strutture e flussi
G5 L'ultimo ticket, una panoramica sugli altri pattern, la simulazione d'esame
G6 Esame

Il ciclo di un ticket

Dal secondo giorno in poi si lavora per ticket: richieste come quelle che arrivano in un team vero. Ogni ticket segue lo stesso ciclo.

Fase Cosa si fa
1. Ticket Il docente presenta la richiesta, con i criteri di accettazione
2. Senza pattern A coppie, senza AI, la risolvete come sapete, scrivendo prima i test
3. Variazione Arriva una seconda richiesta, collegata alla prima
4. Confronto Guardiamo insieme due o tre soluzioni diverse e ci chiediamo quanto costa il prossimo cambiamento
5. Teoria Il pattern: intento, struttura, smell che risolve, quando non usarlo
6. AI Ogni coppia chiede al proprio assistente di rifattorizzare la sua soluzione verso il pattern
7. Soluzione di classe Dal confronto tra le soluzioni nasce quella della classe, scritta passo per passo su develop

Nella fase 2 non serve arrivare a una soluzione perfetta, e nemmeno finire. Serve provarci con quello che sapete, perché il pattern ha senso solo quando avete sentito il problema che risolve.

Coppie e rami

Si lavora a coppie, su un ramo per coppia. All'apertura di un ticket:

git switch develop && git pull && git switch -c t01-cognome1-cognome2

Quando arriva la variazione, sul vostro ramo:

git pull origin develop

Fate commit spesso sul vostro ramo. Non serve fare push: i rami restano sul vostro computer. A fine ticket la soluzione costruita in classe arriva su develop, e il ticket successivo parte da lì.

Le regole sull'AI

Nella fase "senza pattern" niente AI. Nella fase 6 ogni coppia usa il proprio assistente e nel confronto dice cosa gli ha chiesto. Nella caccia agli smell di oggi l'AI entra solo nella seconda parte. Tutto il resto, comprese le regole che TicketHub dà agli assistenti, è in Lavorare con l'AI sul codice.

Il registro del debito

Nel repo c'è un file, DEBITO.md, che oggi è vuoto. È il registro dei problemi noti del codice: ogni riga è uno smell, con dove si trova, quale cambiamento rende costoso e quale principio viola. Lo compiliamo oggi con la caccia agli smell, e ogni ticket ne chiuderà qualche riga. A fine corso vedremo cosa abbiamo ripagato e cosa resta.

L'esame

È uno scritto, con scenari descritti a parole: per ciascuno si sceglie un pattern e si spiega come funziona la soluzione. La simulazione arriva nel G5.

2. Il costo del cambiamento

Esercizio: gli abbonamenti della palestra

Questo è il codice che gestisce i rinnovi degli abbonamenti di una palestra. Funziona, e lo usano da anni.

function rinnovaAbbonamento(socio: Socio, tipo: string, conCorsi: boolean): void {
    let prezzo = 0;
    let mesi = 0;
    switch (tipo) {
        case "mensile":
            prezzo = 45;
            mesi = 1;
            break;
        case "annuale":
            prezzo = 420;
            mesi = 12;
            break;
        case "studenti":
            prezzo = 30;
            mesi = 1;
            break;
        default:
            throw new Error("tipo sconosciuto: " + tipo);
    }
    let totale = prezzo * 1.22;
    if (conCorsi) {
        totale = totale + 15 * mesi * 1.22;
    }
    socio.scadenza = aggiungiMesi(socio.scadenza, mesi);
    db.salva(socio);

    let testo = "Ciao " + socio.nome + ", il tuo abbonamento " + tipo;
    testo += " è rinnovato fino al " + formatta(socio.scadenza) + ".";
    if (tipo === "studenti") {
        testo += " Ricorda di portare il tesserino universitario.";
    }
    testo += " Totale: " + totale.toFixed(2) + " euro, IVA 22% inclusa.";
    const mail = new SmtpClient("smtp.palestra.example");
    mail.invia(socio.email, "Rinnovo abbonamento", testo);
}

function prezzoInVetrina(tipo: string): string {
    switch (tipo) {
        case "mensile": return (45 * 1.22).toFixed(2);
        case "annuale": return (420 * 1.22).toFixed(2);
        case "studenti": return (30 * 1.22).toFixed(2);
    }
    return "";
}

La palestra chiede tre modifiche:

  1. aggiungere l'abbonamento "over 65";
  2. l'IVA dei corsi scende al 10%, quella degli abbonamenti resta al 22%;
  3. avvisare il socio via SMS invece che via mail.

A coppie: quanti punti del codice dovete toccare per ciascuna richiesta?

over 65 iva corsi sms

Cosa ci dice l'esercizio

Nessuna delle tre richieste è difficile. Eppure ognuna costa più di quanto dovrebbe, per motivi che si vedono leggendo il codice:

  • lo stesso switch sta in due funzioni. Un tipo di abbonamento nuovo va aggiunto in entrambe, e se ne dimenticate una il prezzo in vetrina e quello addebitato non coincidono più;
  • l'IVA è un numero scritto a mano in cinque punti, e anche nel testo della mail. Per cambiarla solo per i corsi bisogna capire quale dei 1.22 è quello giusto, e poi ricordarsi che il messaggio adesso dice una cosa falsa;
  • una sola funzione calcola, salva, scrive il messaggio e lo spedisce. Per cambiare il modo in cui si avvisa il socio bisogna mettere le mani nella funzione che calcola il prezzo;
  • il client della mail viene creato dentro la funzione. Non si può provare rinnovaAbbonamento senza spedire una mail vera.

Da qui la definizione di lavoro che useremo per tutto il corso.

Buon codice = cambiarlo costa poco

Il codice non resta mai fermo: arrivano clienti nuovi, regole nuove, fornitori nuovi. Un codice è buono se questi cambiamenti costano poco, cioè se toccano pochi punti, prevedibili, senza rompere il resto.

Le tre qualità che si citano di solito sono modi diversi di dire la stessa cosa, e ognuna ha un prezzo:

Qualità Cosa vuol dire Cosa costa ottenerla
Riuso Non riscrivere la stessa logica in più punti Capire dove serve e renderla abbastanza generale, senza esagerare
Estendibilità Aggiungere un caso nuovo senza riscrivere quelli vecchi Una struttura in più, che va giustificata da un cambiamento reale
Testabilità Poter provare un pezzo da solo, senza il resto del sistema Separare le responsabilità e far arrivare le dipendenze da fuori

Un'ultima cosa: il codice si legge molte più volte di quante si scriva. Chi lo scrive ci passa un'ora, chi lo legge dopo ci passa giorni. Scriverlo chiaro è un favore a chi verrà dopo, che spesso siete voi tra sei mesi.

3. Cosa sono i design pattern

Una definizione

Un design pattern è una soluzione ricorrente a un problema ricorrente, in un contesto preciso. Ha tre parti:

  • il problema: cosa fa male, e in quale situazione;
  • la soluzione: la struttura di classi e oggetti che risponde al problema;
  • le conseguenze: cosa si guadagna e cosa si paga.

Un pattern non è codice da copiare. Due persone che applicano lo stesso pattern scrivono codice diverso, con nomi diversi, adatto al loro progetto. Quello che hanno in comune è la struttura, e il motivo per cui l'hanno scelta.

Pattern e algoritmi

Un algoritmo è una ricetta: passi precisi, in un ordine preciso, che portano a un risultato. Un pattern somiglia di più al progetto tecnico di un mobile: dice com'è fatto il risultato e quali vincoli deve rispettare, ma i passi per costruirlo li decidete voi. Un algoritmo di ordinamento si può copiare e usare così com'è; un pattern va adattato ogni volta al problema che avete davanti.

Un po' di storia

L'idea di pattern nasce in architettura. Nel 1977 Christopher Alexander pubblica A Pattern Language, un catalogo di soluzioni ricorrenti per progettare città, edifici e stanze, ognuna con il suo problema e il suo contesto.

Nel 1994 quattro autori, Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides (la "Gang of Four", GoF), portano la stessa idea nel software con il libro Design Patterns. Il catalogo contiene 23 pattern, con esempi in C++ e Smalltalk. È ancora il riferimento, ma è figlio del suo tempo: alcuni pattern rispondono a limiti dei linguaggi di allora che oggi non esistono più.

Le tre famiglie

Il GoF divide i pattern in tre famiglie, secondo la domanda a cui rispondono.

Famiglia Domanda Cosa cercano di ottenere
Creazionali Come si creano gli oggetti? Che chi usa un oggetto non debba sapere come costruirlo
Strutturali Come si compongono gli oggetti? Che oggetti e classi si combinino in strutture più grandi senza legarsi troppo
Comportamentali Come si dividono il lavoro e come comunicano? Che responsabilità e algoritmi si possano cambiare senza toccare chi li usa

C'è anche una seconda distinzione: alcuni pattern lavorano sulle classi e usano l'ereditarietà, che si decide quando si scrive il codice. Altri lavorano sugli oggetti e usano la composizione, che si può cambiare mentre il programma gira. Il GoF lo riassume con una frase che tornerà spesso: preferire la composizione all'ereditarietà.

Com'è fatta la scheda di un pattern

Ogni pattern del catalogo è descritto con la stessa scheda, e la useremo anche noi:

Voce Cosa dice
Nome Come lo chiamiamo, per poterne parlare
Intento Cosa fa, in una frase
Problema In quale situazione serve, e cosa fa male senza
Soluzione Le classi e gli oggetti coinvolti, e come collaborano
Partecipanti I ruoli, con i loro nomi
Conseguenze Cosa si guadagna, cosa si paga, quando non usarlo
Esempi Dove si incontra nel codice reale

Li avete già usati

Alcuni pattern li avete già incontrati, magari senza saperne il nome.

Dove l'avete visto Il pattern
I middleware di Express, che si passano la richiesta con next() Chain of Responsibility
I servizi Angular con providedIn: 'root': un'unica istanza, creata e gestita dall'injector Un'istanza condivisa gestita dalla dependency injection
Observable e subscribe di RxJS Observer
for...of su un array, una Map o qualunque oggetto iterabile Iterator

I middleware di Express, per esempio, sono una catena: ognuno fa il suo lavoro e passa la richiesta al successivo.

app.use((req, res, next) => { console.log(req.method, req.url); next(); });
app.use((req, res, next) => (req.headers["x-user-id"] ? next() : res.status(401).end()));
app.get("/events", (req, res) => res.json(eventi));

I pattern del corso

Il corso tratta a fondo dieci pattern del GoF:

Famiglia Pattern
Creazionali Factory Method, Abstract Factory, Singleton
Strutturali Adapter, Decorator, Proxy, Composite, Facade
Comportamentali Strategy, Template Method

Gli altri tredici (Builder, Prototype, Bridge, Flyweight, Observer, Command, State, Chain of Responsibility, Iterator, Mediator, Memento, Visitor, Interpreter) li vedremo in una panoramica nel G5.

Attenzione a un nome: in NestJS vedrete decorator ovunque (@Controller, @Get, @Injectable), ma sono una funzionalità di TypeScript, non il pattern Decorator. La differenza è spiegata in Decorator TypeScript e Decorator GoF.

Le critiche

I pattern hanno anche critici, e le loro obiezioni vanno prese sul serio:

  • Compensano limiti dei linguaggi. Alcuni pattern esistono perché il C++ del 1994 non aveva funzioni come valori, iteratori nel linguaggio o moduli. In TypeScript certi problemi si risolvono con una funzione o con un oggetto letterale.
  • Golden Hammer. Chi ha appena imparato un martello vede chiodi ovunque. Dopo questo corso vi verrà voglia di mettere un pattern in ogni classe.
  • Complessità aggiunta. Ogni pattern porta classi e interfacce in più. Se il problema che risolve non c'è, il codice diventa solo più lungo e più difficile da seguire.
  • Il catalogo non è completo. I 23 pattern del GoF sono una scelta, non la lista di tutte le buone soluzioni. Esistono altri cataloghi e moltissime soluzioni che non hanno un nome.
Il rischio di questo corso

Il Golden Hammer è il rischio principale per chi studia i pattern per la prima volta. Un pattern si giustifica solo se il problema che risolve c'è già nel codice, o sta arrivando davvero. Introdurre una struttura per un problema che non avete è Speculative Generality.

Pattern e AI

Gli assistenti AI scrivono codice molto in fretta. I pattern servono anche con loro, in due modi: sono il vocabolario per chiedere una struttura precisa invece di un generico "rendilo più pulito", e sono il metro per giudicare quello che l'assistente produce. Chi non conosce un pattern non sa chiederlo, e non si accorge se è applicato male.

4. SOLID come lente

SOLID è l'acronimo di cinque principi raccolti da Robert C. Martin. Non li useremo come regole da rispettare a ogni costo, ma come una lente: quando un pezzo di codice fa male, quasi sempre c'è un principio violato, e capire quale aiuta a scegliere la soluzione.

S: Single Responsibility

Intento. Una classe dovrebbe avere un solo motivo per cambiare.

Segnale. Per descrivere cosa fa la classe serve una "e": calcola le statistiche e le impagina e le spedisce. Cambia per motivi che non hanno niente a che fare tra loro.

// prima: tre motivi per cambiare (la query, l'impaginazione, il fornitore della mail)
class ReportSerata {
    constructor(private readonly db: Database) {}

    genera(eventoId: string): void {
        const ingressi = this.db.query("SELECT * FROM ingressi WHERE evento = ?", eventoId);
        const perVarco = new Map<string, number>();
        for (const i of ingressi) {
            perVarco.set(i.varco, (perVarco.get(i.varco) ?? 0) + 1);
        }
        let html = "<h1>Serata " + eventoId + "</h1><p>Presenti: " + ingressi.length + "</p>";
        for (const [varco, n] of perVarco) {
            html += "<p>" + varco + ": " + n + "</p>";
        }
        const smtp = new SmtpClient("smtp.example.com");
        smtp.invia("organizzatore@example.com", "Report della serata", html);
    }
}
// dopo: ogni classe ha un motivo per cambiare
class StatisticheSerata {
    constructor(private readonly ingressi: IngressiRepository) {}
    calcola(eventoId: string): Statistiche { /* conteggi per varco */ }
}

class ReportHtml {
    formatta(statistiche: Statistiche): string { /* impaginazione */ }
}

class InvioReport {
    constructor(
        private readonly statistiche: StatisticheSerata,
        private readonly report: ReportHtml,
        private readonly mail: MailSender,
    ) {}

    invia(eventoId: string, destinatario: string): void {
        const html = this.report.formatta(this.statistiche.calcola(eventoId));
        this.mail.invia(destinatario, "Report della serata", html);
    }
}

Abuso tipico. Spezzare tutto fino ad avere decine di classi da una riga, che nessuno sa dove trovare. Una responsabilità è un motivo per cambiare, non un metodo.

O: Open/Closed

Intento. Un modulo dovrebbe essere aperto all'estensione e chiuso alla modifica: per aggiungere un caso nuovo si aggiunge codice, non si riscrive quello che c'è.

Segnale. Uno switch sul tipo che si ripete in più funzioni e che va ritoccato ogni volta che arriva un caso nuovo.

// prima: un ruolo nuovo dello staff obbliga a modificare tutte le funzioni come questa
function zoneAccessibili(badge: Badge): string[] {
    switch (badge.ruolo) {
        case "tecnico": return ["palco", "backstage", "regia"];
        case "sicurezza": return ["ingressi", "platea", "backstage"];
        case "bar": return ["bar", "magazzino"];
        default: return [];
    }
}
// dopo: un ruolo nuovo è una classe nuova, il codice che usa Ruolo non cambia
interface Ruolo {
    zoneAccessibili(): string[];
    colorePass(): string;
}

class Tecnico implements Ruolo {
    zoneAccessibili(): string[] { return ["palco", "backstage", "regia"]; }
    colorePass(): string { return "blu"; }
}

class Sicurezza implements Ruolo {
    zoneAccessibili(): string[] { return ["ingressi", "platea", "backstage"]; }
    colorePass(): string { return "rosso"; }
}

function zoneAccessibili(badge: Badge): string[] { {
	const ruolo: Ruolo = badge.ruolo;
	return ruolo.zoneAccessibili();
}

function test() {
	const badge = getBadgeFromCode(....);
	zoneAccessibili(badge);
}

Abuso tipico. Preparare astrazioni per varianti che non arriveranno mai. Se i ruoli sono tre e cambiano una volta all'anno, a volte basta una tabella di dati.

L: Liskov Substitution

Intento. Un oggetto di una sottoclasse deve poter prendere il posto di un oggetto della classe base senza che chi lo usa se ne accorga.

Segnale. Un override che lancia "non supportato", oppure un instanceof prima di chiamare un metodo, per evitare la sottoclasse che si comporta male.

// prima: BigliettoNominativo non si comporta come un Biglietto
class Biglietto {
    constructor(protected titolare: string) {}
    
    trasferisci(nuovoTitolare: string): void {
        this.titolare = nuovoTitolare;
    }
}

class BigliettoNominativo extends Biglietto {
	owner: string;
	
    trasferisci(nuovoTitolare: string): void {
        throw new Error("i biglietti nominativi non si trasferiscono");
    }
}

function regala(biglietto: Biglietto, amico: string): void {
    biglietto.trasferisci(amico);   // esplode se arriva un BigliettoNominativo
}
// dopo: la capacità di essere trasferito è separata
interface Biglietto {
    titolare(): string;
}

interface Trasferibile {
    trasferisci(nuovoTitolare: string): void;
}

class BigliettoLibero implements Biglietto, Trasferibile { /* ... */ }
class BigliettoNominativo implements Biglietto { /* ... */ }

function regala(biglietto: Trasferibile, amico: string): void {
    biglietto.trasferisci(amico);
}

Abuso tipico. Creare gerarchie solo per riusare qualche riga di codice. Se la sottoclasse non "è un" tipo della base in tutti i sensi, l'ereditarietà è la scelta sbagliata.

I: Interface Segregation

Intento. Nessuno dovrebbe dipendere da metodi che non usa: meglio tante interfacce piccole che una grande.

Segnale. Classi che implementano metodi vuoti, o che lanciano errori, solo per soddisfare un'interfaccia troppo grande.

// prima: l'app degli steward deve "implementare" anche vendite e rimborsi
interface GestioneEvento {
    crea(dati: DatiEvento): Evento;
    pubblica(id: string): void;
    vendi(id: string, posti: string[]): Ordine;
    rimborsa(ordineId: string): void;
    esportaVendite(id: string): string;
    leggiCodice(codice: string): EsitoIngresso;
}

class AppSteward implements GestioneEvento {
    leggiCodice(codice: string): EsitoIngresso { /* ... */ }
    crea(dati: DatiEvento): Evento { throw new Error("non usato"); }
    pubblica(id: string): void { throw new Error("non usato"); }
    // ...altri tre metodi che lanciano errori
}
// dopo: interfacce piccole, ognuno dipende solo da quello che usa
interface LetturaIngressi {
    leggiCodice(codice: string): EsitoIngresso;
}

interface Catalogo {
    crea(dati: DatiEvento): Evento;
    pubblica(id: string): void;
}

class AppSteward implements LetturaIngressi {
    leggiCodice(codice: string): EsitoIngresso { /* ... */ }
}

Abuso tipico. Un'interfaccia per ogni singolo metodo. Le interfacce si dividono seguendo chi le usa, non per principio.

D: Dependency Inversion

Intento. La logica di alto livello non dovrebbe dipendere dai dettagli: tutti e due dipendono da un'astrazione.

Segnale. Un new di un servizio concreto dentro la logica. Per provare la classe serve il servizio vero: il database, il server di posta, l'API esterna.

// prima: la newsletter si può provare solo spedendo mail vere
class Newsletter {
    inviaNovita(iscritti: Iscritto[], eventi: Evento[]): void {
        const smtp = new SmtpClient("smtp.example.com", 587);
        for (const i of iscritti) {
            smtp.invia(i.email, "Nuovi eventi", elenco(eventi));
        }
    }
}
// dopo: la newsletter dipende da un'astrazione, chi la crea sceglie l'implementazione
interface MailSender {
    invia(a: string, oggetto: string, corpo: string): void;
}

class Newsletter {
    constructor(private readonly mail: MailSender) {}

    inviaNovita(iscritti: Iscritto[], eventi: Evento[]): void {
        for (const i of iscritti) {
            this.mail.invia(i.email, "Nuovi eventi", elenco(eventi));
        }
    }
}
// in produzione SmtpMailSender, nei test un MailSender finto che registra i messaggi

Abuso tipico. Un'interfaccia per ogni classe, anche quando ne esisterà sempre una sola implementazione. È Speculative Generality con un nome elegante.

SOLID non è una legge

I cinque principi servono a leggere il codice e a capire dove farà male. Nella pratica si violano quasi sempre insieme: la classe che fa troppe cose (S) di solito crea da sola i suoi servizi (D) e ha uno switch sul tipo (O). Applicati senza pensare, invece, producono codice ben organizzato e difficile da capire.

Principio In una riga Smell tipico quando è violato
S, Single Responsibility Un solo motivo per cambiare Large Class, Divergent Change
O, Open/Closed Si estende aggiungendo, non modificando Switch Statements, Shotgun Surgery
L, Liskov Substitution La sottoclasse sostituisce la base senza sorprese Refused Bequest
I, Interface Segregation Nessuno dipende da metodi che non usa Interfacce con metodi vuoti
D, Dependency Inversion Si dipende da astrazioni, non da dettagli new di servizi concreti nella logica

5. Code smell e antipattern

Smell e antipattern

Concetto Cos'è
Code smell Un sintomo nel codice: non è un errore, ma segnala che un certo cambiamento sarà costoso
Antipattern Una soluzione che si ripete spesso, sembra ragionevole e ha conseguenze negative note
Il fumo e l'incendio

Uno smell è il fumo. L'antipattern è l'incendio già avviato.

Le cinque famiglie di smell

Famiglia Cosa hanno in comune Esempi
Bloaters Codice cresciuto troppo Long Method, Large Class, Long Parameter List
Object-Orientation Abusers Strumenti a oggetti usati male Switch Statements, Refused Bequest
Change Preventers Cambiamenti lenti o sparpagliati Divergent Change, Shotgun Surgery
Dispensables Cose che si potrebbero togliere Duplicate Code, Dead Code, Speculative Generality
Couplers Classi troppo legate tra loro Feature Envy, Message Chains, Middle Man

Ogni smell, con i segnali per riconoscerlo e un esempio, è nel Catalogo dei code smell. Vi servirà già oggi pomeriggio.

Antipattern da riconoscere

Nessuno scrive un antipattern apposta. Nascono da buone intenzioni, dalla fretta, dalla paura di rompere qualcosa. Capire perché nascono aiuta a riconoscerli, anche nel proprio codice.

Antipattern Cos'è Perché nasce
God Object Una classe che sa e fa tutto Ogni funzionalità nuova finisce nel posto dove c'è già tutto il resto, perché è il posto più comodo
Spaghetti Code Codice senza struttura, con un flusso che salta ovunque Tante modifiche urgenti, nessuna occasione per riordinare
Copy-paste programming La stessa logica copiata e leggermente modificata Copiare è più veloce che capire come generalizzare, almeno la prima volta
Lava Flow Codice vecchio che nessuno tocca perché nessuno sa cosa rompe Chi l'ha scritto non c'è più, e senza test cancellare fa paura
Golden Hammer Usare sempre lo stesso strumento, qualunque sia il problema Si usa quello che si conosce meglio, o quello appena imparato
Singletonitis Rendere globale qualunque cosa debba essere unica Una sola istanza sembra la soluzione più semplice, e lo è finché non si deve provare quel codice
Speculative Generality Strutture preparate per casi futuri che non arrivano La paura di dover riscrivere tutto "quando servirà"
Vendor lock-in Il codice dipende così tanto da un fornitore che cambiarlo costa una riscrittura Usare direttamente la libreria del fornitore è la strada più breve, il giorno in cui la si sceglie

Gli smell del codice generato

Il codice scritto da un assistente AI porta i suoi smell ricorrenti:

  • duplicazione plausibile: funzioni quasi identiche generate in momenti diversi;
  • astrazioni senza motivo: interfacce e classi "per il futuro" che nessuno ha chiesto;
  • test modificati per farli passare: il test diventa più permissivo, il bug resta;
  • modifiche fuori scope: file toccati che con la richiesta non c'entravano.

6. TicketHub

Il dominio

TicketHub vende biglietti per concerti ed eventi: gestisce gli eventi, i locali con i loro posti, gli ordini e i pagamenti, i biglietti e i varchi d'ingresso, e importa gli eventi che mandano i promoter. Le parole del dominio sono queste:

Termine Significato
Evento Concerto o spettacolo in una data e in un locale (EventRecord)
Locale Il luogo dell'evento, con la sua mappa dei posti (Venue)
Settore Gruppo di posti con lo stesso prezzo, per esempio A, PLATEA, PIT
Posto numerato Posto con fila e numero, si vende una volta sola
Posto in piedi Settore con una capienza, si vende a quantità
Ordine Un acquisto: uno o più posti e un pagamento
Biglietto Un ingresso, con il codice che si legge ai varchi
Varco Tornello d'ingresso
Promoter Organizzazione esterna che manda gli eventi da importare
Commissione Il 12% aggiunto al prezzo del biglietto

I moduli

Cartella Cosa fa
src/events/ Eventi in vendita e loro dettaglio
src/venues/ Mappa dei posti di un locale per un evento
src/orders/ Acquisto, dettaglio e annullamento degli ordini
src/tickets/ Emissione e download dei biglietti
src/payments/ Notifiche del fornitore dei pagamenti
src/reports/ Incassi per evento
src/gates/ Lettura dei biglietti ai varchi
src/imports/ Import degli eventi dai promoter
src/notifications/ Mail di conferma
src/inventory/ Posti venduti per ogni evento
src/data/ Tipi del dominio, dati di partenza e repository
src/shared/ Utilità comuni
vendor/ SDK dei fornitori esterni: pagamenti, tornelli, feed dei promoter. Non si modifica

Regole del progetto

  • Niente database. I dati stanno in repository in memoria e ripartono dal seed a ogni avvio.
  • Due rami pubblici. main contiene il legacy di partenza e resta fermo; develop riceve le soluzioni costruite in classe.
  • Regole per gli assistenti. Stanno in AGENTS.md.
  • Registro del debito. È DEBITO.md.
  • Refactoring a piccoli passi. Come si cambia codice che funziona senza romperlo è spiegato in Refactoring a piccoli passi.

Avviare il progetto

Il repo è https://github.com/battistaar/its_2527_design_pattern. Serve Node 24, la versione è in .nvmrc.

git clone https://github.com/battistaar/its_2527_design_pattern.git
cd its_2527_design_pattern
nvm use
npm ci
npm test
npm run test:e2e
npm run start:dev

Poi aprite http://localhost:3000/events.

Per i controlli durante il lavoro:

Comando Cosa fa
npm test Test unitari
npm run test:e2e Test e2e
npm run typecheck Controllo dei tipi, test compresi
npm run lint Controllo dello stile

NestJS in breve

NestJS mette ordine sopra Express. L'applicazione è divisa in moduli. I controller ricevono le richieste HTTP. I provider contengono la logica e i dati, e il container di dependency injection li crea e li passa nel costruttore a chi li usa. Se venite da Angular, vi sembrerà familiare: NestJS per chi viene da Express e Angular.

7. Caccia agli smell

Le aree

Ogni coppia riceve un'area di TicketHub. Le cartelle src/inventory/, src/shared/ e src/data/ sono comuni: chiunque può segnalare quello che trova lì.

Area Coppie
src/orders/ 3
src/events/ e src/venues/ 2
src/tickets/ 2
src/payments/ e src/reports/ 2
src/gates/ 2
src/imports/ 2

La scheda

Per ogni smell trovato compilate una riga, con le stesse colonne di DEBITO.md:

Smell Dove Quale cambiamento rende costoso Principio violato
Switch Statements rinnovaAbbonamento e prezzoInVetrina Aggiungere un tipo di abbonamento OCP

L'esempio viene dall'esercizio della palestra. Su TicketHub indicate file e riga.

Prima senza AI, poi con l'AI

Nella prima parte leggete il codice della vostra area e compilate la scheda, aiutandovi con il Catalogo dei code smell.

Nella seconda parte date all'assistente la stessa area, con questa richiesta:

Elenca i code smell che trovi in questi file. Per ognuno indica file e righe, il nome dello smell secondo il catalogo di Fowler e quale cambiamento futuro rende costoso. Non correggere niente.

Poi confrontate il risultato con la vostra scheda:

  1. quali smell avevate trovato anche voi?
  2. quali ha trovato solo l'assistente?
  3. quali sono sbagliati o inventati? Controllate file e riga.

8. Riepilogo

  1. Buon codice è codice che costa poco cambiare.
  2. Un pattern è una soluzione con un contesto e un costo, non codice da copiare.
  3. SOLID serve a leggere il codice e a capire dove farà male.
  4. Uno smell è un sintomo: prima si nomina, poi si decide se correggerlo.
  5. L'AI va guidata e riletta, e i pattern servono a fare entrambe le cose.