1) DRY – Don’t Repeat Yourself
Idea chiave: ogni porzione di conoscenza (regola, logica, informazione) deve avere una sola rappresentazione chiara e autorevole nel sistema.
Perché è importante:
Riduce la probabilità di errori divergenti: se la stessa logica è duplicata in più punti, una modifica potrebbe non propagarsi ovunque.
Facilita la manutenzione e l’evoluzione del codice.
Migliora la leggibilità e la riusabilità.
Come applicarlo:
Funzioni e metodi: sposta le operazioni comuni in funzioni. Se ripeti 3 righe simili in più file, crea una funzione ad hoc.
Classi e oggetti: incapsula la logica e i dati correlati in un’unica entità coerente.
Moduli e librerie comuni: per regole trasversali (es. formattazione, validazione, logging), crea un modulo separato.
Configurazioni centralizzate: evita valori “hardcoded”; usa file di configurazione o costanti uniche.
Idea: evitare la duplicazione di logica o codice. Se una regola cambia, dovrebbe bastare modificarla in un solo punto.
In pratica: incapsula logica comune in funzioni o classi, crea moduli riutilizzabili, elimina i copia&incolla.
# ❌ Non DRY
area1 = 3.14 * 10 * 10
area2 = 3.14 * 5 * 5
# ✅ DRY
def area_cerchio(raggio):
return 3.14 * raggio * raggio
area1 = area_cerchio(10)
area2 = area_cerchio(5)2) KISS – Keep It Simple, Stupid
Idea chiave: la semplicità è un valore tecnico. Il codice deve essere diretto, comprensibile e privo di complessità non necessaria.
Perché è importante:
Un codice semplice è più facile da leggere, testare e mantenere.
La complessità aumenta i rischi di bug e rende il codice fragile.
Riduce il costo cognitivo per chi deve intervenire in futuro (incluso te stesso).
Come applicarlo:
Scrivi per gli altri, non per te: il codice dovrebbe “spiegarsi da solo”.
Evita astrazioni premature: non introdurre pattern o livelli di generalizzazione finché non servono davvero.
Preferisci chiarezza a brevità: una soluzione più lunga ma leggibile è migliore di una compatta ma oscura.
Segui convenzioni note: nomi, strutture e idiomi standard rendono il codice prevedibile.
Riduci i side effect: funzioni semplici, con input chiaro e output prevedibile.
Idea: mantieni il codice semplice e leggibile. Evita astrazioni o “trucchi” inutili.
In pratica: scegli sempre la soluzione più chiara e lineare che risolve il problema.
# ❌ Eccessivamente complicato
def reverse_string(s):
return ''.join(list(reversed()))
# ✅ Semplice e chiaro
def reverse_string(s):
return s[::-1]3) YAGNI – You Aren’t Gonna Need It
Idea chiave: non scrivere codice per funzionalità che potresti usare in futuro — realizza solo ciò che serve oggi.
Perché è importante:
Il codice “preventivo” raramente serve davvero e spesso diventa debito tecnico.
Ogni riga non strettamente necessaria è potenziale fonte di bug o confusione.
Riduce i costi di manutenzione e consente di adattarsi in modo agile a cambiamenti reali, non ipotetici.
Come applicarlo:
Niente “forse servirà dopo”: rimanda implementazioni future finché non esistono requisiti concreti.
Evita over-engineering: non astrarre o generalizzare il codice finché non c’è una seconda esigenza reale.
Scrivi codice minimale e risolutivo: se una singola funzione basta oggi, non creare una gerarchia di classi.
Fidati dei cicli di sviluppo agili: aggiungerai funzionalità quando saranno richieste.
Idea: non implementare oggi funzionalità ipotetiche “per il futuro”.
In pratica: sviluppa solo ciò che serve ora; il codice inutile diventa debito tecnico.
# ❌ Implementazione prematura (mai usata)
def send_sms_notification(user, message, country_code="+39"):
print("SMS inviato...")
# ✅ Serve solo l'email
def send_email(user, message):
print("Email inviata...")4) SOLID (5 principi di OOP)
Un insieme di linee guida per scrivere classi estendibili e manutenibili.
I principi SOLID sono cinque linee guida formulate da Robert C. Martin per scrivere codice leggibile, estendibile e manutenibile nella programmazione orientata agli oggetti.
Essi mirano a ridurre l’accoppiamento tra moduli e ad aumentare la coesione, cioè la chiarezza e la coerenza del comportamento di ogni classe.
| Lettera | Principio | Idea chiave | Obiettivo |
|---|---|---|---|
| S | Single Responsibility Principle | Ogni classe deve avere una sola responsabilità e un solo motivo per cambiare. | Migliorare la coesione e la manutenibilità. |
| O | Open/Closed Principle | Le entità software devono essere aperte per estensioni, ma chiuse per modifiche. | Consentire di aggiungere funzionalità senza alterare il codice esistente. |
| L | Liskov Substitution Principle | Gli oggetti derivati devono poter sostituire quelli base senza cambiare il comportamento del programma. | Garantire correttezza e coerenza nell’uso dell’ereditarietà. |
| I | Interface Segregation Principle | Le interfacce devono essere specifiche e ridotte, non generiche e pesanti. | Evitare implementazioni forzate o metodi inutilizzati. |
| D | Dependency Inversion Principle | Le classi devono dipendere da astrazioni, non da implementazioni concrete. | Ridurre l’accoppiamento tra moduli e favorire l’inversione delle dipendenze. |
S – Single Responsibility
Idea: ogni classe deve avere una sola responsabilità ben definita.
# ❌ Troppe responsabilità in una classe
class Report:
def generate(self): pass
def save_to_db(self): pass
def send_email(self): pass
# ✅ Suddivisione delle responsabilità
class Report:
def generate(self): pass
class ReportRepository:
def save_to_db(self, report): pass
class ReportNotifier:
def send_email(self, report): passO – Open/Closed
Idea: componenti aperte all’estensione ma chiuse alla modifica.
# ❌ Catena di if/elif per nuovi tipi di sconto
def calculate_discount(type, amount):
if type == "student":
return amount * 0.9
elif type == "vip":
return amount * 0.8
# ✅ Polimorfismo estendibile
class Discount:
def apply(self, amount):
return amount
class StudentDiscount(Discount):
def apply(self, amount):
return amount * 0.9
class VipDiscount(Discount):
def apply(self, amount):
return amount * 0.8L – Liskov Substitution
Idea: le sottoclassi devono poter sostituire la superclasse senza rompere le aspettative.
class Bird:
pass
class FlyingBird(Bird):
def fly(self): pass
# ✅ Pinguino non eredita la capacità di volo
class Penguin(Bird):
passI – Interface Segregation
Idea: preferire interfacce piccole e mirate a una “onnicomprensiva”.
# ❌ Interfaccia troppo ampia
class Machine:
def print(self): pass
def scan(self): pass
def fax(self): pass
# ✅ Interfacce specifiche
class Printer:
def print(self): pass
class Scanner:
def scan(self): passD – Dependency Inversion
Idea: dipendi da astrazioni, non da implementazioni concrete.
# ❌ Dipendenza rigida
class MySQLDatabase:
def save(self, data): pass
class UserService:
def __init__(self):
self.db = MySQLDatabase()
# ✅ Inversione della dipendenza
class Database:
def save(self, data): pass
class MySQLDatabase(Database):
def save(self, data): pass
class UserService:
def __init__(self, db: Database):
self.db = db5) Separation of Concerns (SoC)
Idea chiave: dividere un sistema software in parti distinte, ognuna con una responsabilità o interesse proprio ben definito, come presentazione, logica di business o gestione dati.
Perché è importante:
Favorisce un’organizzazione modulare del codice che facilita manutenzione, riuso e testabilità.
Riduce la complessità, evitando che cambiamenti in un’area impattino altre parti indipendenti.
Permette di lavorare in parallelo su moduli differenti con meno conflitti tra team.
Come si applica:
Suddividi le funzionalità in moduli o componenti separati.
Nascondi i dettagli di implementazione dietro interfacce chiare.
Usa pattern architetturali come MVC (Model-View-Controller), che separa la gestione dati (Model), l’interfaccia utente (View) e la logica di controllo (Controller).
Mantieni separate le preoccupazioni tra livelli diversi: ad esempio, database, logica applicativa e interfaccia utente.
Esempio classico (Web):
HTML gestisce la struttura e il contenuto (concern struttura).
CSS si occupa dello stile e dell’aspetto (concern presentazione).
JavaScript controlla la logica e il comportamento dinamico (concern comportamento).
Relazione con altri principi:
SoC è molto vicino al Single Responsibility Principle perché entrambi mirano a ridurre l’accoppiamento incapsulando specifiche responsabilità in parti separate
Idea: separa responsabilità diverse (presentazione, business, dati) in moduli distinti.
# ❌ Mescola UI e business
def process_order(order):
print("Processing order:", order)
save_to_db(order)
# ✅ Ruoli separati
def process_order(order):
save_to_db(order)
def save_to_db(order): pass
def show_message(order):
print("Processing order:", order)6) Legge di Demetra (Principio della minima conoscenza)
La Legge di Demetra, nota anche come Principio della minima conoscenza, è una linea guida per lo sviluppo software, in particolare nella programmazione orientata agli oggetti. È stata formulata nel 1987 nell’ambito del progetto Demetra alla Northeastern University di Boston.
Idea chiave: un oggetto dovrebbe conoscere e interagire solo con pochi altri oggetti strettamente collegati a sé, evitando di chiamare metodi di oggetti ottenuti da altri oggetti (cioè non “parlare con sconosciuti”). Formalmente, un metodo di un oggetto può invocare metodi solo su:
se stesso,
oggetti passati come parametri,
oggetti da lui creati,
suoi componenti diretti.
Il principio evita di dipendere dalla struttura interna di oggetti indirettamente accessibili, aumentando l’incapsulamento e l’indipendenza tra classi.
Vantaggi:
codice più robusto e facilmente manutenibile, perché riduce le dipendenze fragili e l’effetto domino di cambiamenti in strutture profonde;
facilita la modifica e l’evoluzione del software senza dover ristrutturare larghe porzioni di codice.
Svantaggi:
può portare a dover scrivere molti metodi wrapper per propagare chiamate, aumentando la quantità di codice;
potenziali implicazioni sulle performance.
Seguire la Legge di Demetra rende il software più modulare e meno soggetto a bug derivanti da dipendenze eccessive.
Idea: “parla solo con i tuoi amici più stretti”: evita catene di chiamate lunghe.
# ❌ Catena di accessi
order.customer.address.city
# ✅ Fornisci un metodo dedicato
class Customer:
def get_city(self):
return self.address.city
city = order.customer.get_city()7) Fail Fast
Il principio Fail Fast in programmazione e sviluppo software consiglia di rilevare e segnalare immediatamente eventuali errori o condizioni anomale nel momento in cui si verificano, invece di tentare di continuare l’esecuzione in stato di errore o in condizioni non corrette.
Idea chiave:
Il sistema o componente dovrebbe “fallire velocemente” appena si accorge che qualcosa non va, interrompendo l’operazione o sollevando eccezioni non appena possibile. Questo evita che errori minori si propagano in modo nascosto creando problemi più difficili da risolvere successivamente.
Vantaggi principali:
Facilita il debug e la localizzazione dell’errore, perché il problema emerge istantaneamente.
Riduce il tempo e costo necessario per la correzione, evitando di sprecare risorse su elaborazioni errate.
Migliora la qualità del software e la sua affidabilità, consentendo di intervenire subito sulle cause reali.
Come si applica in pratica:
Controlli rigorosi su input e stato iniziale: validazione dei parametri in ingresso.
Uso di eccezioni o interruzioni immediate in caso di condizioni anomale.
Programmazione difensiva per verificare invarianti e precondizioni.
In architetture distribuite, i servizi che falliscono subito aiutano a isolare guasti e a mantenere la stabilità complessiva.
Differenza con Fail Safe: Fail Fast interrompe l’esecuzione alla prima evidenza di malfunzionamento; Fail Safe cerca di continuare in modo sicuro anche in presenza di errori.
Idea: fallire subito, in modo esplicito, quando qualcosa non va: facilita debug e stabilità.
# ❌ Silenzioso e ambiguo
def divide(a, b):
if b == 0:
return None
return a / b
# ✅ Fallisci subito con errore chiaro
def divide(a, b):
if b == 0:
raise ValueError("Divisione per zero!")
return a / b8) Convention over Configuration
Principio: ridurre il numero di decisioni che il programmatore deve prendere durante lo sviluppo, affidandosi a convenzioni predefinite invece di richiedere ogni dettaglio di configurazione.
Come funziona: un framework o sistema applica comportamenti e strutture di default basate su regole consolidate. Per esempio, una classe User sarà automaticamente associata a una tabella database chiamata users senza dover configurare esplicitamente questa mappatura. Solo quando si vuole deviare da queste convenzioni si inseriscono configurazioni specifiche.
Vantaggi:
Accelera notevolmente lo sviluppo, evitando boilerplate e configurazioni ridondanti.
Migliora la coerenza e la standardizzazione del codice tra progetti e team.
Riduce il rischio di errori di configurazione grazie a comportamenti predefiniti affidabili.
Facilita l’onboarding dei nuovi sviluppatori, che trovano un ambiente prevedibile.
Svantaggi:
Può apparire come “magia” o complesso da capire, specialmente per i nuovi arrivati, perché molte cose avvengono implicitamente.
Meno flessibilità immediata se si vuole scostarsi dalle convenzioni standard.
Esempi celebri:
Ruby on Rails ha popolarizzato questo paradigma, con file, classi e cartelle nominate seguendo standard che sono automaticamente “collegati” dal framework.
Anche framework come Grails, Django e Spring adottano questo approccio.
In sintesi:
Convention over Configuration permette di scrivere meno codice di configurazione, mantenendo alta flessibilità e velocità sviluppo, a patto di adottare le convenzioni stabilite dallo strumento o community.
Idea: ridurre la configurazione usando convenzioni prevedibili.
Esempio “artigianale” di convenzione sul nome tabella a partire dal nome classe:
class User: pass
class Product: pass
def get_table_name(cls):
return cls.__name__.lower()
print(get_table_name(User)) # "user"
print(get_table_name(Product)) # "product"9) Composition over Inheritance
Principio: in programmazione orientata agli oggetti, la composizione è preferita all’ereditarietà per costruire funzionalità riusabili e mantenibili. Invece di estendere una classe base e creare gerarchie rigide, si compongono oggetti più piccoli con comportamenti specifici.
Idea chiave:
Ereditarietà modella relazioni “is-a” (es. un “Cane” è un “Animale”).
Composizione modella relazioni “has-a” (es. un “Car” ha un motore, ruote, ecc.).
Vantaggi della composizione:
Riduce il coupling forte tra classi; un cambiamento in un componente non impatta l’intero albero di ereditarietà.
Accresce la flessibilità, permettendo di cambiare comportamenti semplicemente assemblando componenti senza riscrivere classi figlio.
Elimina problemi tipici dell’ereditarietà, come il “diamond problem” in ereditarietà multipla.
Favorisce il riuso di componenti standardizzati e piccoli, più semplici da testare singolarmente.
Implementazione tipica:
Si definiscono interfacce per comportamenti (es. Movable, Drawable).
Si realizzano classi che implementano queste interfacce.
Gli oggetti “macro” compongono i singoli comportamenti come componenti.
Esempio:
Un personaggio in un gioco può essere composto da componenti come “Movimento”, “Salto” e “Attacco” invece che ereditare da varie classi specializzate.
Quando preferire l’ereditarietà:
Quando c’è una chiara relazione “is-a” e tutte le sottoclassi condividono tutto il comportamento del genitore (rispettando il principio di sostituzione di Liskov).
Quando servono specializzazioni chiare e definite.
Sintesi:
La composizione aiuta a costruire sistemi più modulabili e flessibili, facilitando l’adattamento ai cambiamenti senza compromettere la stabilità del codice, mentre l’ereditarietà può introdurre dipendenze rigide e difficili da modificare.
Idea: preferisci la composizione di oggetti a gerarchie profonde di ereditarietà.
# ✅ Composizione
class Dog:
def __init__(self, behavior):
self.behavior = behavior
class ShepherdBehavior:
def guard(self):
print("Protegge la fattoria!")
dog = Dog(ShepherdBehavior())
dog.behavior.guard()10) Principle of Least Privilege (PoLP)
Il Principio di Minimo Privilegio stabilisce che ogni utente, processo o programma deve avere soltanto i permessi essenziali e minimi necessari per svolgere le proprie funzioni, e nulla di più.
Perché è importante:
Riduce la superficie di attacco: limitando i privilegi si diminuiscono le possibilità di utilizzo malevolo o accidentale delle risorse o funzionalità critiche.
Limita la propagazione di effetti dannosi: in caso di compromissione, il danno è confinato all’area di privilegi concessi.
Migliora la sicurezza e la stabilità: impedisce che un errore o un attacco possa compromettere l’intero sistema o dati sensibili.
Facilita la conformità normativa e la tracciabilità: i permessi strettamente necessari rendono più chiara la gestione e l’auditabilità degli accessi.
Come implementarlo:
Condurre audit dei permessi esistenti per rimuovere accessi eccessivi.
Partire con privilegi minimi e concederli solo se necessario.
Separare ruoli e privilegi amministrativi da quelli normali.
Utilizzare privilegi “just-in-time” o temporanei.
Registrare e tracciare ogni modifica di privilegi.
Controllare periodicamente l’adeguatezza dei privilegi concessi.
Esempio: un programma per backup dovrebbe poter solo leggere e scrivere i file necessari, ma non avere privilegi di installazione o configurazione di sistema.
Idea: ogni componente deve avere solo i permessi strettamente necessari.
# ❌ Pericoloso
def delete_file(path):
import os
os.system(f"rm -rf {path}")
# ✅ Approccio più sicuro e limitato
import os
def safe_delete_file(path):
if os.path.exists(path) and path.endswith(".txt"):
os.remove(path)11) Clean Code
Il concetto di Clean Code si riferisce a un insieme di pratiche e principi per scrivere codice sorgente che sia chiaro, semplice e facile da capire, modificare e mantenere nel tempo. Questo modo di programmare punta a rendere il codice leggibile non solo dalla macchina, ma soprattutto dagli altri sviluppatori (incluso il futuro sé stesso).
Principi chiave di Clean Code:
Leggibilità: il codice deve comunicare chiaramente cosa fa. Nomi di variabili, funzioni e classi devono essere significativi e descrittivi.
Semplicità: evitare logiche complesse o trucchi inutili, preferendo soluzioni lineari (KISS).
Funzioni piccole e focalizzate: ogni funzione deve fare una sola cosa, rispettando il Single Responsibility Principle (SRP).
Evitare la duplicazione (DRY): la logica deve essere scritta una sola volta e riutilizzata, non duplicata.
Uso parsimonioso dei commenti: il codice dovrebbe essere talmente chiaro da limitare i commenti solo a casi speciali o spiegazioni di contesto.
Struttura e formattazione coerenti: spaziatura, indentazione e layout devono facilitare la lettura e la scansione del codice.
Gestione efficace degli errori: usare meccanismi di controllo coerenti e chiari per prevenire comportamenti non previsti.
Testabilità: codice pulito è facile da testare e da automatizzare con unit test e test di integrazione.
Perché scrivere Clean Code?
Investire nel clean code significa ridurre i bug, facilitare le modifiche future, migliorare la collaborazione tra sviluppatori e mantenere elevata la qualità del software. È una mentalità che punta a trasformare il codice da semplice “mezzo per far funzionare un programma” a un prodotto leggibile, stabile e scalabile
Idea: privilegia leggibilità, nomi chiari, funzioni brevi e assenza di side-effect nascosti.
# ❌ Poco chiaro
def f(a, b):
return a * b / 2
# ✅ Autodescrittivo
def area_triangolo(base, altezza):
return base * altezza / 2Tips veloci: nomi espressivi, funzioni piccole, testabili, commenti solo quando aggiungono valore.
Fonti
fonti utilizzate nelle risposte precedenti:
Wikipedia: “Principio di singola responsabilità”, 2012.
FreeCodeCamp: “I princìpi SOLID della programmazione”, 2022.
IonOS Digital Guide: “Principi SOLID”, 2025.
Wikipedia: “Separation of Concerns”, 2003.
Wikipedia: “Legge di Demetra”, 2005.
Wikipedia: “Fail-fast system”, 2006.
Wikipedia: “Convention over configuration”, 2007.
Wikipedia: “Composition over inheritance”, 2010.
Wikipedia: “Principle of least privilege”, 2005.
Codacy Blog: “What Is Clean Code?”, 2023.
