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:
- aggiungere l'abbonamento "over 65";
- l'IVA dei corsi scende al 10%, quella degli abbonamenti resta al 22%;
- 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
rinnovaAbbonamentosenza spedire una mail vera.
Da qui la definizione di lavoro che useremo per tutto il corso.
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 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 |
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.
maincontiene il legacy di partenza e resta fermo;developriceve 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:
- quali smell avevate trovato anche voi?
- quali ha trovato solo l'assistente?
- quali sono sbagliati o inventati? Controllate file e riga.
8. Riepilogo
- Buon codice è codice che costa poco cambiare.
- Un pattern è una soluzione con un contesto e un costo, non codice da copiare.
- SOLID serve a leggere il codice e a capire dove farà male.
- Uno smell è un sintomo: prima si nomina, poi si decide se correggerlo.
- L'AI va guidata e riletta, e i pattern servono a fare entrambe le cose.