:::: MENU ::::

S.O.L.I.D. ovvero i 5 principi della programmazione ad oggetti

Il termine SOLID  viene utilizzato per indicare i cinque principi di progettazione orientata agli oggetti (OOD) di Robert C. Martin, conosciuto al mondo come zio Bob. I principi SOLID sono intesi come linee guida per lo sviluppo di software estendibile e manutenibile, in particolare nel contesto di pratiche di sviluppo agili e fondate sull’identificazione di code smell e sul refactoring. La parola SOLID è un acronimo che serve a ricordare tali principi (Single responsibility, Open-closed, Liskov substitution, Interface segregation, Dependency inversion), e fu coniata da Michael Feathers.

  • S – Single-responsiblity principle
  • O – Open-closed principle
  • L – Liskov substitution principle
  • I – Interface segregation principle
  • D – Dependency Inversion Principle

Single-responsibility Principle

S.R.P in breve, questo principio dice:

A class should have one and only one reason to change, meaning that a class should have only one job.

Il principio afferma che ogni classe dovrebbe avere una ed una sola responsabilità, interamente incapsulata al suo interno. Facciamo un esempio concreto: diciamo di avere alcune classi che rappresentano forme geometriche e che vogliamo avere la somma delle aree di tutte le forme istanziate. Fin qui sembra abbastanza facile. Per prima cosa bisogna implementare tutte le forme, in questo modo:

Successivamente avremo bisogno di un’altra classe che implementi il metodo per la somma e per visualizzare l’output della somma a schermo. Chiamiamo questa classe AreaCalculator:

A questo punto basterà istanziare degli oggetti di tipo forma circle o square e passarle tramite array a un oggetto che si occupi di sommare e stampare il risultato:

Il problema è che l’oggetto AreaCalculator che abbiamo istanziato gestisce le operazioni di output dei dati. Cosa succederebbe se l’utente volesse visualizzare i dati in formato JSON o XML o qualcos’altro? Bisognere implementare tutta questa logica all’interno di AreaCalculator, ma verrebbe meno il principio di singola responsabilità: infatti AreaCalculator dovrebbe occuparsi solo di sommare le aree delle forme e non preoccuparsi di come l’utente vuole visualizzare i dati, siano essi JSON, XML, HTML.

Per risolvere il problema si crea una nuova classe che chiameremo SumCalculatorOutputter che implementa la logica per gestire le modalità di visualizzazione dei dati calcolati di tutte le forme. A questo punto aggiungendo qualche riga a quanto definito sopra, si avrebbe:

Open-closed principle

O.C.P. in breve, questo principio dice:

Objects or entities should be open for extension, but closed for modification.

Il principio afferma che un oggetto o un’entità (software) dovrebbe essere aperta alle estensioni, ma chiusa alle modifiche. In riferimento alla classe già definita sopra, ecco un esempio del metodo somma() per chiarire le idee:

Se avessimo voluto calcolare l’area di altri tipi di forme (rettangolo, trapezio, rombo, etc…) avremmo dovuto aggiungere più costrutti if / else. Ciò va evidentemente contro la principio di OCP.

Una soluzione al problema è quella di rimuovere la logica di calcolo dell’area dal metodo somma() e implementare un metodo area() direttamente in ogni classe forma. Per la classe Quadrato, quindi avremo:

La stessa cosa avviene per la classe Cerchio e per tutte le classi forme che verranno implementate successivamente. A questo punto per calcolare la somma delle aree, basterà che il metodo somma() contenga le poche righe di codice qui sotto. In questo modo, senza modificare righe di codice, possiamo calcolare l’area di nuove forme:

Infine, per essere sicuri che la classe contenga un metodo area(), possiamo creare un’interfaccia che implementi il metodo all’interno di ogni singola classe. In questo modo:

Quindi il metodo somma() si trasforma definitivamente, aggiungendo un if che controlla se l’oggetto forma è di tipo InterfacciaForme, quindi che abbia al suo interno implementato il metodo area(), altrimenti genera un eccezione.

Liskov substitution principle

L.S.P. in breve, afferma che:

Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.

Se q(x) è una proprietà che si può dimostrare essere valida per oggetti x di tipo T, allora q(y) deve essere valida per oggetti di tipo S dove S è un sottotipo di T. Più semplicemente B è un sottotipo di A se e solo se, per ogni programma che usi oggetti di classe A, posso utilizzare al loro posto oggetti di classe B e lasciare immutato il comportamento “logico” del programma.

