Sei sulla pagina 1di 27

Capitolo 3: Refactoring

Che cos’è? A cosa serve? Quando e come va utilizzato?

Quando si parla di “refactoring” possono essere molteplici le domande che il


lettore si pone, soprattutto se non è particolarmente esperto in tale campo.
Come sostenuto anche dai principali autori in materia, come Martin Fowler e
Kent Beck, è possibile definire il “refactoring” come una pratica che preveda
la modifica di un sistema software con l’obiettivo di migliorare la sua struttura
interna, senza però alterarne le funzionalità. In altre parole, è un modo
disciplinato per “ripulire” il codice esistente. Dopo l’applicazione del
refactoring, infatti, il software risulterà più comprensibile, più facilmente
gestibile e meno incline alla possibilità che si verifichino nuovi bug.

Tale tecnica può risultare fondamentale quando il sistema software degenera


al punto da diventare difficilmente gestibile e, soprattutto, estendibile se non a
fronte di interventi costosi in termini di tempo e risorse. Il refactoring permette
di effettuare modifiche di tipo evolutivo, incrementale o migliorativo al codice,
a parità di funzionalità del prodotto. L’effetto cumulativo di piccole e semplici
modifiche di refactoring, unito all’esecuzione di alcune classi di test, possono
migliorare drasticamente il progetto software.

Come sostenuto anche da Martin Fowler, noto ingegnere britannico membro


dell’Agile Alliance e autore del libro “Refactoring: Improving the Design of
Existing Code”, ogni ciclo di refactoring deve durare poco in termini di tempo.
Così facendo, infatti, è meno probabile incorrere nei vari errori di
programmazione.

Un altro aspetto fondamentale quando si esegue il refactoring sono le classi


di test. Avere dei buoni test che permettano di avere confidenza e robustezza
nel proprio codice è una condizione fondamentale e imprescindibile per poter
pensare di effettuare refactoring su un qualsiasi sistema, semplice o
complicato che sia. Sono i test che danno la sicurezza al programmatore di
aver prodotto codice funzionante.

Il refactoring funziona? Come è possibile stabirlo?

Secondo Kent Beck, blablabla, il valore di un programma può essere stabilito


in base a:
 quello che può eseguire oggi;
 quello che potrà eseguire nel futuro prossimo.

Spesso molti programmatori si concentrano sull’attuale sviluppo del software,


senza, dunque, considerare quali migliorie egli potrà apportare in futuro al
programma. Così facendo, il programmatore sarà in grado di lavorare
correttamente nel presente, ma nel futuro avrà enormi difficoltà per sviluppare
correttamente il software. Occorre, dunque, cercare di sviluppare il
programma, ed eseguire le varie operazioni di refactoring, anche in base ai
futuri sviluppi che riteniamo il software necessiti.

Quando si parla di refactoring occorre anche distinguere tra refactoring


automatico e refactoring manuale.

Si tratta di refactoring automatico quando, ad esempio, si modifica il nome di


una variabile o di un pacchetto. La mancata automatizzazione di tale
passaggio comporta un notevole carico di lavoro extra per il programmatore
che è quindi costretto ad analizzare attentamente il codice e sostituire ogni
occorrenza di tale variabile o di tale pacchetto con il nuovo valore. Soprattutto
quando il codice è molto sviluppato, è facile commettere errori come, ad
esempio, la mancata sostituzione di un’occorrenza.

Più interessanti da analizzare sono i refactoring manuali. A differenza dei


refactoring automatici, dove le operazioni che dovevano essere effettuate nel
codice erano semplici e facilmente automatizzabili, i refactoring manuali sono
sviluppati a discrezione del programmatore. Essi costituiscono una parte
fondamentale nello sviluppo del software poiché permettono di diminuire
l’importanza del design iniziale. Tale tipo di refactoring, infatti, permette di
procedere parallelamente sia allo sviluppo del codice sia allo sviluppo del
relativo design. Se così non fosse, come sostiene anche Sandro Pedrazzini
nel libro “Elementi di progettazione agile: utilizzo pratico di un design pattern”,
otterremo una struttura interna degradata del nostro software poiché
avremmo effettuato molti tentativi per far evolvere il codice senza modificarne
l’architettura.

Vantaggi
Quali possono essere i principali benefici derivanti dall’utilizzo del
refactoring?
Come sostenuto anche dai principali esperti in materia come, ad esempio,
Sandro Pedrazzini e Martin Fowler, è possibile distinguere i seguenti
vantaggi:

o Miglioramento del design;

o Software più leggibile;

o Si scoprono errori in anticipo;

o Si programma più velocemente.

MIGLIORAMENTO DEL DESIGN

