Come comprendere e applicare i principi SOLID in Python

Ultimo aggiornamento: 28/01/2026
Autore: Isaac
  • I principi SOLID definiscono cinque linee guida di progettazione orientate agli oggetti che migliorano la chiarezza, l'estendibilità e la testabilità del codice.
  • Applicare SRP, OCP, LSP, ISP e DIP in Python Ciò implica la separazione delle responsabilità, il ricorso ad astrazioni e la progettazione di gerarchie coerenti.
  • Un progetto SOLID viene combinato con DRY, KISS e YAGNI per ottenere sistemi manutenibili senza sovraprogettazione, adattati al contesto reale del progetto.

Principi SOLID in Python

Nelle righe che seguono vedremo in dettaglio cos'è SOLID, da dove deriva, A cosa serve Python nell'uso quotidiano e come applicarlo con esempi concreti? in questo linguaggio. Inoltre, integreremo altri principi chiave come DRY, KISS e YAGNI, e discuteremo in quali contesti potrebbe non valere la pena essere ultra-puristi con SOLID.

Cos'è SOLID e da dove viene?

Quando parliamo di SOLID, ci riferiamo a cinque principi classici della progettazione orientata agli oggetti che si concentrano su come strutturare classi, moduli e interfacce per rendere il software facile da estendere, testare e manutenere.

Questi principi divennero popolari grazie a Robert C. Martin (zio Bob), uno dei padri dello sviluppo agile e autore di libri come Codice pulito o Architettura pulitaNegli anni '90 e nei primi anni 2000 ha pubblicato diversi articoli sulla progettazione orientata agli oggetti, tra cui "The Principles of OOD" e "Design Principles and Design Patterns".

Successivamente, l'ingegnere Michael Feathers propose l'acronimo SOLID per riferirsi all'insieme dei cinque principi più importanti emersi da quelle opere. L'idea era di avere un semplice regola mnemonica che qualsiasi sviluppatore potrebbe ricordare e applicare alla progettazione della propria classe.

L'acronimo sta per:

  • S - Principio unico di responsabilità (SRP, Principio di responsabilità unica).
  • O - Principio aperto-chiuso (OCP, Principio Aperto/Chiuso).
  • L - Principio di sostituzione di Liskov (LSP, Principio di sostituzione di Liskov).
  • I - Principio di separazione dell'interfaccia (ISP, Principio di segregazione dell'interfaccia).
  • D - Principio di inversione della dipendenza (DIP, Principio di inversione della dipendenza).

Sebbene siano stati originariamente formulati pensando a linguaggi fortemente tipizzati, come Java o C#Questi principi sono perfettamente applicabili a Python, C++, PHP e, in generale, a qualsiasi linguaggio che supporti l'orientamento agli oggetti.

Spiegazione solida di Python

A cosa servono i principi SOLID nei progetti concreti?

Al di là della teoria, SOLID è particolarmente evidente quando il progetto cresce e Ci sono diversi sviluppatori che modificano lo stesso codice per mesi o anni.L'applicazione di questi principi comporta diversi vantaggi molto evidenti:

Da un lato migliorano la leggibilità e chiarezza dell'architetturaLe classi hanno responsabilità definite, le dipendenze sono controllate ed è più facile seguire il flusso aziendale senza perdersi nei dettagli dell'infrastruttura.

Inoltre, favoriscono la riutilizzo ed estensibilità del codice. Se le classi sono ben progettate, aggiungere nuove funzionalità non significa smantellare metà del sistema. La tendenza è quella di estendere il sistema attraverso nuove implementazioni piuttosto che riscrivere ciò che già funziona e viene testato.

Un altro punto fondamentale è il testabilitàRiducendo l'accoppiamento e affidandosi alle astrazioni, il tutto diventa più semplice. iniettare doppio test (mock, stub) e scrivere test unitari rapidi che non dipendono da database chiamate reali, di rete o altri sistemi esterni.

python
Articolo correlato:
Come utilizzare Unittest, Pytest, Mock e altri strumenti Python per l'automazione dei test

