lunedì 5 aprile 2021

Refactoring: improving abstraction

 Astrazione

L'astrazione consente di semplificare le entità attraverso la riduzione dei dettagli non rilevanti e la generalizzazione, operata individuandone le caratteristiche principali.
L'astrazione è cruciale nel problem solving per poter decomporre un sistema reale in componenti e stabilire le loro connessioni in modo da poterlo gestire più efficacemente.


I principi che dovrebbero guidare la progettazione e l'individuazione delle varie entità per comporre un modello di un sistema sono:
  • Identificare  e definire il contorno di un "oggetto"
  • Gli oggetti del dominio applicativo dovrebbero avere corrispondenza nel suo modello logico o concettuale. E' molto più facile parlare di oggetti che  hanno una corrispondenza nel mondo reale che di oggetti completamente astratti, pensiamo al caso in cui si voglia validare il modello con il cliente, o anche solo parlarne con un collega.
  • Ogni componente  del modello dovrebbe essere un oggetto "completo" di tutte le funzioni necessarie per operarvi. "Chiuso" potrebbe essere un altro aggettivo applicabile.
  • Ogni componente del modello dovrebbe avere un unico scopo e che abbia sufficiente complessità da garantirgli il diritto ad esistere
  • Non creare entità simili, ma gestire correttamente l'astrazione e la generalizzazione. Ad esempio non avrebbe senso sviluppare da zero una classe "lista di ordini" e "lista di fatture", ma può avere senso una classe "lista" generica su cui operare a prescindere dal contenuto, e poi in qualche modo ottenere le due classi specifiche a partire da quella

Vediamo allora dei possibili problemi che possiamo riscontrare in un sistema e come porvi rimedio.


Astrazione mancante

Un sintomo è la presenza di pezzi di codice simili in varie parti del programma al fine di ottenere gli stessi obiettivi. 
Le cause possono essere una progettazione incompleta, o manutenzioni di tipo quick-fix, o si è pensato di cercare di migliorare la velocità di esecuzione a scapito della modularità.

La soluzione consiste di solito nell'introdurre nuove classi, e sostituirle ai tipi semplici usati nel codice esistente, o in alternativa raggruppare strutture dati in classi e gestirle con i metodi della classe.
Questo refactoring favorirà la manutenibilità, l'estendibilità, la riusabilità e la testabilità, tuttavia bisogna stare attenti a non eccedere e creare classi per funzioni davvero troppo semplici (over-engineering)

Astrazione imperativa