Un motivo per cui conviene utilizzare il refactoring risiede nel miglioramento


del design del software. Un regolare esercizio di refactoring, infatti, aiuta a
mantenere sia la struttura che la leggibilità del codice ai più alti livelli
qualitativi.

Un altro aspetto fondamentale risiede nell’eliminazione di eventuali duplicati.


Tali cancellazioni non provocheranno un immediato aumento della velocità
del software, ma produrranno effetti a medio-lungo termine. Eliminando i
duplicati, infatti, si garantisce che il codice non presenti ripetizioni inutili,
evitando di confondere il programmatore in eventuali modifiche future.

SOFTWARE PIÙ LEGGIBILE

La programmazione è sempre stata considerata come una sorta di


comunicazione tra il computer ed il programmatore. Come sostenuto anche
da Sandro Pedrazzini, tuttavia, non è nel nostro interesse scrivere codice
illeggibile. Sarà più semplice eseguire le modifiche desiderate o, ad esempio,
far analizzare il codice ad altri colleghi programmatori, se il codice è leggibile
e facilmente comprensibile.

La leggibilità, dunque, è un fattore di fondamentale importanza. Conviene far


eseguire, ad esempio, più cicli al nostro programma, ma possedere codice
leggibile piuttosto che avere un software che risponde più prontamente al
comando dell’utente, ma che ha meno leggibilità rispetto al precedente.
In alcuni casi, inoltre, il refactoring viene utilizzato anche per comprendere
meglio porzioni sconosciute di codice. È possibile, infatti, che alcuni membri
di un team di sviluppo non abbiano ben chiaro il funzionamento di una
determinata porzione di codice e decidano, ad esempio, di eseguirne il
refactoring per cercare di comprenderne meglio il funzionamento.

SI SCOPRONO ERRORI IN ANTICIPO

Un’ulteriore vantaggio del refactoring è il rilevamento di eventuali bug


presenti nel codice. L’utilizzo di tale processo, infatti, permette al
programmatore di comprendere meglio la struttura del software che sta
sviluppando e, soprattutto, gli permette di individuare eventuali imprecisioni o
errori che possono essere sorti all’interno del codice.

Alcune volte può non essere necessario eseguire il refactoring se lo scopo è


solamente quello di trovare i bug presenti all’interno del codice. Succede,
infatti, che tali errori o imprecisioni siano talmente evidenti e facili da risolvere
da catturare immediatamente l’attenzione del programmatore che provvede,
dunque, a modificare adeguatamente il codice.

In alcuni casi, tuttavia, il suddetto problema può essere talmente complicato


da impedire al programmatore di risolverlo adeguatamente. Se ciò avviene, è
dunque necessario e indispensabile eseguire il processo di refactoring al fine
di comprendere meglio la natura del suddetto guasto.

SI PROGRAMMA PIÙ VELOCEMENTE

Il refactoring, dunque, è uno strumento utilizzato per migliorare la qualità, la


progettazione e la leggibilità del software che stiamo sviluppando, oltre
naturalmente a ridurre o eliminare eventuali bug presenti nel codice. Tuttavia,
il principale vantaggio derivante dall’utilizzo del refactoring consiste
nell’aiutare il programmatore a sviluppare codice più velocemente. Come
sostenuto anche da Sandro Pedrazzini nel libro “Elementi di progettazione
agile: utilizzo pratico di design pattern, refactoring e test”, grazie alla
presenza di un buon design, è possibile sviluppare codice più rapidamente.
Senza la presenza di un buon design, infatti, è possibile progredire
velocemente per un po’, ma poi, inevitabilmente, lo sviluppo rallenta. In tal
caso, difatti, colui che sviluppa il software spende gran parte del tempo a sua
disposizione per eliminare i bug piuttosto che introdurre nuove funzionalità
all’interno dello stesso.

Code Smell
Il termine “code smell” è stato coniato da Kent Beck e Martin Fowler, autori
anche di “Refactoring: Improving the Design of Existing Code”, con l’obiettivo
di indicare una serie di caratteristiche che il codice sorgente può avere e che
sono riconosciute unanimemente come difetto di programmazione.

I “code smell”, tuttavia, non sono veri e propri errori di programmazione,


bensì debolezze di progettazione che riducono la qualità del software
prodotto. La loro identificazione è soggettiva, ossia a discrezione del
programmatore.

Alcuni esempi di code smell possono essere i seguenti:

 Duplicated code;

 Long method;

 Large class;

 Long parameter list;

 Switch;

 Lazy class.