Infine, una base di codice che aderisce ai principi SOLID soffre meno di odori di codice, codice corrotto e “codice spaghetti”In altre parole, si degrada meno con il tempoAccumula meno debito tecnico ed è più sicuro e stabile negli ambienti critici.

S – Principio di responsabilità singola in Python

Il principio di responsabilità unica afferma che Una classe dovrebbe avere un unico motivo per cambiare.Ciò non significa che faccia una singola cosa microscopica, ma piuttosto che sia responsabile di un'area di funzionalità ben definita.

Quando una classe combina logica aziendale, accesso ai dati, formattazione dei report e logica di presentazione, ogni modifica in una qualsiasi di queste aree richiede la modifica dello stesso pezzo di codice. Ciò aumenta il rischio di errori incrociati e conflitti tra squadre.

Pensa, ad esempio, ad una classe Utente In Python, questa classe, oltre a rappresentare l'utente, è responsabile della connessione al database, del salvataggio dei dati utente e della generazione di report di testo. Questa classe potrebbe dover essere modificata per diversi motivi: se cambiano gli attributi dell'utente, se il database viene modificato o se il formato del report viene modificato.

L'alternativa allineata con SRP è quella di separare le responsabilità: una classe o dataclass per l' modello di dominio dell'utente, un altro per il persistenza (repository o gateway) e un altro per il generare report o visualizzazioniOgni modifica ricade nella parte corrispondente, riducendo l'accoppiamento.

  Quali sono le reazioni rapide che Instagram può attivare/disattivare? Scarica per Android o iOS

Qualcosa di simile può essere visto con un classico esempio Python: una classe Duck che definisce l'anatra e, allo stesso tempo, gestisce le conversazioni tra le anatre attraverso un metodo greet()Quella parte di comunicazione può essere estratta in un oggetto Communicatorin modo che l'anatra sappia solo volare, nuotare ed emettere suoni, e il comunicatore sappia come articolare un dialogo tra due uccelli.

Questo approccio si adatta molto bene ad altri principi come ASCIUTTO (non ripetere la logica in più classi) e con l'idea di mantenere un alta coesione all'interno di ciascun modulo: tutte le sue funzioni e attributi rimandano alla stessa responsabilità aziendale.

O – Principio aperto/chiuso applicato alle estensioni comportamentali

Il principio aperto/chiuso afferma che Le entità software dovrebbero essere aperte all'estensione ma chiuse alla modificaIn termini quotidiani: dovremmo essere in grado di aggiungere nuove varianti di comportamento senza dover modificare il codice che già funziona.

Un tipico anti-pattern in Python è avere una classe con un metodo che decide il comportamento con un if/elif gigante a seconda del tipo: ad esempio, un Calcolatore di area che controlla se l'oggetto è Rectangle, Circle, Triangle e così via. Ogni nuova forma geometrica impone l'aggiunta di un altro ramo al metodo e richiede la modifica di una classe che è già stata testata.

Rispettare l'OCP implica programmazione contro le astrazioniIn questo caso definiamo una classe astratta. Shape con un metodo area() e facciamo in modo che ogni forma (rettangolo, cerchio, triangolo...) implementi quel metodo. Il calcolatore dell'area invoca semplicemente shape.area() senza sapere a quale tipo specifico si sta rivolgendo.

Continuando con Python, un altro esempio discusso in varie fonti è quello di un oggetto Communicator che viene utilizzato per le conversazioni tra uccelli. Se vogliamo supportare diverse forme di conversazione (semplice, formale, con emoji, ecc.), invece di modificare il metodo communicate() ogni volta definiamo un astrazione della conversazione (per esempio, AbstractConversation) e abbiamo creato implementazioni come SimpleConversation che generano le frasi. Il comunicatore stampa semplicemente ciò che la conversazione restituisce, senza modificare il proprio codice.

Nel mondo degli affari, l'OCP è fondamentale per ridurre i rischi quando si introducono modifiche: evita di riaprire classi base ampiamente utilizzate e promuove... estensione attraverso nuove sottoclassi o strategie, mantenendo il codice stabile il più intatto possibile.

L – Principio di sostituzione di Liskov ed ereditarietà corretta