Se vogliamo, ancora più semplicemente: gli oggetti dovrebbero poter essere sostituiti con dei loro sottotipi, senza alterare il comportamento del programma che li utilizza.

Facciamo un esempio con la classe AreaCalculator. Diciamo di voler implementare una classe chiamata VolumeCalculator che estende la classe AreaCalculator.

Nel SumCalculatorOutputter avremo:

Tuttavia, quando si istanziano gli oggetti di tipo Quadrato e Cerchio e si cerca di invocare il metodo HTML (il quale richiama il metodo somma()), verrà prodotto un E_NOTICE, che ci informa della conversione di array di stringhe. Per risolvere il problema, bisogna trasformare il metodo somma() nella classe VolumeCalculator, in modo da ritornare non un array di valori, ma un singolo valore float, double o int.

Interface segregation principle

I.S.P. in breve, dice che:

A client should never be forced to implement an interface that it doesn’t use or clients shouldn’t be forced to depend on methods they do not use.

Una classe client non dovrebbe dipendere da metodi che non usa, e che pertanto è preferibile che le interfacce siano molte, specifiche e piccole (composte da pochi metodi) piuttosto che poche, generali e grandi.

Riprendendo l’esempio precedente, se volessimo aggiungere un nuovo metodo per calcolare il volume, possiamo definire l’intestazione nell’interfaccia InterfacciaForme. Se volessimo seguire il principio di segregazione delle interfacce allora conviene definire due interfacce distinte (InterfacciaForme e SolidInterfacciaForme), in questo modo:

Questo approccio funziona ed è valido ma se vogliamo evitare qualsiasi tipo di trappole possiamo creare una nuova interfaccia chiamata GestioneInterfacciaForme e implementarla nelle classi di tipo forme piatte e solide. In questo modo basterà un singolo metodo calcola() per gestire i calcoli di qualsiasi tipo di forma, sia esso volume o area.

Ora, in AreaCalculator, si può facilmente sostituire la chiamata al metodo area() con il metodo calcola() e verificare se l’oggetto è un’istanza di GestioneInterfacciaForme e non InterfacciaForme.

Dependency Inversion Principle

D.I.P. in breve, afferma che:

Entities must depend on abstractions not on concretions. It states that the high level module must not depend on the low level module, but they should depend on abstractions.

Ovvero, i moduli di alto livello non devono dipendere da quelli di basso livello. Entrambi devono dipendere da astrazioni; Le astrazioni non devono dipendere dai dettagli; sono i dettagli che dipendono dalle astrazioni. Detta così, potrebbe sembrare un tantino confusionario. Il principio di inversione delle dipendenze aiuta a disaccoppiare il codice in modo tale che le classi dipendano da astrazioni piuttosto che da implementazioni concrete. Ma vediamo di capire meglio con un esempio:

La classe PasswordReminder è di alto livello, mentre la classe MySQLConnection è di basso livello. Questo costrutto viola il principio DIP poichè la classe PasswordReminder è costretta a dipendere dalla classe MySQLConnection. In seguito se si dovesse cambiare il motore di database, è necessario modificare il PasswordReminder e ciò comporta anche una violazione al principio OCP. Il PasswordReminder non dovrebbe preoccuparsi di quale database viene utilizzato nell’applicazione: per risolvere questo problema, possiamo creare l’interfaccia:

L’interfaccia dispone di un metodo di connessione chiamato connect() e la MySqlConnection implementa questa interfaccia. In questo modo, anzichè utilizzare MySqlConnection all’interno del costruttore di PasswordReminder, possiamo utilizzare l’interfaccia e cosi facendo non ci importa più il tipo di database che viene utilizzato. PasswordReminder può facilmente connettersi al database senza problemi e il principio OCP non è violato.

 

I principi SOLID sono strumenti da prendere in considerazione sia quando si scrive codice o quando si effettua il refactoring di sistemi legacy.

La sensazione dei programmatori meno esperti che si imbattono in codice e framework progettati con i principi SOLID sia quello di avere a che fare con sistemi “over-engineered”. In realtà l’utilizzo di questi principi, meglio se utilizzati in combinazione tra di essi, aiuta gli sviluppatori ad estendere, modificare e testare il codice in modo più efficace.

 


Comments are closed.

By continuing to use the site, you agree to the use of cookies. more information

The cookie settings on this website are set to "allow cookies" to give you the best browsing experience possible. If you continue to use this website without changing your cookie settings or you click "Accept" below then you are consenting to this.

Close