DUPLICATED CODE
Un possibile esempio di code smell è il codice ridondante. Succede spesso
che all’interno di una classe siano presenti porzioni di codice identiche o
semplicemente ridondanti. Potrebbe accadere, infatti, che due metodi della
medesima classe contengano analoghe istruzioni. In tal caso, occorre
applicare il refactoring Extract Method, ossia occorre creare un nuovo metodo
contenente tali istruzioni ridondanti e, al contempo, modificare i metodi iniziali
poiché, al termine del refactoring, questi ultimi dovranno contenere solo una
chiamata al nuovo metodo appena creato.

Tale situazione potrebbe accadere anche all’interno di una o più sottoclassi.


In tal caso, occorre applicare sempre il refactoring Extract Method ma, a
differenza dell’esempio precedente, va applicato anche il Pull Up Field
refactoring. Il nuovo metodo, che conterrà le istruzioni ridondanti e sarà
richiamato dai metodi che prima contenevano tali istruzioni, sarà contenuto
nella superclasse e non nelle varie sottoclassi.

LONG METHOD
Nella moderna programmazione ad oggetti si tende, spesso, ad utilizzare
metodi abbastanza corti per facilitare la comprensione e l’utilizzo del software
che si sta progettando. Come sostiene anche Martin Fowler, infatti,
“ogniqualvolta si ha la necessità di commentare qualcosa, invece occorre
scrivere un metodo”. Tale metodo, che conterrà il codice che si voleva
commentare, è fondamentale poiché consente al programmatore di ridurre la
distanza semantica tra ciò che il metodo fa e come lo fa. Per fare ciò, nella
maggior parte dei casi, si applica il refactoring Extract Method.

La presenza di un metodo contenente molte linee di istruzioni, dunque, è un


segnale di code smell.

LARGE CLASS
Un altro esempio di code smell sono le large class, ossia quelle classi che
fanno troppo, anche quello che non dovrebbero fare. Una large class può,
dunque, essere vista come un insieme di troppe variabili di istanza che, di
conseguenza, possono produrre una notevole quantità di codice ridondante.
Analogamente ai metodi, se all’interno del codice sono presenti una o più
large class, allora occorre verificare se hanno alcuni elementi in comune e, in
caso affermativo, occorre estrarre tali linee di codice in modo da ridurre le
classi in questione.

LONG PARAMETER LIST


Un’altra cattiva abitudine dei programmatori riguarda il passaggio dei
parametri ai metodi. Spesso, infatti, succede che siano passati ad un
particolare metodo una lunga lista di parametri, molti dei quali non necessari
per il corretto funzionamento del suddetto metodo. Ciò succedeva soprattutto
nel recente passato poiché, come sostengono anche Martin Fowler e Kent
Beck, veniva insegnato ai nuovi programmatori di passare tutti i possibili
parametri ai metodi. Ciò era necessario perché i parametri erano definiti,
soprattutto, globalmente. Al giorno d’oggi, invece, si ritiene necessario ridurre
al minimo indispensabile le liste di parametri che vengono passate ai metodi.
Saranno, infatti, i metodi in questione a doversi ricavare alcune informazioni.
In alcuni casi, invece di passare la lista di parametri come argomento di un
metodo, viene passato direttamente l’oggetto.

SWITCH
Quando si sviluppa codice Object Oriented occorre cercare di evitare
l’inserimento, all’interno del codice, di istruzioni come, ad esempio, switch. Se
ciò non avviene, allora tali istruzioni potrebbero ridurre notevolmente la
qualità del software che stiamo producendo e si avrebbe un caso di “code
smell”.

L’introduzione dell’istruzione switch, infatti, può introdurre problemi di


duplicazione all’interno del codice. Lo stesso switch può comparire in diversi
punti del programma che stiamo sviluppando e, dunque, un cambiamento in
uno dei suddetti switch dovrebbe comportare anche un cambiamento negli
altri switch.

Una possibile soluzione è il polimorfismo, ovvero il meccanismo che permette


di riferirsi ad un oggetto attraverso qualsiasi variabile che abbia la stessa
interfaccia. Nel caso di codice Object Oriented, se una funzione definisce di
accettare come parametro un oggetto di una certa classe, allora significa che
a quella funzione si potranno passare anche istanze di sue sottoclassi.

LAZY CLASS
La creazione di una nuova classe, all’interno di un progetto software,
comporta naturalmente ulteriori costi al programmatore. È necessario, infatti,
mantenerla e carpirne il comportamento in ogni momento dello sviluppo. Se
una classe non è indispensabile per la creazione di un software, è possibile,
dunque, eliminarla. Questo potrebbe succedere, ad esempio, quando una
classe aggiunta inizialmente poi non è stata realizzata concretamente, ma
solo pianificata.

Quando eseguire il refactoring?