Il principio di sostituzione di Liskov afferma che Gli oggetti di una sottoclasse dovrebbero essere in grado di sostituire quelli della loro superclasse senza interrompere il sistemaIn altre parole, se un codice si aspetta un'istanza della classe base, dovrebbe funzionare allo stesso modo se gli forniamo un'istanza di una qualsiasi classe figlia.

In pratica, ciò significa che la sottoclasse non dovrebbe rompere le aspettative o il contratto che definisce la classe base: i tipi di ritorno, le eccezioni, le precondizioni e le postcondizioni devono rimanere coerenti.

Un esempio ben noto (e ingannevole) è quello della gerarchia Rettangolo / QuadratoMatematicamente, un quadrato è un rettangolo, ma nel codice questo non è sempre comodo. Square ereditato direttamente da RectangleSe il rettangolo ha parametri separati per altezza e larghezza, un metodo di test può presumere che modificando solo l'altezza la larghezza rimanga invariata. Se la classe square forza entrambe le variabili a modificarsi simultaneamente, il contratto della superclasse si interrompe improvvisamente.

Qualcosa di simile accade nelle gerarchie come Uccello In Python. Se la classe base include un metodo fly()Ma poi abbiamo sottoclassi come Ostrich o pinguini che non possono volare, forzare tale implementazione e lanciare un NotImplementedError interrompe LSP. Qualsiasi funzione che riceve un Bird e chiama fly() Dovrei riuscire a farlo senza sorprese.

La soluzione prevede la riprogettazione della gerarchia: viene mantenuta una classe base generica Uccello senza il metodo fly()e vengono create sottoclassi intermedie come Uccello volante y Uccello nuotatoreGli uccelli che volano implementano il primo; quelli che nuotano, il secondo; e quelli che fanno entrambe le cose ereditano da entrambe. In questo modo, nessuna sottoclasse è costretta a offrire un comportamento che non ha senso per essa.

Il rispetto dell'LSP richiede un'attenta considerazione delle relazioni di ereditarietà in Python e in molti casi porta a preferire composizione e interfacce semplici di fronte ad alberi di ereditarietà profondi in cui è facile violare il contratto di superclasse.

I – Principio di segregazione dell’interfaccia e classi più specifiche

Il principio di segregazione dell'interfaccia afferma che Non dovremmo obbligare un cliente ad affidarsi a metodi che non utilizza.In pratica, ciò si traduce nella progettazione di piccole interfacce specifiche (o classi astratte in Python) anziché di un'unica interfaccia gigante per tutto.

  5 migliori programmi per i sottotitoli

Immagina un'interfaccia Lavoratore in Python che definisce i metodi work() y eat()Questa interfaccia ha senso per un essere umano che lavora e mangia, ma non per un robot che si limita a lavorare. Se forziamo la classe Robot implementare eat(), terminerà con un metodo vuoto o un'eccezione, indicando che "il robot "Non mangiano", il che è un chiaro segno di violazione dell'ISP.

La soluzione consiste nel dividere quell'interfaccia in Lavorabile (qualcosa che può funzionare) e commestibile (qualcosa che puoi mangiare). Quindi, la classe Human implementa entrambi, mentre Robot Implementa solo la parte operativa. Ogni cliente si affida solo ai metodi di cui ha effettivamente bisogno.

Tornando agli uccelli, qualcosa di simile accade con un'interfaccia che combina metodi come fly() y swim()Non tutti gli uccelli fanno entrambe le cose, quindi ha più senso separarli in Uccello volante y Uccello nuotatoreIl design risultante è più flessibile ed evita di dover riempire metodi irrilevanti con codice fittizio.

In Python, sebbene non abbiamo interfacce formali come in Java, possiamo ottenere lo stesso effetto usando classi astratte del modulo abc o protocolli di tipizzazione. La segregazione delle interfacce rende ogni classe più definita e riduce la probabilità di introdurre dipendenze non necessarie.

D – Principio di inversione della dipendenza e progettazione basata sull’astrazione

