Torna al blog

SOLID: I primi 5 principi della progettazione orientata agli oggetti?

SOLID: I primi 5 principi della progettazione orientata agli oggetti?

Introduzione

SOLID è un acronimo mnemonico per cinque principi di progettazione orientata agli oggetti che sono stati introdotti da Robert C. Martin, popolarmente noto come Uncle Bob. Questi principi hanno lo scopo di aiutare progettisti, architetti, ingegneri e sviluppatori di software a creare software più flessibile, manutenibile e scalabile. Seguendo questi principi, è possibile progettare classi più facili da testare, sottoporre a refactoring, riutilizzare ed estendere.

L'acronimo SOLID sta per:

S – Principio di singola responsabilità

O – Principio di apertura-chiusura

L – Principio di sostituzione di Liskov

I – Principio di segregazione delle interfacce

D – Principio di inversione delle dipendenze

In questo articolo spiegheremo ogni principio individualmente per capire come può aiutarti a scrivere codice migliore. Inoltre, aggiungeremo frammenti di codice per ciascun principio per mostrarti come applicarli nel tuo percorso di programmazione, oltre a cosa dovresti evitare nell'architettura del codice pulito. Per dimostrare i concetti, utilizzeremo il linguaggio di programmazione Kotlin sviluppato da JetBrains e dai contributori open-source.

Principio di singola responsabilità

Il Principio di singola responsabilità (SRP) è un principio di progettazione software che stabilisce che ogni classe o modulo in un programma dovrebbe avere una singola responsabilità ben definita. Ciò significa che una classe dovrebbe avere un solo motivo per cambiare e dovrebbe essere responsabile di una sola parte delle funzionalità del programma.

Considera il seguente frammento di codice come esempio di come questo principio possa essere applicato in Kotlin:

La data  keyword in Kotlin indica che questa classe è una data class, il che significa che è destinata a contenere dati e non ha alcun comportamento complesso. Utilizzando questa data class, possiamo creare una classe UserService  per la gestione degli utenti come mostrato di seguito:

In questo esempio, la classe UserService ha una singola responsabilità: gestire gli utenti in un database. Ogni utente è rappresentato dal codice data class User  condiviso in prevenzione. Tutti i metodi nella classe UserService sono correlati a questa responsabilità e sono quindi coesi. Ciò rende la classe più facile da comprendere e mantenere, poiché è chiaro che tutti i metodi al suo interno sono correlati a un unico compito ben definito.

Un metodo che viola il Principio di singola responsabilità (SRP) nella classe UserService sarebbe uno non correlato alla responsabilità primaria della classe, ovvero la gestione degli utenti in un database. Ad esempio, considera la seguente variante della classe UserService:

In questo esempio, il sendEmail  metodo non si riferisce alla responsabilità primaria della classe UserService, che consiste nel gestire gli utenti in un database. Questo metodo è responsabile dell'invio di email, che è una preoccupazione separata rispetto alla gestione degli utenti. Di conseguenza, questo metodo viola l'SRP, poiché introduce un secondo motivo di modifica per la classe UserService.

Per aderire al Single Responsibility Principle, sarebbe meglio separare la funzionalità di invio email in una classe distinta, come una classe EmailService . Ciò consentirebbe alla classe UserService di concentrarsi sulla sua responsabilità primaria di gestire gli utenti, e alla classe EmailService di concentrarsi sulla sua responsabilità di inviare email.

Si noti che il Single Responsibility Principle non riguarda il numero di metodi di una classe, ma piuttosto la coesione dei metodi e la chiara separazione delle responsabilità all'interno di una classe.

Open-Closed Principle

L'Open-Closed Principle (OCP) è un principio di progettazione software che stabilisce che le entità software (come classi, moduli o funzioni) dovrebbero essere aperte all'estensione, ma chiuse alle modifiche. Ciò significa che dovrebbe essere possibile aggiungere nuove funzionalità a una classe o a un modulo senza modificarne il codice esistente.