Un ulteriore aspetto da analizzare quando si sceglie di eseguire il refactoring
è il tempo che va dedicato all’esecuzione di tale processo. Quanto tempo,
infatti, occorre dedicare al refactoring?

Secondo Martin Fowler, il refactoring non è un’attività la cui durata può


essere già stabilita nella fase iniziale, ossia in un momento antecedente
l’inizio dello stesso. Il refactoring, infatti, non può essere preveduto poiché si
svolge a piccoli passi (“little by little”). Nonostante questo è possibile
individuare alcune “situazioni” in cui è possibile determinare quando va
eseguito il refactoring. Di seguito ne enunceremo alcune di esse.

“LA REGOLA DEL TRE”

Un possibile metodo per stabilire quando eseguire l’attività di refactoring può


essere applicare la “regola del tre”, come sostiene anche Martin Fowler nel
suo libro. Egli, infatti, si rifà ad una dichiarazione di un suo collaboratore, Don
Roberts, professore all’università dell’Illinois, che sostiene:

“La prima volta che si sviluppa qualcosa, si fa e basta. La seconda volta che
si sviluppa qualcosa di simile al codice già creato precedentemente, ci
potrebbe essere il rischio di incorrere in eventuali duplicazioni, ma si lascia
correre e si va avanti comunque. La terza volta che si crea qualcosa di simile,
si esegue il refactoring”.

QUANDO SI AGGIUNGONO NUOVE FUNZIONALITÀ AL


SOFTWARE
Il momento più comune in cui eseguire il refactoring è quando si aggiungono
nuove funzionalità al software che stiamo sviluppando. Il principale motivo di
tale scelta, infatti, risiede nella necessità di dover capire esattamente quale
parte di codice occorre “preservare” ed, invece, quale parte di codice occorre
modificare.

QUANDO C’È LA NECESSITÀ DI CORREGGERE BUG

Come già detto precedentemente, il refactoring permette al programmatore di


ridurre o eliminare gli eventuali bug o imprecisioni che sono presenti nel
software che si sta sviluppando. È possibile, dunque, sostenere che tale
processo di refactoring può e deve essere eseguito anche quando, ad
esempio, il sistema rileva la presenza di un bug all’interno del codice ed
occorre, dunque, correggerlo adeguatamente.

QUANDO C’È LA NECESSITÀ DI REVISIONARE IL CODICE

È, infine, possibile eseguire il refactoring quando c’è la necessità di


revisionare il codice. Tali revisioni sono, infatti, fondamentali quando si
sviluppa software all’interno di un team; essi, infatti, aiutano i membri del
gruppo con minor esperienza o con maggiori difficoltà a comprendere meglio
il progetto software che si sta realizzando. È possibile, infatti, che vengano
coinvolte attivamente più persone all’interno di uno sviluppo software ed, in
questo caso, può essere necessario che vengono effettuate delle regolari
revisioni del codice per assicurare una corretta stesura dello stesso.

Quando non eseguire il refactoring?


Non sempre è possibile attuare il refactoring. Come già sostenuto
precedentemente, infatti, ci sono casi in cui è necessario effettuarlo poiché
consente di rendere più efficiente e chiaro il software che stiamo sviluppando.

In alcuni casi, tuttavia, è di difficile attuazione. Ci sono momenti, infatti, in cui


il codice è talmente confuso che sarebbe più facile riscrivere il programma
ripartendo da zero. Tali decisioni, naturalmente, sono difficili da prendere
perché non esistono alcune linee guida o regole su cui è possibile basarsi.
Possiamo, tuttavia, sostenere che è necessario riscrivere completamente il
codice quando, ad esempio, quest’ultimo è non funzionante e sono presenti
così tanti bug che non è possibile stabilizzare il software. L’attività di
refactoring, infatti, può essere effettuata solo se il codice lavora, in gran parte,
correttamente.

Un’ulteriore situazione in cui sarebbe da evitare il refactoring è quando il


programmatore si avvicina alla scadenza entro la quale bisogna consegnare
il programma al cliente. In questo caso, infatti, il tempo non sarebbe
sufficiente per poter eseguire l’attività di refactoring.

Tipi di refactoring
Il refactoring, dunque, è un metodo che viene utilizzato per fattorizzare il
codice, riorganizzarlo e revisionarlo al fine di soddisfare al meglio le esigenze
del cliente. Naturalmente, esistono molti tipi di refactoring, ma,
generalizzando, possiamo distinguere le seguenti 3 macro-categorie:

o Refactoring “Extract”;

o Refactoring “Push/Pull”;

o Refactoring complessi.

REFACTORING “EXTRACT”

Nella seguente categoria di refactoring risiedono tutti i metodi di revisione del