Il principio di inversione della dipendenza evidenzia due idee chiave: I moduli di alto livello non dovrebbero dipendere dai moduli di basso livello e entrambi devono dipendere da astrazioniInoltre, le astrazioni non dovrebbero dipendere dai dettagli; i dettagli dipendono dalle astrazioni.

Un errore molto comune è che una classe di logica aziendale o di dominio in Python crei direttamente un'istanza concreta di un database, di un servizio HTTP o di un client di messaggistica: ad esempio, un Repository utente che internamente fa self.database = MySQLDatabase()Se domani volessimo passare a PostgreSQL o a un motore in memoria per i test, dovremo modificare quella classe.

L'applicazione del DIP comporta l'introduzione di un astrazione del database (ad esempio, una classe astratta) Database con metodi connect() y query()) e creare implementazioni concrete come MySQLDatabase o PostgreSQLDatabaseIl repository smette di creare istanze del database stesso e inizia a riceverle dall'esterno, solitamente tramite il costruttore.

Questo modello è noto come iniezione di dipendenza Ed è uno dei pilastri per ottenere un codice modulare e testabile: in produzione iniettiamo un'implementazione reale; in fase di test iniettiamo un doppio test che simula il comportamento senza toccare sistemi esterni.

In un esempio più ricco, si può definire un'astrazione di un canale di comunicazione, come Canale astratto, e un'astrazione del comunicatore, Comunicatore astratto, con chi lavora AbstractConversationInvece SMSCommunicator crea internamente un SMSChannelLa versione allineata a DIP garantisce che il comunicatore riceva un canale astratto nel costruttore. Ciò significa che non dipende da una classe concreta, ma da un'interfaccia, e può funzionare con qualsiasi tipo di canale che soddisfi il contratto.

In sintesi, DIP incoraggia la progettazione di livelli in cui la logica aziendale si basa su interfacce generiche e framework, configurazioni o factory sono responsabili del collegamento di queste interfacce con implementazioni specifiche a seconda dell'ambiente.

Esempio pratico in Python: servizio di notifica SOLID

Un caso molto esemplificativo per vedere come vengono combinati diversi principi di progettazione è quello di un sistema di notifica che può inviare messaggi attraverso diversi canali, come e-mail o SMS, ed è anche facile da espandere con nuovi mezzi (push, webhook, ecc.).

L'obiettivo è avere la logica principale che decide "Devo notificare la cosa X" non deve essere riscritto ogni volta che aggiungiamo un canale, e che possiamo testarlo senza dover attivare vere e-mail o SMS durante i test.

Per fare questo, definiamo un'astrazione Notifier con un metodo send(to, subject, body) che restituisce un valore booleano che indica successo o fallimento. Da lì abbiamo creato implementazioni come EmailNotifier y SMSNotifierognuno riceve un client esterno nel suo costruttore (ad esempio, smtp_client o sms_client) incaricato di parlare con il servizio reale.

Oltre a tutto questo, un Servizio di notifica che riceve un elenco di notificatori (qualsiasi cosa che implementi il ​​contratto) e offre un metodo per inviare una notifica tramite tutti i canaliQuesto servizio non sa nulla di SMTP, provider SMS o API HTTP: si limita a iterare e chiamare send() su ciascun notificatore.

Questo progetto dimostra chiaramente diversi principi in azione:

  • SRPOgni classe ha una responsabilità molto limitata: una EmailNotifier Gestisce solo le email, SMSNotifier Solo gli SMS, il servizio orchestra e solo i clienti esterni conoscono il trasporto.
  • DIP: NotificationService Dipende dall'astrazione Notifiernon di classi specifiche e i notificatori a loro volta dipendono dai client iniettati.
  • OCP: aggiungere un PushNotifier o WebhookNotifier Non c'è bisogno di toccare NotificationServiceBasta creare una nuova implementazione e registrarla.
  • LSP e ISP: tutte le implementazioni di Notifier Rispettano lo stesso contratto send(), senza metodi obbligatori che non hanno senso per un canale specifico.
  Il modo semplice per capovolgere, ruotare o capovolgere un'immagine in Photoshop