Considera la classe UserService dell'esempio precedente. Supponiamo di voler aggiungere una nuova funzionalità alla classe UserService che ci consenta di cercare gli utenti per indirizzo email. Un modo per farlo sarebbe aggiungere un nuovo metodo alla classe UserService come evidenziato nel codice qui sotto:

Questo approccio funziona, ma viola l'Open-Closed Principle, poiché abbiamo dovuto modificare la classe esistente UserService per aggiungere la nuova funzionalità. Un approccio migliore consisterebbe nell'utilizzare l'ereditarietà o la composizione per estendere le funzionalità della classe UserService senza modificarne il codice.

Per ottenere questo risultato, potremmo creare una nuova classe chiamata UserSearchService  che estende la classe UserService e aggiunge la funzionalità di ricerca per email:

In questo esempio, la classe UserSearchService è aperta all'estensione, poiché fornisce funzionalità aggiuntive rispetto a quelle offerte dalla classe UserService classe. Allo stesso tempo, la classe UserService rimane chiusa alle modifiche, in quanto non abbiamo avuto bisogno di modificare il suo codice per aggiungere la funzionalità di ricerca via email.

Principio di Sostituzione di Liskov

Il Principio di Sostituzione di Liskov (LSP) è un principio di progettazione software che stabilisce che gli oggetti di una superclasse dovrebbero poter essere sostituiti con oggetti di una sottoclasse senza influire sulla correttezza del programma. Ciò significa che una sottoclasse dovrebbe essere un valido sostituto per la sua superclasse e dovrebbe comportarsi allo stesso modo della superclasse quando utilizzata nello stesso contesto.

Continueremo la dimostrazione utilizzando le classi User e UserService dei precedenti esempi. Per consentire alla classe User di essere estesa, utilizziamo la open  parola chiave in Kotlin:

Ecco la classe originale UserService che utilizza la data class User di cui sopra:

Supponiamo di voler creare una sottoclasse di User chiamata AdminUser che rappresenta gli utenti con privilegi amministrativi. Potremmo farlo in questo modo:

In questo esempio, la classe AdminUser è un valido sostituto per la classe User , poiché si comporta allo stesso modo della classe User e può essere utilizzata ovunque sia previsto un oggetto User. Ad esempio, possiamo utilizzare la classe AdminUser con la classe UserService  in questo modo:

Questo codice è corretto, poiché la classe AdminUser è un valido sostituto per la classe User e può essere utilizzata allo stesso modo di un oggetto User.

È importante notare che il Principio di Sostituzione di Liskov va oltre la semplice ereditarietà. Si tratta di garantire che gli oggetti di una sottoclasse si comportino allo stesso modo degli oggetti della superclasse, indipendentemente da come la sottoclasse è implementata. Ad esempio, se la classe AdminUser dovesse comportarsi diversamente dalla classe User in qualche modo, violerebbe il Principio di Sostituzione di Liskov, poiché non sarebbe un valido sostituto per la classe User.

Principio di Segregazione delle Interfacce

Il Principio di Segregazione delle Interfacce (ISP) è un principio di progettazione software che stabilisce che i client non dovrebbero essere forzati a dipendere da interfacce che non utilizzano. Ciò significa che in genere è una buona idea creare piccole interfacce mirate che fanno bene una sola cosa, piuttosto che creare grandi interfacce di uso generale che cercano di fare molte cose.

Ecco un esempio di come questo principio possa essere applicato riscrivendo le classi User  e UserService  dei precedenti esempi:

In questo esempio, l'interfaccia UserService definisce quattro metodi relativi alla gestione degli utenti in un database. La classe DatabaseUserService implementa questa interfaccia e fornisce implementazioni concrete per questi metodi.

Supponiamo di voler aggiungere una nuova funzionalità all'interfaccia UserService che ci consenta di cercare gli utenti per indirizzo email. Un modo per farlo sarebbe aggiungere un nuovo metodo all'interfaccia UserService:

Il codice non funzionerà, a meno che non si implementi questo metodo anche in tutte le classi che implementano l'interfaccia UserService:

