Software Engineering

KISS, YAGNI e DRY: tre adesivi e cosa vogliono dire davvero

Parzialmente scremato un po’ più di corpo

Tienilo semplice, non ti servirà, non ripeterti. Da dove vengono le tre sigle più citate del software (un aereo spia, l’Extreme Programming e un libro del 1999), cosa dicono davvero e cosa succede quando litigano. Con una fattoria, il prezzo del latte e nessun cammello.

cowsay

Sul portatile di ogni sviluppatore c’è almeno un adesivo con una sigla. Tre sono particolarmente famose, e sembrano usciti da un biscotto della fortuna: KISS, YAGNI e DRY. Tienilo semplice. Non ti servirà. Non ripeterti.

Si citano nelle code review per vincere le discussioni, e quasi sempre un po’ a sproposito. Così la mucca è tornata alle fonti: da dove viene ogni sigla, cosa dice davvero e cosa non dice. Con esempi in Python, tutti eseguiti davvero, e una fattoria che vende latte, che è la cosa più seria che conosciamo.

SiglaSignificaNasceSenso veroUsata male come
KISSKeep it simple, stupidKelly Johnson, Skunk Works della LockheedMeno pezzi, meno modi di rompersi«Non voglio imparare niente di nuovo»
YAGNIYou aren’t gonna need itExtreme Programming, fine anni ’90Non costruite funzioni per un futuro che state indovinando«Niente test, niente refactoring, niente design»
DRYDon’t repeat yourselfThe Pragmatic Programmer, 1999Ogni conoscenza vive in un posto solo«Fondete due righe qualsiasi che si somigliano»

KISS: keep it simple, stupid

KISS non viene dal software. Viene dagli aerei. La fonte più nota è Kelly Johnson, l’ingegnere che fondò gli «Skunk Works» della Lockheed, l’officina segreta che costruì l’aereo spia U-2 nel 1955 e poi l’SR-71 Blackbird, l’aereo più veloce e più alto del suo tempo. Nel ricordo scritto nel 1995 per la National Academy of Sciences americana da Ben Rich, che lavorò con lui agli Skunk Works, tra i motti di Johnson ci sono «Se funziona, non aggiustarlo» e questo:

«Keep it simple, stupid (KISS) è il nostro promemoria costante.» (Tienilo semplice, stupido.)

La frase girava in quegli anni: un dizionario dello slang americano registra un «Project KISS» della Marina degli Stati Uniti già nel 1960. Il senso è lo stesso. Se si può tenere semplice un aereo a Mach 3, si può tenere semplice anche la vostra fattura.

A proposito di fatture. Ecco come si calcola il prezzo di 12,5 litri di latte a 1,20 € al litro, scritto da qualcuno che ha appena letto un libro sui design pattern:

from abc import ABC, abstractmethod


class PricingStrategy(ABC):
    @abstractmethod
    def price(self, liters: float) -> float: ...


class StandardMilkPricingStrategy(PricingStrategy):
    def __init__(self, rate: float):
        self.rate = rate

    def price(self, liters: float) -> float:
        return round(liters * self.rate, 2)


class PricingStrategyFactory:
    _registry = {"standard": StandardMilkPricingStrategy}

    @classmethod
    def create(cls, kind: str, **kwargs) -> PricingStrategy:
        return cls._registry[kind](**kwargs)


class MilkInvoiceService:
    def __init__(self, factory=PricingStrategyFactory):
        self.strategy = factory.create("standard", rate=1.20)

    def invoice(self, liters: float) -> float:
        return self.strategy.price(liters)


print(MilkInvoiceService().invoice(12.5))

Ed ecco la stessa cosa scritta dalla mucca:

def milk_price(liters, rate=1.20):
    return round(liters * rate, 2)


print(milk_price(12.5))
$ python3 over.py
15.0
$ python3 simple.py
15.0
$ wc -l over.py simple.py
 33 over.py
  5 simple.py
 38 total

Stesso risultato, 15 €. Una versione ha una classe astratta, una strategia, una factory, un registro e un servizio: 33 righe per fare una moltiplicazione. L’altra ne ha 5. Quando cambia il prezzo, tutte e due vanno modificate in un punto. Quando c’è un bug, però, una delle due ha cinque posti in cui nasconderlo.

🐄
Una factory con un registro per un’unica strategia che ha un’unica implementazione. Sbagliato. Questa non è architettura, è imballaggio. Il latte è lo stesso, ma adesso arriva in cinque scatole, una dentro l’altra.
🐄
Scrivere la funzione da cinque righe, e aggiungere la strategia il giorno in cui le regole di prezzo sono davvero due. Giusto. Il codice semplice è facile da complicare. Il contrario non succede quasi mai.
🐄
«Semplice» secondo chi? Dipende. Semplice non vuol dire corto, e non vuol dire «la prima cosa che mi è venuta in mente». Una riga furbissima che nessuno sa leggere non è semplice. Semplice vuol dire che un collega la capisce al primo colpo.