Dal punto di vista dei test, è sufficiente creare uno o più DummyNotifier che registrano le chiamate a send() senza fare altro. Iniettandole nel servizio, possiamo verificare che tutte le notifiche siano state attivate senza bisogno di reti, code o sistemi esterni.

Questo esempio può anche essere completato con principi quali ASCIUTTO (evitare di duplicare la logica del formato comune), BACIO (non introdurre nuovi tentativi, code o priorità finché non è realmente necessario) e YAGNI (Non aspettatevi funzionalità che non siano presenti nella nostra tabella di marcia immediata).

Relazione di SOLID con altri principi di progettazione

Sebbene SOLID si concentri sulla progettazione orientata agli oggetti, in pratica è combinato con altri principi generali di progettazione del software come ASCIUTTO, BACIO e YAGNIche compaiono anche in modo ricorrente nella letteratura sul codice pulito.

ASCIUTTO (non ripetere te stesso) Ricordate che la logica di business più importante dovrebbe avere una sola rappresentazione nel sistema. Questo si adatta perfettamente a SRP e OCP: se ripetiamo un comportamento in classi diverse, estendere o correggere un caso d'uso richiede modifiche in molti punti e aumenta il rischio di incoerenze.

BACIO (Mantienilo semplice, stupido) Ci incoraggia a evitare architetture e astrazioni inutilmente complesse. Sebbene possa sembrare contraddittorio, applicare SOLID non significa riempire il progetto di interfacce fine a se stesse, ma piuttosto introdurne solo il necessario per mantenere... design comprensibile per il team.

YAGNI (Non ne avrai bisogno) È un antidoto all'eccesso di progettazione. Nei progetti con requisiti altamente volatili, cercare di anticipare tutti i possibili cambiamenti futuri aggiungendo livelli su livelli può essere controproducente. L'approccio ideale è trovare un equilibrio: applicare SOLID alle parti che sappiamo cresceranno o cambieranno e semplificare ciò che sappiamo essere stabile.

Presi insieme, tutti questi principi portano ad uno stile di sviluppo in cui la priorità è codice chiaro, modulare e orientato alla collaborazionema senza cadere nel feticismo dell'architettura fine a se stessa.

Vantaggi, limiti e casi in cui essere puristi non paga

I principi SOLID sono considerati quasi un regola d'oro nei progetti di medie e grandi dimensioniMa questo non significa che debbano essere applicati alla lettera in ogni contesto. È anche importante conoscerne i limiti.

Nelle applicazioni molto piccole, negli script una tantum o nei progetti a basso budget che non saranno mantenuti a lungo termine, introdurre troppi livelli di astrazione può essere problematico. più costoso che vantaggiosoL'onere mentale e di codifica implicato nell'applicare DIP o ISP a tutto potrebbe non valerne la pena.

Ci sono anche situazioni con requisiti di tempo molto ristretti o lo sviluppo di un MVP in cui la velocità di consegna è una priorità. Ha senso posticipare parte del perfezionamento SOLID a un momento successivo, quando il prodotto si sarà stabilizzato e sapremo quali componenti sopravvivranno.

Nei sistemi legacy con molto codice legacyCercare di "consolidare" tutto in una volta è solitamente irrealistico. In questi scenari, è meglio applicare i principi in modo incrementale alle aree del codice che vengono utilizzate di frequente o che bloccano nuove funzionalità.

Infine, ci sono domini che sono molto sensibili a prestazioni estreme (ad esempio, alcune routine ad alta frequenza) in cui i livelli di astrazione possono introdurre un sovraccarico indesiderato. In questi casi, a volte è giustificabile sacrificare parte della purezza di SOLID in blocchi molto specifici, a condizione che il resto del sistema rimanga ben progettato.

Nel complesso, comprendere e applicare i principi SOLID in Python consente di passare da un codice che "funziona e basta" a un codice che Resiste bene alla prova del tempoSi adatta meglio ai cambiamenti aziendali, facilita la collaborazione in team e riduce le sorprese in produzione; la chiave è usarli giudiziosamente, sapendo quando vale la pena introdurre un'altra astrazione e quando è meglio mantenere le cose semplici e dirette.