codice che si rivelano estremamente utili quando si sviluppa software. I
refactoring Extract sono tutti automatizzabili ed hanno in comune l’obiettivo di
creare qualcosa di nuovo partendo da una struttura preesistente. Di seguito
ne elenchiamo alcuni di essi.

EXTRACT SUBCLASS

Il refactoring di tipo Extract Subclass prevede la possibilità di estrarre una


sottoclasse da una classe in cui sono presenti elementi utilizzabili in alcune
istanze specifiche.
Ecco un possibile esempio:

EXTRACT CLASS

Data una classe esistente, il refactoring Extract Class provvede a creare una
nuova classe aggregata alla classe esistente. Ecco un possibile esempio:

EXTRACT HIERARCHY

Il refactoring Extract Hierarchy crea, da una classe di partenza, una gerarchia


di classi dove ogni sottoclasse rappresenta un caso speciale.

Ecco un possibile esempio:


EXTRACT INTERFACE

Questo tipo di refactoring, invece, prevede la possibilità di estrarre


un’interfaccia da una classe esistente; può essere particolarmente utile
quando si vuole separare l’interfaccia dall’implementazione.

Ecco un esempio:

EXTRACT METHOD

Il refactoring Extract Method prevede, invece, la possibilità di estrarre una


sequenza di istruzioni e formando, dunque, un metodo contenente la
sequenza. Di seguito proponiamo una piccola e semplice porzione di codice
in cui eseguiamo questo tipo di refactoring.
void stampa(String nome, double importo){

stampa_pubblicita();

System.out.println(“Nome : ”+nome);

System.out.println(“Importo : ”+importo);

che diventa:
void stampa(String nome, double importo){

stampa_pubblicita();

stampa_dettagli(nome, importo);

void stampa_dettagli(String nome, double importo){

System.out.println(“Nome : ”+nome);

System.out.println(“Importo : ”+importo);

EXTRACT SUPERCLASS

Supponiamo di avere due classi indipendenti che hanno alcuni elementi in


comune; il refactoring Extract Superclass prevede la possibilità di creare una
superclasse spostando, dunque, gli elementi in comune nella nuova classe.

Ecco un piccolo esempio:


REFACTORING “PUSH/PULL”

Dopo aver analizzato i refactoring di tipo Extract, vediamo ora alcuni tipi di
refactoring che utilizzano le operazioni di Push e Pull.

PULL UP CONSTRUCTOR BODY

Un possibile refactoring di tipo “Push/Pull” è il Pull Up Constructor Body. Tale


refactoring prevede, se si hanno più sottoclassi con costruttori simili, la
possibilità di riunire tutto nel costruttore della superclassi, naturalmente
richiamandolo opportunamente nelle sottoclassi. Come conseguenza,
dunque, si ha lo spostamento del contenuto dei costruttori delle sottoclassi
nella superclasse.

Ecco un piccolo esempio di codice in cui può essere applicato tale


refactoring:

public class Manager extends Employee{

...

public Manager (String name, String ID, String grado){


_name = name;
_ID = ID;
_grado = grado;
}

che diventa:
public class Manager extends Employee{

...

public Manager (String name, String ID, String grado){


super(name, ID);
_grado = grado;
}
}

PULL UP FIELD
Pull Up Field, invece, si occupa di spostare nella superclasse un campo in
comune tra tutte le sottoclassi. Un possibile esempio può essere il seguente:

PULL UP METHOD

Il refactoring Pull Up Method provvede a spostare nella superclasse un


metodo in comune tra tutte le sottoclassi. Ecco un possibile esempio:

PUSH DOWN FIELD

Un esempio di refactoring che utilizza l’operazione Push può essere il Push


Down Field, che si occupa di spostare dalla superclasse in una sottoclasse
un campo utilizzato solo nella sottoclasse coinvolta. Ecco un esempio:
PUSH DOWN METHOD

Il Push Down Method, invece, si occupa di spostare un metodo dalla


superclasse in una sottoclasse. Di seguito proponiamo un semplice esempio:

REFACTORING COMPLESSI

Dopo aver analizzato i refactoring di tipo “Extract” e di tipo “Push/Pull”,


descriviamo ora una categoria di refactoring più complessi da utilizzare.
Questi ultimi, tuttavia, non consentono, a differenza dei refactoring
precedenti, un’immediata presa visione delle modifiche da parte del
programmatore e, di conseguenza, non ne producono un’immediata
soddisfazione. Essi sono fondamentali quando si desidera che il refactoring
sia efficace e, dunque, produca risultati, anche su software che, ad esempio,
hanno una maggiore complessità progettuale. Nei paragafi successivi sono
descritti alcuni esempi di refactoring complessi.

REPLACE CONDITIONAL WITH POLYMORPHISM


Supponiamo di avere un’espressione condizionale all’interno di un metodo in
una determinata classe e supponiamo, inoltre, che tale classe abbia una
propria sottoclasse. Tramite il Replace Conditional with Polymorphism è
possibile spostare ogni ramo di tale espressione condizionale all’interno di un
metodo della sottoclasse in questione. In tal caso, dunque, il metodo della
superclasse diverrà astratto, mentre il metodo della sottoclasse dovrà
sovrascriverlo.

Supponiamo di avere le seguenti classi:

con il metodo getStipendio della classe Persona così definito:


int getStipendio(){
switch(qualifica){
case "operaio": return 1000;

case "impiegato": return 2000;

case "manager": return 5000;


}throw new RuntimeException("Errore");
}

Tramite il Replace Conditional with Polymorphism, dunque, il metodo


getStipendio della classe Persona diventa astratto; al contempo, nella
sottoclasse Lavoratore, verrà creato il metodo getStipendio, definito come
sopra, che sovrascriverà il metodo getStipendio della superclasse.
REPLACE INHERITANCE WITH DELEGATION

Un altro esempio di refactoring complesso è il Replace Inheritance with


Delegation, che si occupa di sostituire la relazione di ereditarietà con la
relazione di composizione. Questo tipo di refactoring si utilizza quando una
sottoclasse riusa solo una minima parte della superclasse.

Ecco un possibile esempio:

REPLACE DELEGATION WITH INHERITANCE

Il Replace Delegation with Inheritance, invece, permette di effettuare


l’operazione inversa rispetto al precedente tipo di refactoring. È possibile,
infatti, sostituire la relazione di composizione con la relazione di ereditarietà.
Un possibile esempio può essere il seguente:
SEPARATE DOMAIN FROM PRESENTATION

A differenza dei refactoring precedenti, Separate Domain from Presentation


ha come obiettivo quello di separare gli elementi di una interfaccia grafica
dalla logica del programma. Naturalmente, la complessità di tale refactoring
dipende dalla complessità del programma da modificare.

Esempio di refactoring
Per approfondire ulteriormente il refactoring Separate Domain from
Presentation andiamo ad analizzare un esempio di codice software in cui, da
una struttura unica di partenza, occorre derivare una struttura modulare,
ossai una struttura in cui il modello e la presentazione siano separati.
Per fare ciò occorre applicare il modello MVC ed il pattern Observer.

MODELLO MVC (Model – View – Controller)


Il modello MVC (Model – View - Controller) è un pattern architetturale molto
diffuso nello sviluppo di sistemi software poiché è in grado di separare,
all’interno di un programma, la logica
dell’applicazione dall’interfaccia utente. Come
sostenuto anche da Sandro Pedrazzini in
“Elementi di progettazione agile: utilizzo pratico
di design pattern, refactoring e test”, tale
modello fu impiegato, inizialmente,
esclusivamente dal linguaggio Smalltalk, ossia
un linguaggio di programmazione orientato agli
oggetti con gestione dinamica dei tipi. Solo
successivamente, infatti, il pattern MVC fu
applicato anche ad altri linguaggi di
programmazione come Java, C o Python.
Il pattern Model View Controller si occupa di separare le varie componenti
che compongono un’applicazione: il modello, la visualizzazione del modello
(view) ed il controller.
Il modello si occupa di rappresentare ed approssimare gli oggetti del mondo
reale all’interno del programma, mentre la view è un modo per rappresentare
graficamente il modello scelto. Naturalmente, lo stesso modello può essere
rappresentato contemporaneamente da più grafiche, ma ogni volta che
avviene un cambiamento interno, occorre che queste ultime siano
correttamente informate. Infine, il controller, che si occupa di interpretare i
segnali in arrivo dall’utente tramite, ad esempio, mouse o tastiera, e li
trasforma in comandi inviati al modello.
Tale relazione tra view e model, ossia tra il modello e la sua visualizzazione
grafica, può essere vista anche come una istanza del pattern Observer.

PATTERN OBSERVER
Il pattern Observer è un design pattern comportamentale utilizzato per tenere
sotto controllo lo stato di diversi oggetti. Tale pattern si basa su uno o più
oggetti osservatori (o Observer) ed un oggetto Subject. Esso, dunque,
definisce una dipendenza uno-molti tra oggetti tali che, quando un oggetto
cambia il suo stato (Subject), tutti gli altri devono esserne informati e
aggiornarsi automaticamente. Da notare è che il Subject non conosce con
quale tipo di Observer sta comunicando, sa solo che è un Observer.
Il pattern Observer ha senso quando la dipendenza non è nota, non è fissata
a priori, ossia quando le dipendenze si aggiornano dinamicamente. Ad
esempio, quando viene modificata la lista degli Observer, quando si creano
nuovi Subject, …
ESEMPIO
Supponiamo che inizialmente il programma abbia un’interfaccia grafica
contenente tre campi di testo e che i contenuti dei campi di testo sia in
relazione tra di loro. Il valore del terzo campo, infatti, corrisponde alla somma
del primo con il secondo.
Il nostro obiettivo è separare la logica del programma, ossia la relazione che
intercorre tra i vari campi di testo, e la presentazione, ossia i campi di testo.

Ecco i passaggi che occorre effettuare per realizzare il refactoring:

o Si modifica la vecchia interfaccia grafica in modo da separare gli


osservatori dall’oggetto osservato;

o Si compila e si esegue il test;

o Si isola l’accesso ai dati del modello tramite i metodi get e set;

o Si compila e si esegue il test;

o Si creano i nuovi campi nel modello tramite i metodi get e set e si


sposta nel modello le funzioni dell’applicazione;

o Si compila e si esegue il test.

Ecco il codice iniziale:


import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JFrame;
import javax.swing.JTextField;

public class Esempio extends JFrame {

private JTextField addendo1;


private JTextField addendo2;
private JTextField somma;

Esempio(){
...
}

/**
* metodo utilizzato quando si modifica il contenuto del primo campo di testo
* e, di conseguenza, si vuole aggiornare il valore della somma
*/
void start_check(){
calcola_somma();
}

/**
* metodo utilizzato quando si modifica il contenuto del secondo campo di testo
* e, di conseguenza, si vuole aggiornare il valore della somma
*/
void end_check(){
calcola_somma();
}

/**
* metodo utilizzato quando si modifica il contenuto del terzo campo di testo
* e, di conseguenza, si vuole calcolare il valore del secondo addendo
*/
void check_somma(){
calcola_addendo2();
}

/**
* Dati i due addendi, questo metodo che calcola effettivamente la somma
*/
void calcola_somma(){
int add1 = Integer.parseInt(addendo1.getText());
int add2 = Integer.parseInt(addendo2.getText());
somma.setText(String.valueOf(add1+add2));
}

/**
* Dato il primo addendo e il valore della somma, il metodo calcola_addendo2()
* provvede a calcolare il valore del secondo addendo
*/
void calcola_addendo2(){
int add1 = Integer.parseInt(addendo1.getText());
int som = Integer.parseInt(somma.getText());
addendo2.setText(String.valueOf(som-add1));
}

/**
* Tramite questa classe l'utente può modificare il valore nel campo
* di testo del secondo addendo e poi premere il tasto enter della tastiera
*
*/
class EndAction implements ActionListener{
public void actionPerformed(ActionEvent event){
end_check();
}
}

/**
* Tramite questa classe l'utente può modificare il valore nel campo
* di testo del primo addendo e poi premere il tasto enter della tastiera
*
*/
class StartAction implements ActionListener{
public void actionPerformed(ActionEvent event){
start_check();
}
}
/**
* Tramite questa classe l'utente può modificare il valore nel campo
* di testo della somma e poi premere il tasto enter della tastiera
*
*/
class SumAction implements ActionListener{
public void actionPerformed(ActionEvent event){
check_somma();
}
}

Dopo aver definito opportunamente il codice iniziale, adesso è possibile


iniziare il refactoring Separate Domain from Presentation.
La prima fase di tale processo prevede la modifica della vecchia interfaccia
grafica (GUI) per fare in modo che gli oggetti observable (osservati) e gli
oggetti observer (osservatori) vengano separati gli uni dagli altri. Occorre,
dunque, creare le classi Modello e View servendosi opportunamente dei
pattern Observer e Model View Controller.
Dopo aver completato la prima fase del refactoring, occorre, naturalmente,
compilare ed eseguire le classi di test al fine di verificare la correttezza e la
funzionalità del software.
In caso affermativo, è possibile procedere con il meccanismo di refactoring
isolando l’accesso ai dati del modello tramite i metodi get and set. In caso
negativo, invece, occorre analizzare adeguatamente il codice e trovare
l’errore che causa il failure del test.
Dopo aver introdotto i metodi get e set che permettono di isolare l’accesso ai
dati del modelo e, dopo aver verificato nuovamente che le classi di test
continuino ancora a funzionare correttamente, è possibile procedere con il
terzo passaggio della fase di refactoring. Quest’ultimo consiste nel creare i
nuovi campi nel modello con i metodi di accesso get and set e, dunque,
spostare nel modello le funzioni dell’applicazione. In questo modo, quindi,
creiamo il modello vero e proprio; la classe modello, infatti, precedentemente
era vuota se escludiamo la lista che ci permetteva di gestire gli oggetti
Observer. In quest’ultima fase, dunque, procediamo allo “spostamento della
logica”, ovvero allo spostamento dei metodi calcola_somma() e
calcola_addendo2() nella classe “modello”. Naturalmente, lo spostamento di
tali metodi dalla classe view alla classe modello provoca anche la necessità
di modificare opportunamente la classe view, oltre alla suddetta classe
modello. Occorre, infatti, collegare le chiamate get and set della view con
quelle del modello.
Il codice risultante sarà il seguente:

import java.awt.*;
import java.util.*;

public class Modello {

private ArrayList Observers = new ArrayList();


private String addendo1, addendo2, somma;

Modello(){
addendo1="0";
addendo2="0";
somma="0";
}

/**
* ritorna il valore del primo addendo
*/
String getaddendo1(){
return addendo1;
}

/**
* setta il valore del primo addendo
*/
void setaddendo1(String arg){
addendo1 = arg;
notifyObservers();
}

/**
* ritorna il valore del secondo addendo
*/
String getaddendo2(){
return addendo2;
}

/**
* setta il valore del secondo addendo
*/
void setaddendo2(String arg){
addendo2 = arg;
notifyObservers();
}

/**
* ritorna il valore della somma
*/
String getsomma(){
return somma;
}

/**
* setta il valore della somma
*/
void setsomma(String arg){
somma = arg;
notifyObservers();
}

/**
* si aggiunge un elemento alla lista degli osservatori
*/
public void addObserver(Observer o){
Observers.add(o);
}

/**
* metodo che provvede a notificare a tutti gli osservatori un aggiornamento
* dello stato dell'oggetto osservato
*/
protected void notifyObservers(){
Iterator i = Observers.iterator();
while(i.hasNext()){
Observer o = (Observer)i.next();
o.update(this);
}
}

/**
* Dato il primo addendo e il valore della somma, il metodo calcola_addendo2()
* provvede a calcolare il valore del secondo addendo
*/
void calcola_addendo2(){
int add1 = Integer.parseInt(getaddendo1());
int som = Integer.parseInt(getsomma());
setaddendo2(String.valueOf(som-add1));
}

/**
* Dati i due addendi, questo metodo che calcola effettivamente la somma
*/
void calcola_somma(){
int add1 = Integer.parseInt(getaddendo1());
int add2 = Integer.parseInt(getaddendo2());
setsomma(String.valueOf(add1+add2));
}

import javax.swing.JFrame;
import javax.swing.JTextField;

public class View extends JFrame implements Observer{

private JTextField addendo1;


private JTextField addendo2;
private JTextField somma;
private Modello model;

View(Modello m){
model = m;
model.addObserver(this);
update(model);
}

/**
* restituisce il valore del secondo addendo
*/
String getaddendo2(){
return model.getaddendo2();
}

/**
* metodo che provvede a settare il valore del secondo addendo
*/
void setaddendo2(String arg){
model.setaddendo2(arg);
}

/**
* restituisce il valore del primo addendo
*/
String getaddendo1(){
return model.getaddendo1();
}

/**
* metodo che provvede a settare il valore del primo addendo
*/
void setaddendo1(String arg){
model.setaddendo1(arg);
}

/**
* restituisce il valore della somma
*/
String getsomma(){
return model.getsomma();
}

/**
* metodo che provvede a settare il valore della somma
*/
void setsomma(String arg){
model.setsomma(arg);
}

/**
* metodo utilizzato quando si modifica il contenuto del primo campo di testo
* e, di conseguenza, si vuole aggiornare il valore della somma
*/
void start_check(){
setaddendo1(addendo1.getText());
calcola_somma();
}

/**
* metodo utilizzato quando si modifica il contenuto del secondo campo di testo
* e, di conseguenza, si vuole aggiornare il valore della somma
*/
void end_check(){
setaddendo2(addendo2.getText());
calcola_somma();
}

/**
* metodo utilizzato quando si modifica il contenuto del terzo campo di testo
* e, di conseguenza, si vuole calcolare il valore del secondo addendo
*/
void check_somma(){
setsomma(somma.getText());
calcola_addendo2();
}

/**
* Dato il primo addendo e il valore della somma, il metodo calcola_addendo2()
* provvede a calcolare il valore del secondo addendo
*/
void calcola_addendo2(){
model.calcola_addendo2();
}

/**
* Dati i due addendi, questo metodo che calcola effettivamente la somma
*/
void calcola_somma(){
model.calcola_somma();
}

public void update(Modello m){


addendo1.setText(getaddendo1());
addendo2.setText(getaddendo2());
somma.setText(getsomma());
}

Potrebbero piacerti anche