YAGNI: you aren’t gonna need it

YAGNI nasce nell’Extreme Programming alla fine degli anni ’90. Martin Fowler racconta com’è cominciata: sul progetto C3, Chet Hendrickson continuava a proporre funzionalità che un giorno sarebbero servite, e ogni volta Kent Beck rispondeva «non ti servirà». Ron Jeffries, un altro dei fondatori di XP, nel 1998 la riassunse così:

«Implementate le cose sempre quando vi servono davvero, mai quando prevedete soltanto che vi serviranno.»

Ecco una funzione di mungitura scritta da qualcuno che «prevede» molto:

def milk(animal, liters, species="cow", currency="EUR", blockchain=False, plugin=None):
    if species == "goat":
        raise NotImplementedError("goats: coming soon")
    if species == "camel":
        raise NotImplementedError("camels: phase 2")
    if blockchain:
        raise NotImplementedError("ask the investors")
    if plugin is not None:
        plugin.before_milking(animal)
    return f"{animal}: {liters} L"

Capre, cammelli, valute, plugin e una blockchain. Vediamo chi la chiama davvero, in tutto il progetto:

$ grep -rn "milk(" --include=*.py . | grep -v "def milk"
./evening.py:3:print(milk("Ercolina", 11.0, species="cow", currency="EUR"))
./morning.py:3:print(milk("Ercolina", 12.5, species="cow"))
./morning.py:4:print(milk("Carolina", 9.0))

Tre chiamate. Tutte per mucche, tutte in euro, mai un plugin, mai una blockchain. Il parametro currency non è nemmeno usato dentro la funzione. La versione onesta è questa:

def milk(animal, liters):
    return f"{animal}: {liters} L"

Fowler spiega perché la versione «non si sa mai» costa più di quanto sembri, con quattro costi. Il costo di costruzione: il tempo passato a scrivere, testare e documentare i cammelli. Il costo del ritardo: quello che nel frattempo non avete costruito, e che i clienti aspettavano davvero. Il costo di mantenimento: ogni parametro inutile rende il codice più pesante da leggere e da cambiare, per sempre. E il costo di riparazione: quando i cammelli arriveranno davvero, i requisiti saranno diversi da quelli che avevate immaginato, e dovrete riscrivere tutto comunque. Il suo esempio è una startup che assicura spedizioni via mare: sta lavorando al rischio tempeste e sa già che tra sei mesi le servirà il rischio pirateria. YAGNI dice: la pirateria non costruitela adesso.

🐄
Supportare capre, cammelli e una blockchain in una fattoria con due mucche. Sbagliato. La mucca ha controllato: nella stalla non ci sono cammelli. Non ci sono mai stati.
🐄
Cancellare i parametri e i rami inutilizzati quando li trovate. Giusto. Se ne ricorda il controllo di versione al posto vostro. Se mai arriveranno i cammelli, il codice sarà nella storia, e comunque lo riscriverete meglio, sapendo cosa vogliono i cammelli.

Ma attenzione, perché YAGNI è la più abusata delle tre. Fowler è esplicito: YAGNI non vale per il lavoro che rende il codice più facile da cambiare, come i test, il refactoring e un design pulito. Anzi: YAGNI funziona solo se il codice è facile da cambiare, perché tutta la scommessa è «lo aggiungeremo dopo, quando servirà». Se aggiungerlo dopo è un incubo, la scommessa è persa.

🐄
«Non scriviamo test: YAGNI.» Sbagliato. I test sono proprio quello che vi permette di dire YAGNI senza paura. Senza, non è YAGNI, è YOLO.

DRY: don’t repeat yourself

DRY viene da The Pragmatic Programmer di Andy Hunt e Dave Thomas, 1999. La definizione originale è più precisa dell’adesivo:

«Ogni conoscenza deve avere una rappresentazione unica, non ambigua e autorevole all’interno di un sistema.»

Notate la parola: conoscenza, non codice. Dave Thomas lo ha ripetuto in un’intervista del 2003: «La maggior parte delle persone pensa che DRY voglia dire non duplicare il codice. Non è quello il suo scopo.» La conoscenza di un sistema, dice, comprende anche gli schemi del database, i piani di test, il sistema di build e perfino la documentazione.

Ecco una vera violazione di DRY. Il prezzo del latte è scritto in due posti. Lunedì il prezzo sale a 1,35 €, e qualcuno aggiorna la fattura. L’etichetta sullo scaffale, invece, no:

# invoice.py
def invoice_total(liters):
    return round(liters * 1.35, 2)  # new price since Monday

# shelf.py
def shelf_label(liters):
    return f"{liters} L - EUR {liters * 1.20:.2f}"