Sebbene questo approccio funzioni, viola l'Interface Segregation Principle, poiché costringe la classe DatabaseUserService ad implementare un metodo ( searchUsersByEmail ) di cui potrebbe non aver bisogno o che potrebbe non utilizzare.

Un approccio migliore sarebbe quello di creare una interfaccia separata per la funzionalità di ricerca email:

Ora abbiamo interfacce separate, piccole e mirate, ovvero UserService e UserSearchServiceche hanno una singola responsabilità. Una classe che richiede tutte le funzionalità di queste interfacce può implementarle come mostrato nello snippet di codice qui sotto:

Questo rispetta il principio di segregazione delle interfacce, in quanto garantisce che i client (come la DatabaseUserService classe) non siano costretti a dipendere da interfacce che non utilizzano.

Per comprendere meglio questo concetto, supponiamo di avere un'altra classe chiamata MemoryUserService  che implementa l'interfaccia UserService  ma non ha bisogno della funzionalità di ricerca email, possiamo scrivere il codice in questo modo:

In questo esempio, la classe MemoryUserService deve solo implementare i metodi definiti nell'interfaccia UserService, e non deve preoccuparsi della funzionalità di ricerca email. Questo consente alla classe MemoryUserService di concentrarsi sulla sua responsabilità primaria di gestire gli utenti in memoria, invece di essere costretta a implementare funzionalità non correlate.

Principio di inversione delle dipendenze

Il principio di inversione delle dipendenze (DIP) è un principio di progettazione software che stabilisce che i moduli di alto livello non dovrebbero dipendere da moduli di basso livello, ma entrambi dovrebbero dipendere da astrazioni. Ciò significa che in genere è una buona idea progettare il software in modo tale che i componenti di alto livello non siano legati a implementazioni specifiche di componenti di basso livello, ma dipendano piuttosto da astrazioni (come interfacce o classi astratte) che possono essere implementate in modi diversi.

Vediamo un esempio di come questo principio possa essere applicato alle classi User e UserService utilizzate negli snippet di codice precedenti:

In questo esempio, la classe UserService dipende da un'astrazione chiamata interfaccia UserRepository, invece di dipendere da un'implementazione specifica di un repository utente. Questo ci consente di implementare l'interfaccia UserRepository in modi diversi, ad esempio con un database o un'archiviazione in memoria, senza influire sulla classe UserService.

Ad esempio, ecco un'implementazione dell'interfaccia UserRepository che utilizza un database:

Ecco un'altra implementazione dell'interfaccia UserRepository che utilizza l'archiviazione in memoria:

Questo rende il sistema più flessibile e manutenibile, in quanto ci consente di modificare l'implementazione del repository senza influire sul resto del sistema. Inoltre, rende più semplice testare la classe UserService, poiché possiamo simulare la dipendenza UserRepository nei nostri test.

Conclusione

In questo articolo abbiamo parlato dei cinque principi del SOLID  codice e frammenti di codice condivisi che soddisfano ciascun principio. L’adesione a questi principi può aiutarti a progettare sistemi software più flessibili, manutenibili e scalabili. Tuttavia, è importante tenere presente che questi principi sono linee guida, piuttosto che regole rigide e assolute, e spetta allo sviluppatore decidere quando e come applicarli nel contesto del proprio progetto specifico. Continua ad apprendere dando un’occhiata al nostro blog per articoli più approfonditi e aggiornati e tutorial su cloud computing e DevOps, progettazione e sviluppo software, tendenze tecnologiche da tenere d’occhio e altro ancora.

Buona programmazione!

author

Preslav Dobrev

Autore · CloudSigma

Preslav Dobrev è un designer creativo presso CloudSigma, con un focus su un'identità aziendale coerente attraverso l'uso di canali di marketing tradizionali e innovativi. È abile nel fondere la visione artistica con il marketing strategico per creare narrazioni di brand di grande impatto.

Commenti

Ancora nessun commento. Scrivi il primo.