- Identificazione delle classi e oggetti: Questa è
la fase di analisi in cui vengono effettuati degli incontri con il
cliente per stabilire ed identificare in modo non ancora definitivo le
classi che saranno coinvolte nel progetto. - Definizione della semantica delle classi: In
questo stadio del processo si prosegue assegnando ad ogni classe una
semantica ben precisa. Al termine di questa fase dovrebbe essere ben
chiara la struttura delle classi e delle interfacce eventualmente da
implementare. - Relazioni tra le classi: Dopo aver definito le
classi del progetto è molto importante identificare ed indicare tutte
le tipologie di relazione che intercorrono tra di esse. - Implementazione del Codice: Lo stadio della
scrittura del codice è, ovviamente, di enorme importanza. Talvolta,
durante la stesura del codice possono essere evidenziate delle
incongruenze di analisi o disegno che vanno notificate e riviste per
poi procedere di nuovo alla codifica.
Calendario
Avviso
Chi c'é online?
Visitatore: 1
Categorie
Link
programmatori poco esperti in OOP è quello di utilizzare una sorta di
approccio "misto", ovvero di programmare ad oggetti ricorrendo talvolta
alle vecchie abitudini proprie della programmazione procedurale.
Soprattutto i linguaggi come il C++, per compatibilità con il C,
consentono di utilizzare questa modalità ibrida che rappresenta,
sicuramente, una delle cose da evitare. Questa riflessione serve, tra
l'altro, a mettere in luce l'importanza che riveste la qualità del
codice quando si lavora in ambiente Object Oriented.
Due dei principali fattori da cui dipende una buona qualità del codice sono i seguenti:
- Accoppiamento (Coupling)
- Coesione (Cohesion)
L'Accoppiamento fa riferimento ai legami esistenti
tra unità (classi) separate di un programma. In generale, diremo che se
due classi dipendono strettamente l'una dall'altra (ovvero hanno molti
dettagli che sono legati vicendevolmente) allora esse sono strettamente
accoppiate (si parla anche di strong coupling).
Riflettendo un attimo su quanto detto nei paragrafi precedenti,
quando si è parlato di incapsulamento, manutenzione e riutilizzo del
codice, si può facilmente arguire che per una buona qualità del codice
l'obiettivo sarà, dunque, quello di puntare ad un basso accoppiamento
(weak coupling o loose coupling), consentendo in tal modo una migliore
manutenibilità del software.
Infatti, un basso accoppiamento consente
sicuramente di avere una buona comprensione del codice associato ad una
classe senza doversi preoccupare di andare a reperire i dati delle
altre classi coinvolte. Inoltre, utilizzando un basso accoppiamento,
eventuali modifiche apportate ad una classe avranno poche o nessuna
ripercussione sulle altre classi con cui è instaurata una relazione di
dipendenza.
Per fare un esempio di basso accoppiamento nel mondo reale, si può
pensare ad una Radio connessa con degli Altoparlanti attraverso l'uso
di un cavo. Sostituendo o modificando il cavo, le due entità (Radio e
altoparlanti) non subiranno alcuna modifica sostanziale alle loro
strutture. Viceversa, un forte accoppiamento può essere rappresentato
da due travi di acciaio saldate tra di loro. Infatti, per poter muovere
una trave, anche l'altra subirà inevitabilmente degli spostamenti.
La Coesione, invece, rappresenta una informazione
sulla quantità e sulla eterogeneità dei task di cui una singola unità
(una classe o un metodo appartenente ad una classe) è responsabile. In
altre parole, attraverso la coesione si è in grado di stabilire quali e
quanti siano i compiti per i quali una classe (o un metodo) è stata
disegnata. In generale, si può affermare che più una classe ha una
responsabilità ristretta ad un solo compito più il valore della
coesione è elevato; in tal caso si parlerà di alta coesione (strong
cohesion).
Come si evince dalle definizioni appena fornite, a differenza
dell'accoppiamento, il concetto di coesione può essere applicato sia
alle classi che ai metodi.
È proprio l'alta coesione l'obiettivo da
prefiggersi quando si vuole scrivere del codice di buona qualità.
Infatti, il raggiungimento di un'alta coesione ha svariati vantaggi. In
particolare: semplifica la comprensione relativamente ai compiti propri
di una classe o di un metodo, facilita l'utilizzo di nomi appropriati e
favorisce il riutilizzo delle classi e dei metodi.
Le incertezze circa l'utilità della ereditarietà multipla nei programmi Object Oriented hanno portato alla definizione di una strada alternativa ma sicuramente più efficiente. Infatti, con l'avvento di Java è stato introdotto il concetto di interfaccia.
In generale, un interfaccia rappresenta una sorta di "promessa" che una classe si impegna a mantenere. La promessa è quella di implementare determinati metodi di cui viene resa nota soltanto la definizione (un po' come si è già visto per le classi ed i metodi astratti). Ciò che è importante non è tanto come verranno implementati tali metodi all'interno della classe ma, piuttosto, che la denominazione ed i parametri richiesti siano assolutamente rispettati.
Si supponga, ad esempio, di voler creare un'interfaccia denominata StackInterface che debba essere utilizzata da ogni classe che desideri definire la classica struttura dati a pila (LIFO). Una siffatta interfaccia sarà definita nel seguente modo (sintassi Java):
public interface StackInterface
{
public void push(int i);
public int pop();
}
Come si vede, sono stati definiti due metodi (push e pop), corrispondenti rispettivamente alle due operazioni di inserimento ed eliminazione di elementi dalla pila ma non ne viene in alcun modo fornita l'implementazione.
Supponiamo, ora, di voler definire una classe Stack che contenga il codice necessario per la corretta gestione di una pila. Tale classe sarà definita nel seguente modo:
public class Stack implements StackInterface
{
public void push(int i)
{
... ....
}
public int pop()
{
... ....
}
}
Come si potrà osservare, all'interno della classe Stack sono implementati i metodi che sono stati definiti all'interno dell'interfaccia StackInterface. Poiché la classe Stack promette di fare uso dell'interfaccia StackInterface (lo si può notare dalla istruzione "implements StackInterface"), qualora il programmatore omettesse di definire e implementare uno o entrambi questi metodi, si otterrebbe un errore di compilazione.
Sebbene le interfacce non vengano istanziate, come avviene per le classi, esse conservano determinate caratteristiche che sono simili a quelle viste nelle classi ordinarie. Ad esempio, una volta definita un'interfaccia, è possibile dichiarare un oggetto come se fosse del tipo dichiarato dall'interfaccia stessa utilizzando la medesima notazione utilizzata per la dichiarazione di variabili.
Inoltre, allo stesso modo delle classi, è possibile utilizzare l'ereditarietà anche per le interfacce, ovvero definire una interfaccia che estenda le caratteristiche di un'altra, aggiungendo altri metodi all'interfaccia padre.
Infine, una classe può implementare più di una interfaccia. Ovvero, è possibile obbligare una classe ad implementare tutti i metodi definiti nelle interfacce con le quali essa è legata. Questa ultima caratteristica fornisce, indiscutibilmente, la massima flessibilità nella definizione del comportamento che si desidera attribuire ad una classe.
Il singleton rappresenta un tipo particolare di classe che garantisce che soltanto un'unica istanza della classe stessa possa essere creata all'interno di un programma.
Per ottenere un siffatto comportamento è necessario avvalersi dello specificatore di accesso «private» anche per il costruttore della classe (cosa che generalmente non viene mai praticata in una classe "standard") ed utilizzare un metodo statico che consenta di accedere all'unica istanza della classe.
Vediamo un esempio in Java:
public class Singleton
{
private static Singleton istanza;
private Singleton()
{
}
public static Singleton getInstance()
{
if (istanza == null)
{
istanza = new Singleton();
}
return istanza;
}
public void helloWorld()
{
System.out.println("Hello World");
}
}
public class usaSingleton
{
public static void main(String args[])
{
Singleton.getInstance().helloWorld();
}
}
Come si può notare dal codice, il costruttore della classe Singleton è stato definito con access specifier private e, in tal modo, l'unico punto di accesso alla classe per il mondo esterno viene fornito attraverso il metodo statico getInstance() che si occupa di restituire (creandola prima se non è mai stata creata) l'unica istanza della classe.
Il lettore critico potrebbe domandarsi, giustamente, quando possa rivelarsi utile avvalersi dei singleton. In generale, la scelta del singleton viene effettuata in tutti quei casi in cui è necessario che venga utilizzata una sola istanza di una classe.
Ciò consente di:
- Avere un accesso controllato all'unica istanza della classe
- Avere uno spazio di nomi ridotto
- Evitare la dichiarazione di variabili globali
- Assicurarsi di avere un basso numero di oggetti utilizzati in condivisione grazie al fatto che viene impedita la creazione di nuove istanze ogni volta che si voglia utilizzare la stessa classe.
Nella programmazione ad oggetti, riveste una grande importanza la definizione dei livelli di visibilità che vengono assegnati ai metodi e alle proprietà di una classe. Infatti, è proprio attraverso la corretta definizione della visibilità applicata ad ogni singolo oggetto che si realizzano gli importanti e vantaggiosi risultati elencati nei paragrafi precedenti, in cui si sono illustrati i capisaldi di OOP.
Ad esempio, è facile comprendere che se un oggetto mettesse a completa disposizione del mondo esterno tutti i suoi metodi e tutte le sue proprietà si perderebbe nel nulla il concetto di incapsulamento. È un po' come se, nel mondo reale, un masterizzatore venisse venduto dando all'utente il completo accesso ai suoi componenti elettronici!
Per assegnare un determinato livello di visibilità ad una proprietà o ad un metodo è necessario utilizzare quelli che in OOP vengono definiti "access specifiers" (specificatori di accesso o anche descrittori di visibilità).
Esistono, in generale, quattro descrittori di visibilità. Alcuni linguaggi, tuttavia, ne utilizzano tre, non considerando il package:
- public
- protected
- private
- package
In generale, poi, possiamo considerare utile valutare i livelli di visibilità di metodi e proprietà nei seguenti casi:
- Classe
- SottoClasse
- Mondo Esterno
- Package
Il concetto di Package potrebbe risultare nuovo a chi non ha mai utilizzato un linguaggio di programmazione ad Oggetti. Esso rappresenta un insieme di classi e interfacce che operano nello stesso contesto e che sono raggruppate per consentire un utilizzo più organizzato ed efficiente delle stesse.
In altre parole, un package può essere tranquillamente visto come una libreria di classi che possono essere utilizzate ogniqualvolta se ne presenti la necessità. È, altresì, possibile annidare dei package all'interno di altri package in modo da ottimizzare la suddivisione delle classi (e interfacce).
Il linguaggio Java ha introdotto il concetto di package ed alcuni classici esempi sono i seguenti: java.lang; system.out; javax.swing. Altri linguaggi più recenti, come il C# o i l VB.Net utilizzano la denominazione namespace per definire un concetto del tutto analogo.
Mettendo, ora, in una tabella i descrittori di visibilità da una parte e i contesti in cui essi operano dall'altra, è possibile definire con precisione tutte le casistiche di visibilità su una classe:
|
Vediamo come interpretare la precedente tabella. Nella colonna Classe,ad esempio, è indicata la visibilità che una classe ha dei suoi metodi e delle sue proprietà definiti utilizzando i descrittori presenti nella colonna iniziale.
Come si vede, in tale circostanza, una classe ha sempre accesso e visibilità a tutti i suoi metodi e proprietà a prescindere da quali siano gli access specifiers utilizzati.
Nel caso del Package la situazione è simile: ovvero, tutte le altre classi appartenenti allo stesso Package di una particolare classe hanno sempre accesso ai metodi e proprietà di quest'ultima tranne nel caso in cui siano dichiarati private.
Nella colonna SottoClasse, invece, viene evidenziato come una classe B figlia di una classe A abbia accesso soltanto ai metodi e proprietà di A che sono definiti public o protected.
Infine, l'ultima colonna (Mondo Esterno) evidenzia come soltanto gli attributi e le proprietà di una classe che siano definite public possano essere visibili dall'esterno. In particolare, con la nomenclatura Mondo Esterno si suole identificare ogni classe che non rientri nelle precedenti casistiche esaminate (non sia una sottoclasse della classe in questione e non appartenga allo stesso package).
La scelta degli access specifiers, quando si definisce una classe è tutt'altro che trascurabile. Da essa dipende fortemente la struttura ad oggetti del progetto che si vuole creare e, conseguentemente, l'efficacia dell'implementazione del codice.
Se, ad esempio, si definissero tutti i metodi e le proprietà di un oggetto come public, si perderebbe immediatamente il concetto di incapsulamento in quanto ogni elemento (proprietà o metodo) sarebbe sempre visibile dal mondo esterno. È un po', riprendendo sempre l'esempio del masterizzatore, come se tutti i dettagli interni hardware e software che costituiscono tale dispositivo fossero sempre noti e visibili.
Viceversa, una classe che abbia tutti i metodi (compresi i costruttori) e le proprietà definite come private sarebbe una classe inutilizzabile (esiste tuttavia un tipo particolare di classe, denominata singleton, che vedremo a breve, il cui costruttore è private) poiché nessuno potrebbe avvalersi delle sue caratteristiche.
Vediamo qualche esempio in Java che faciliti la comprensione di quanto esposto:
package Prova;public class A
{
//Proprietà della classe
private int private_int = 1;
//nessun acces specifier = package access
int package_int = 2;
protected int protected_int = 3;
public int public_int = 4;
//Metodi
private void privateMethod()
{
System.out.println("output con accesso Private");
}
void packageMethod()
{
System.out.println("output con accesso Package");
}
protected void protectedMethod()
{
System.out.println("output con accesso Protected");
}
public void publicMethod()
{
System.out.println("output con accesso Public");
}
public static void main(String[] args)
{
A a = new A (); // Crea un oggetto di classe A
a.privateMethod();
a.packageMethod();
a.protectedMethod();
a.publicMethod();
System.out.println("private_int: " + a.private_int);
System.out.println("package_int: " + a.package_int);
System.out.println("protected_int: " + a.protected_int);
System.out.println("public_int: " + a.public_int);
}
}
L'output dell'esempio precedente sarà il seguente:
output con accesso Private
output con accesso Package
output con accesso Protected
output con accesso Public
private_int: 1
package_int: 2
protected_int: 3
public_int: 4
Tale output mette in evidenza come una classe abbia sempre accesso a tutte le sue proprietà e metodi, a prescindere dall'access specifier definito per essi.
Il prossimo esempio, invece, illustra la situazione indicata dalla seconda colonna della tabella precedente, ovvero l'accesso di una classe ai metodi e alle proprietà di un'altra classe appartenente allo stesso package:
package Prova;
public class B
{
public static void main(String[] args)
{
A a = new A();
a.privateMethod(); //Errore!!!
a.packageMethod();
a.protectedMethod();
a.publicMethod();
//Errore
System.out.println("private_int: " + a.private_int);
System.out.println("package_int: " + a.package_int);
System.out.println("protected_int: " + a.protected_int);
System.out.println("public_int: " + a.public_int);
}
}
Come si può notare, ci sono un paio di righe di codice evidenziate in rosso in cui viene messo in luce un errore. Infatti, se si provasse a compilare un programma del genere il compilatore identificherebbe un paio incongruenze causate proprio dalle due linee in rosso.
In queste due linee si sta provando ad accedere ad un metodo e ad una proprietà della classe A (definita nell'esempio iniziale) entrambi identificati da un access specifier di tipo private. Questo è l'unico caso in cui si genera un errore poichè tutti gli altri accessi (package, protected e public) non danno alcun problema.
Passiamo ad analizzare il comportamento descritto dalla terza colonna della tabella: l'accesso di una sottoclasse alle proprietà e ai metodi della sua classe padre (nel nostro esempio la sottoclasse appartiene ad un package diverso da quello della clase A per non ricadere nella casistica dell'esempio precedente):
package ProvaB;
import Prova.*;
public class C extends A
{
public static void main(String[] args)
{
A a = new A();
a.privateMethod(); // Errore
a.packageMethod(); // Errore
a.protectedMethod(); // Errore
a.publicMethod();
//Errore
System.out.println("private_int: " + a.private_int);
//Errore
System.out.println("package_int: " + a.package_int);
//Errore
System.out.println("protected_int: "+ a.protected_int);
System.out.println("public_int " + a.public_int);
C c = new C();
c.protectedMethod();
System.out.println("protected_int: " + c. protected_int);
}
}
Come si vede, ora gli errori di compilazione sono parecchi (sempre quelli evidenziati in rosso). Vale la pena di soffermarsi sul terzo e sul sesto errore, ovvero quello sulla invocazione del metodo protectedMethod()e sul tentativo di accesso alla variabile protected_int.
Infatti, dalle informazioni presenti sulla tabella iniziale ci si aspetterebbe che ad una classe figlia sia consentito far riferimento alle proprietà e ai metodi della classe padre quando su di essi è utilizzato l'access specifier protected.
In effetti, ciò è vero nel senso che è consentito ad una istanza di una classe figlia far riferimento ai metodi e alle proprietà implementate nella classe padre ma non è permesso accedere ad essi attraverso una istanza della classe padre. Le ultime tre righe di codice evidenziano proprio tale concetto.
L'ultimo esempio è quello correlato all'accesso del "Mondo Esterno" ai membri di una classe. Viene utilizzata, in tal caso, un'altra classe non derivante da A e appartenente ad un altro package:
package ProvaC;
import Prova.*;
public class D
{
public static void main(String[] args)
{
A a = new A();
a.privateMethod(); //Errore
a.packageMethod(); //Errore
a.protectedMethod(); //Errore
a.publicMethod();
//Errore
System.out.println("private_int: " + a.private_int);
//Errore
System.out.println("package_int: " + a.package_int);
//Errore
System.out.println("protected_int: "+ a.protected_int);
System.out.println("public_int: " + a.public_int);
}
}
Anche qui, le uniche righe di codice immuni da errori sono soltanto quelle in cui viene fatto accesso al metodo e alla proprietà con specificatore di accesso public. Tutte le altre righe sono errate e non verranno accettate dal compilatore.
1, 2, 3 ... 10 ... 19 Pagina Successiva