$ python3 main.py
shelf:   12.5 L - EUR 15.00
invoice: 16.88

Il cliente legge 15,00 € sullo scaffale e ne paga 16,88 alla cassa. Questo non è un problema di stile, è un reclamo. La correzione è dare al prezzo una casa sola:

# prices.py
PRICE_PER_LITER = 1.35  # the one and only place where the price lives

# invoice.py
from prices import PRICE_PER_LITER

def invoice_total(liters):
    return round(liters * PRICE_PER_LITER, 2)

# shelf.py
from prices import PRICE_PER_LITER

def shelf_label(liters):
    return f"{liters} L - EUR {liters * PRICE_PER_LITER:.2f}"
$ python3 main.py
shelf:   12.5 L - EUR 16.88
invoice: 16.88
🐄
Il prezzo del latte, l’aliquota IVA, la dimensione di una pagina: scritti in un posto solo e importati ovunque. Giusto. Quando lo stesso fatto vive in due posti, prima o poi i due posti non saranno d’accordo. E il cliente troverà sempre quello più economico.

Ora il caso opposto. Queste due funzioni sono identiche, carattere per carattere (non le abbiamo eseguite, sono qui per essere guardate):

def valid_ear_tag(code):
    return len(code) == 8 and code.isdigit()


def valid_invoice_number(code):
    return len(code) == 8 and code.isdigit()

Codice duplicato! Il riflesso DRY dice: unitele in una valid_8_digit_code(). Ma non sono la stessa conoscenza. Una è una regola sulle marche auricolari delle mucche, l’altra è una regola dell’ufficio contabilità. Oggi per caso coincidono. Il giorno in cui cambiano le regole delle marche auricolari, e quelle delle fatture no, la funzione unificata mette su un parametro. Poi un altro. Poi un altro ancora:

def valid_code(code, *, is_ear_tag=False, country_prefix=False,
               invoice_year=None, legacy_mode=False, vet_mode=False):
    ...  # 60 lines of ifs. Nobody touches it. Nobody knows why.

Sandi Metz ha descritto questa storia passo per passo nel 2016, in «The Wrong Abstraction». Il programmatore A vede una duplicazione ed estrae un’astrazione. Arriva un nuovo requisito che ci sta quasi, e il programmatore B aggiunge un parametro e un if. Si ripete finché nessuno ci capisce più niente. La sua conclusione è diventata famosa: «la duplicazione costa molto meno dell’astrazione sbagliata». E la cura va contro l’istinto: rimettete il codice duplicato dov’era, in ogni chiamante, togliete quello che a ciascuno non serve, e solo allora cercate l’astrazione giusta.

🐄
Unire due funzioni solo perché si somigliano. Sbagliato. Due mucche con le stesse macchie restano due mucche. Stesso codice non vuol dire stessa conoscenza.

Come si capisce quando unire? Una buona regola pratica è la regola del tre, che Martin Fowler riporta in Refactoring (1999) come consiglio di Don Roberts: la prima volta che fate una cosa, la fate e basta. La seconda volta fate una smorfia davanti alla duplicazione, ma la fate lo stesso. La terza volta, fate refactoring. Con tre esempi sotto mano è molto più facile vedere cosa hanno davvero in comune.

🐄
Copiare e incollare un blocco di codice. Dipende. La seconda volta va bene, con una smorfia. La terza volta la mucca si aspetta una funzione. Se si tratta di conoscenza, come un prezzo o una regola, già una volta è troppo.

Quando le tre litigano

Le tre sigle non vanno sempre d’accordo. DRY spinge ad astrarre; KISS e YAGNI spingono a non costruire quello che non serve. La funzione con cinque parametri in più qui sopra è quello che succede quando DRY vince troppo presto: per non ripetervi, avete costruito qualcosa che non è semplice e che serve casi che forse non arriveranno mai. In questi casi l’ordine di precedenza della mucca è questo: prima, funziona e si legge? (KISS); poi, serve oggi? (YAGNI); e solo dopo, c’è una conoscenza ripetuta? (DRY).

🐄
Morale: tenetelo semplice, perché le cose semplici si rompono meno e si aggiustano più facilmente. Non costruite per i cammelli che immaginate, ma tenete il codice facile da cambiare, per quando arriveranno davvero. E non ripetete la conoscenza, ma non abbiate paura di un po’ di codice ripetuto. Gli adesivi vanno sui portatili, non nelle code review. La mucca ha parlato, e la mucca è legge.

Muuu. 🐄

Fonti

Gli esempi Python sono stati eseguiti con Python 3.13.5; gli output mostrati sono quelli reali. I due validatori e la funzione con i flag sono illustrativi e non sono stati eseguiti.

Ti è piaciuto? Segui il feed RSS e non ti perdi il prossimo articolo. Niente email, niente spam: parola di mucca.

Feed RSS