Questo problema si evidenzia quando ci sono funzioni che sono state erroneamente trasformate in classi. Un sintomo è il nome della classe, di solito costituito da un verbo, come "leggiOrdini" o "stampaFatture". 
L'errore consiste nel fatto che la classe non rappresenta un entità del sistema, ma un'azione che si può compiere su essa. Può derivare da una cattiva traduzione da un programma di tipo procedurale ad uno ad oggetti, o una cattiva applicazione dell'OOP.
La soluzione può consistere nella creazione di un metodo ad una classe esistente, o la creazione di una nuova classe avente come oggetto i dati su cui operava la precedente classe, e magari altri metodi inerenti gli stessi dati.
Anche in questo caso il refactoring migliorerà la riusabilità (è molto meglio avere una classe che gestisce un'informazione "in toto" che non avere n classi per ogni operazione che si vuole svolgere su essa. Anche la leggibilità e la testabilità ne avranno beneficio, potendo collegare tali funzioni ai dati della classe e non a dati generici non strutturati o addirittura dislocati altrove.

Astrazione incompleta

Accade quando una classe non fornisce tutti i metodi necessari per operare sull'oggetto da essa gestito.
Il risultato è che per eseguire tali operazioni ci sono metodi esterni o codice duplicato sparso nel programma.
La causa potrebbe essere una progettazione incompleta che non ha previsto la realizzazione di una certa funzione, seguita da manutenzioni quick-fix operate su classi diverse.
Un indizio a cui prestare attenzione è la presenza di alcune funzioni, che di solito vanno in coppia, senza la presenza del proprio "simmetrico", come ad esempio la presenza di un min senza un max, o similmente per first/last, push/pop, left/right, open/close, insert/delete, start/stop e via dicendo.
La soluzione è certamente creare i metodi mancanti con attenzione ai parametri per poterli usare nel codice sostituendo lo spaghetti-code presente al momento.
Inutile dire che la manutenibilità ne avrà gran giovamento, così come la leggibilità e la testabilità.

Astrazione sovraccarica

Accade quando ad una classe sono addossate troppe responsabilità. E' un problema simile a quello che abbiamo già trovato quando abbiamo parlato di modularità (insufficiente in questo caso).
In questo caso abbiamo una ridotta manutenibilità e maggiore impatto delle modifiche della classe nei confronti del resto del sistema.
A volte si presenta quando si vuole gestire con la stessa classe dei  dati eterogenei, con metodi tuttavia simili. E' spesso complicato correggere tale situazione, perché per la sua natura è usata in molti punti. Il risultato è la mancanza di coesione e tutto quel che ne consegue.
Spesso la  soluzione è estrarre tante classi quante sono le diverse responsabilità che sono state in essa sovrapposte, ed usare questa nella classe stessa o direttamente nei client esistenti.

Astrazione inutilizzata

Si ha quando una classe è del tutto inutilizzata, ossia addirittura suo codice risulta irraggiungibile.
Può succedere se è stata progettata ma poi abbandonata in favore di altre classi, o era una classe astratta poi non seguita da alcuna implementazione concreta.
La progettazione non dovrebbe dare luogo a questo genere di classi, bensì dovrebbero essere sviluppate solo le classi che realmente sono necessarie. Cercare di prevedere futuri bisogni in maniera speculativa ed iperprudenziale tende a creare queste classi. In alternativa può capitare che in manutenzioni quick-fix siano state create nuove classi in sostituzione di quelle inutilizzate invece di manutenerle, per timore di creare malfunzionamenti sul codice esistente.
Tutto questo causa maggior lavoro di manutenzione (quando si analizza l'impatto di nuove modifiche) su codice che in realtà non è nemmeno utilizzato.
Di solito la soluzione è molto semplice: rimuovere del tutto queste classi, o iniziare ad usarle se per caso in qualche punto avrebbero dovuto essere usate ma ci si è dimenticati della loro esistenza e aggirato l'ostacolo con del codice collocato a macchia di leopardo in altre classi.


Astrazione non necessaria

Accade quando è introdotta una classe che non sarebbe stata necessaria, perché svolge un compito troppo semplice o quasi del tutto assente.
Può capitare se il codice deriva da traduzioni da altri linguaggi procedurali, e sono state introdotte classi che sostituiscono strutture dati, prive (o quasi) di metodi, oppure per sopperire a limitazioni del linguaggio, oppure come causa di over-engineering, una progettazione iperprotettiva.
In questi casi si può, a seconda, sopprimere la classe, spostarne i metodi in altre classi, o trovare i metodi per sopperire alle carenze di un linguaggio con altri costrutti del linguaggio ospitante.


Astrazione duplicata

Accade quando ci sono classi con lo stesso nome o con la stessa funzione. La duplicazione andrebbe evitata in ogni caso, perché crea problemi sia in fase di comprensione che di manutenzione e viola il principio DRY (Don't repeat yourself)
Possono essere generati da programmazione copia-incolla, da più rami di progetto sviluppati da team che non comunicavano tra loro, o necessità di derivare classi che però erano non derivabili.
La soluzione può essere rimuovere le copie e lasciare in vita solo una delle classi, ove possibile, oppure ove presenti piccole differenze, effettuare un subclassing da una classe base comune. In alternativa modificare i client per usare solo una delle classi esistenti e rimuovere le altre.
Se si trattava di non poter derivare una classe "chiusa" alle derivazioni, valutare se utilizzare una classe bridge




bibliografia
Refactoring for Software design smells,  Girish Suryanarayana, Ganesh Samarthyam, Tushar Sharma

Nessun commento:

Posta un commento

Refactoring: improving modularization

  Modularità In generale, per modularità si intende la misura in cui un sistema può essere decomposto e ricombinato. La modularizzazione è a...