(:title Ingegneria del Software - Appunti del 4 Aprile 2009:)
Torna alla pagina di Ingegneria del Software
:: Ingegneria del Software - Appunti del 4 Aprile 2009 ::
Relazioni tra classi
Il diagramma delle classi mi permette anche di specificare quali relazioni intercorrano tra le varie classi. La faccenda non è di poca rilevanza, perché influenza moltissimo il design e la conseguente implementazione del nostro software.
Innanzitutto, possiamo dire che una classe rappresenta le proprietà che un oggetto (inteso in senso generico e non programmatorio) deve avere per poter dire di appartenere ad un certo insieme.
Le relazioni tra classi sono di tre tipi:
- ereditarietà
- associazione
- composizione
Ereditarietà
L'ereditarietà è una relazione molto stretta tra classi.
Una classe identifica un tipo del mio linguaggio di programmazione, come un int o un float. Dire che una classe eredita da un'altra vuol dire che la classe ereditante è un sottotipo della classe da cui eredita.
Se la classe è il criterio per identificare i membri di un insieme, allora la sottoclasse è il criterio per identificare sottinsiemi di quell'insieme. Se abbiamo l'insieme degli Animali e il sottinsieme dei Gatti, un oggetto del sottinsieme dei Gatti È anche un Animale, ma non è detto che un qualsiasi Animale sia anche un gatto.
La cosa importante è che una sottoclasse eredita dal padre l'interfaccia pubblica e, a seconda delle possibilità offerte da un dato linguaggio, anche l'implementazione della classe padre. Per questo bisogna fare molta attenzione quando si utilizza la relazione di ereditarietà.
In UML si segna in questo modo:
Un altro punto di vista per affrontare l'ereditarietà è dire che un sottotipo è una specializzazione del supertipo (o superclasse, basta ci sia dentro super). Il Gatto è una specializzazione dell'Animale, perché oltre a fare tutte le cose "standard" che un animale deve fare, può anche fare cose che gli altri animali non possono fare, per esempio fare le fusa.
A livello di implementazione, quello che una classe può fare è rappresentato dai suoi metodi. Se l'animale ha i metodi mangia() e bevi(), il Gatto avrà sicuramente i metodi mangia() e bevi(), ma anche il metodo pruprurra().
L'ereditarietà può andare avanti e non fermarsi al primo livello: posso avere intere gerarchie di tipi e sottotipi etc. Inoltre, a seconda del linguaggio di programmazione, posso avere un tipo che eredita da più di un padre (magie dell'ingegneria genetica). Ma questa è un'altra faccenda, permessa in C++ ma non in Java, e da usare con molta cautela.
In Java, per esempio, TUTTI gli oggetti sono sottotipi della classe primigenia che fantasiosamente è stata chiamata Object. Pertanto, se da qualche parte dichiaro un Array di Object, posso infilarci tutto quello che voglio:)
Cautela nell'uso dell'ereditarietà
Come dicevamo sopra, l'ereditarietà è un legame molto molto forte tra due classi (e tra i relativi oggetti istanze di quelle classi). Qualsiasi modifica faccio alla classe padre, avrà delle ripercussioni su tutta la sua progenie, il che è potenzialmente dannoso!
Il software engineer tende, al contrario, ad avere componenti il più possibile indipendenti, così che le modifiche ad uno non siano necessariamente delle catastrofi per il resto dell'ambiente. Per questo motivo preferiscono far sì che classi diverse si chiamino, piuttosto che siano legate dall'ereditarietà.
Associazione
L'associazione è una relazione molto debole, che dice solamente che un oggetto istanza di una certa classe ha una non meglio precisata nozione dell'esistenza di un oggetto appartenente ad un'altra classe.
Una relazione deve avere come soggetti due individui, cioè due oggetti. In un'associazione tuttavia non è detto che ciascun oggetto debba essere a conoscenza dell'esistenza dell'altro oggetto: l'Amante della Moglie sa dell'esistenza del Marito, ma il Marito non sa niente dell'esistenza dell'Amante.
UML permette di distinguere quindi il caso in cui entrambi sono a conoscenza l'uno dell'altro, e il caso in cui solo uno è a conoscenza dell'altro. Ecco come:
Nel disegno c'è scritto navigable. È il concetto di cui abbiamo appena parlato: se posso andare da A verso B, vuol dire che posso "navigare" da A verso B. Allo stesso modo, una linea semplice tra due classi vuol dire che ognuna è consapevole dell'esistenza dell'altra, e quindi posso partire da Class1 e arrivare a Class2, ma anche partire da Class2 ed arrivare a Class1. Il canale che connette Class1 e Class2 è navigabile in due direzioni.
A destra invece c'è un'associazione unidirezionale: in questo caso Class1 è a conoscenza di Class2, ma Class2 non sa niente di Class1. Posso navigare quel canale solo in una direzione.
Attenzione: se in UML segno l'esistenza di un'associazione, sto anche contemplando la possibilità che l'associazione rimanga insoddisfatta. Ovvero, dire che Class1 è associata a Class2 in realtà vuol dire che Class1 PUÒ essere associata a Class2, ma anche no.
Aggregazione
L'aggregazione è una relazione molto stretta tra due classi. Il concetto che descrive è quello che un certo oggetto è PARTE DI un altro oggetto.
Nell'associazione mi limito a dire che una classe ha nozione dell'esistenza di un'altra classe. Qui invece mi spingo oltre e dico che una classe è composta da una o più altre classi.
Questa composizione può essere obbligatoria oppure opzionale. Per spiegare la differenza, facciamo un esempio. Ho l'Essere Umano, ho la Testa e ho la Gamba. Un Essere Umano per essere completo deve avere sia la Testa che la Gamba, ma con una differenza: senza la Gamba, l'Essere Umano vive lo stesso. Senza la Testa, l'Essere Umano non può nemmeno esistere.
Nel caso della Testa, ho un'aggregazione immutabile, mentre nel caso della gamba l'aggregazione è opzionale. Le aggregazioni immutabili si segnano con una losanga nera, mentre quelle opzionali con una losanga vuota.
Molteplicità
Stiamo parlando del diagramma delle classi, e quindi ci si aspetta che le relazioni abbiano come soggetti le classi stesse. E invece no! La classe è solo la dichiarazione di un tipo, mentre le istanze di questa classe vengono chiamate oggetti, e saranno gli oggetti ad avere vera "vita" all'interno del software, a essere relazionati e composti e così via.
Nel caso dell'ultimo disegno che abbiamo visto, quello con Sailboat, Engine, Sail, Hull, ci sono dei numerini accanto alle frecce di aggregazioni. Essi stanno ad indicare la cardinalità dell'aggregazione stessa.
Per esempio, la freccia losangata che collega Sailboat ad Engine ha, su entrambi i lati, la dicitura 0..1:
- dal pdv di Sailboat, posso avere 0 oppure 1 Engine
- dal pdv di Engine, posso essere montato su 0 oppure 1 Sailboat
Prendiamo invece la freccia losangata che collega Sailboat a Sail. Sul lato di Sailboat ha la dicitura 0..1, mentre sul lato di Sail ha la dicitura *:
- dal pdv di Sailboat, posso avere 0 o 1 Sail
- dal pdv di Sail, posso essere montata su un numero qualsiasi (0, 100, 1000...) di Sailboat
Prendiamo ora l'ultima freccia losangata, tra Sailboat e Hull. Dal lato di Sailboat la dicitura è 0...1, con losanga nera. Dal lato di Hull la dicitura è 1. Avere la losanga nera significa che l'aggregazione è immutabile, ovvero, senza Hull la Sailboat non esiste nemmeno. Posso non avere nessun Hull, ma allora non esisto nemmeno.
Che cosa c'entra questo con la faccenda di cui parlavo prima, cioè che saranno gli Oggetti e non le Classi ad essere i veri soggetti delle relazioni? Ora lo spiego. Se ci pensiamo un attimo, c'è qualcosa di dissonante in questi disegni. Se esiste UNA classe chiamata Sailboat, UNA classe chiamata Engine, e UNA classe chiamata Sail, com'è possibile avere una relazione in cui compaia un numero imprecisato di Sail? Ovviamente non è così: la molteplicità della relazione mi dice quante istanze di Sail possono essere aggregate a Sailboat, e quindi sta parlando di Oggetti e non di Classi.
Vincoli
La molteplicità di una relazione può anche rappresentare un vincolo. Ad esempio, se su un capo di una linea metto 2..10, vuol dire che posso avere nozione di un numero che va da 2 a 10 istanze di quella particolare classe. Sto quindi esprimendo un vincolo sulla molteplicità.
Relazioni ternarie e oltre
Ecco, le relazioni ternarie, quaternarie etc. sono il male e vanno evitate. In tutti i linguaggi di programmazione, visto che le relazioni si esprimono tramite un attributo oppure coppie di attributi, tutto ciò che trascende l'1 e il 2 non va bene:)
UML mi permette di avere questo tipo di relazioni ternarie, ma per i motivi di cui parlavo sopra vanno evitate, altrimenti implementarle diventa un macello non più finito.
Torna alla pagina di Ingegneria del Software