• Non ci sono risultati.

Eredit`a e polimorfismo

usato per le classi destinate a contenere dati persistenti.

Biblioteca Catalogo RegistroUtenti Amministrazione «boundary» InterfBiblio RegistroBibliotecari Libro «entity» «entity» Utente «entity» Bibliotecario * * * * 1 prestito

Figura 5.2: Esempio: un progetto OO (2).

La Fig. 5.3 mostra un’ulteriore evoluzione del modello, in cui si imple-menta l’associazione logica prestito per mezzo di una classe apposita e di un registro dei prestiti incluso nel sottosistema di amministrazione della biblio-teca. Inoltre si `e scelto di implementare le varie aggregazioni per mezzo di classi istanziate dal template list, e si `e aggiunto un sottosistema di persi-stenza, che deve provvedere a memorizzare i vari cataloghi e registri in un database.

5.1 Eredit`a e polimorfismo

In questa sezione tratteremo tre concetti che sono alla base della progetta-zione orientate agli oggetti: l’eredit`a, il polimorfismo e il binding dinamico.

5.1.1 Eredit`a

Nella fase di analisi dei requisiti, l’eredit`a permette di modellare la relazio-ne di gerelazio-neralizzaziorelazio-ne (e quindi di specializzaziorelazio-ne) fra entit`a del dominio di applicazione. Di solito, alle entit`a del dominio dell’applicazione devono corrispondere delle entit`a del sistema software. Nella fase di progetto viene definita un’architettura software in cui vengono mantenute le relazioni fra entit`a del dominio di applicazione, fra cui la generalizzazione, come mostra il seguente esempio (in C++):

Catalogo list<Libro> Libro «entity» RegistroPrestiti list<Prestito> «entity» Prestito Amministrazione Persistenza Database RegistroUtenti list<Utente> «entity» Utente * * 1 * * 1 1

Figura 5.3: Esempio: un progetto OO (3).

class Person { char* name; char* birthdate; public: char* getName(); char* getBirthdate(); };

class Student : public Person { char* student_number;

public:

char* getStudentNumber(); };

In questo caso il fatto che la classe Student erediti dalla classe Person corrisponde al fatto che nel dominio dell’applicazione (per esempio, il da-tabase dei dipendenti e degli studenti di una scuola) gli studenti sono un sottoinsieme delle persone. La classe derivata ha gli attributi e le operazioni della classe base, a cui aggiunge ulteriori attributi e operazioni.

5.1. EREDIT `A E POLIMORFISMO 147 In fase di progetto si pu`o usare l’eredit`a come tecnica di riuso, cio`e per riusare le operazioni di una classe base nel contesto di una classe derivata: per esempio, se si dispone di una classe Shape con le operazioni per dise-gnare e spostare una figura geometrica, si pu`o definire una classe derivata ColoredShape che aggiunge le operazioni per colorare una figura, e riusa le operazioni della classe base per disegnarla e spostarla. Questo `e conveniente quando la classe derivata ha una semantica (cio`e uno scopo e un significato) analoga a quella della classe base, rispettando cos´ı il principio di Liskov. In molti casi, per`o, il meccanismo della composizione (usare un’istanza di una classe, o un puntatore ad essa, come attributo di un’altra) `e pi´u flessibile del-l’eredit`a. `E comunque da evitare l’uso dell’eredit`a per costruire una classe derivata che non abbia una parentela logica con la classe base. Per esempio, sia le aziende che le persone hanno un nome e un indirizzo, ma questa non `e una buona ragione per definire una classe Azienda come derivata di una classe Persona.

5.1.2 Polimorfismo e binding dinamico

Il polimorfismo ed il binding dinamico sono due concetti che nei linguaggi orientati agli oggetti sono strettamente legati fra di loro e con il concetto di eredit`a.

Il polimorfismo `e la possibilit`a che un riferimento (per esempio un iden-tificatore o un puntatore) denoti oggetti o funzioni di tipo diverso. Esistono diversi tipi di polimorfismo, e la forma di polimorfismo tipica dei linguaggi object oriented `e quella basata sull’eredit`a: se una classe D deriva da una classe B, allora ogni istanza di D `e anche un’istanza di B, per cui qualsiasi riferimento alla classe D `e anche un riferimento alla classe B. Questo tipo di polimorfismo si chiama polimorfismo per inclusione. In C++, per esempio, un oggetto di classe D pu`o essere assegnato ad un oggetto di classe B ed un valore di tipo “puntatore a D” o “riferimento a D” pu`o essere assegnato, rispettivamente, ad una variabile di tipo “puntatore a B” o “riferimento a B”. Osserviamo che in questo e in altri linguaggi esistono altre forme di polimorfismo, come l’overloading, che non sono legate all’eredit`a.

Il binding `e il legame fra un identificatore (in particolare un identificatore di funzione) ed il proprio valore. Si ha un binding dinamico quando il signi-ficato di una chiamata di funzione (cio`e il codice eseguito dalla chiamata) `e noto solo a tempo di esecuzione: il binding dinamico `e quindi il meccanismo che rende possibile il polimorfismo. In C++, le funzioni che vengono invocate con questo meccanismo sono chiamate virtuali . Se si chiama una funzione

virtuale di un oggetto attraverso un puntatore, questa chiamata `e polimorfica (per inclusione). Consideriamo questo (classico) esempio:

// file Shape.h class Shape {

Point position; public:

virtual void draw() {};

virtual void move(Point p) { position = p; }; };

// file Circle.h #include "Shape.h"

class Circle : public Shape { //...

public:

void draw() { cout << "drawing Circle\n"; }; };

// file Square.h #include "Shape.h"

class Square : public Shape { //...

public:

void draw() { cout << "drawing Square\n"; }; }; // file main.cc #include "Circle.h" #include "Square.h" void drawall(Shape** shps) {

for (int i = 0; i < 2; i++) shps[i]->draw();

} main() {

Shape* shapes[2];

shapes[0] = new Circle; shapes[1] = new Square; drawall(shapes);

}

La struttura del programma pu`o essere rapresentata in UML come in Fig. 5.4, dove lo stereotipo ≪utility≫ ´ındica che una classe contiene solo operazioni o dati globali (non `e quindi una vera classe, ma un espediente per rappresentare costrutti non orientati agli oggetti).

5.1. EREDIT `A E POLIMORFISMO 149 Shape Circle Square «use» «utility» Globals drawall()

Figura 5.4: Esempio: polimorfismo (1)

Nella funzione drawall() il tipo dell’oggetto a cui viene applicata la fun-zione draw() (che deve disegnare una figura sullo schermo) `e noto soltanto a tempo di esecuzione: si sa solo che l’oggetto apparterr`a alla classe Shape o a una classe derivata da questa. La funzione draw() `e quindi polimorfica, e viene chiamata con binding dinamico. Il fatto che la funzione drawall() ignori il tipo degli oggetti su cui deve operare ne migliora grandemente la mo-dularit`a e la riusabilit`a rispetto ad un’implementazione che invece richieda una conoscenza statica dei tipi. Questa funzione `e completamente disaccop-piata dall’implementazione delle classi Circle e Square, e inoltre continua a funzionare, immutata, anche se si aggiungono altre classi derivate da Shape.

5.1.3 Classi astratte e interfacce

Osserviamo che nel nostro esempio l’operazione draw() della classe Shape ha un’implementazione banale, `e un’operazione nulla, poich´e non si pu`o disegna-re una forma generica: il concetto di “forma” (shape) `e astratto, e si possono disegnare effettivamente solo le sue realizzazioni concrete, come “cerchio” e “quadrato”. La classe Shape `e in realt`a un cattivo esempio di programma-zione, poich´e non rappresenta adeguatamente il concetto reale che dovrebbe modellare, e questa inadeguatezza porta alla realizzazione di software poco affidabile. Infatti `e possibile istanziare (contro la logica dell’applicazione) un oggetto Shape a cui si potrebbe applicare l’operazione draw(), ottenendo un risultato inconsistente.

Una prima correzione a questo errore di progetto consiste nel rendere esplicito il fatto che la classe rappresenta un concetto astratto. In C++, questo si ottiene specificando che draw `e un’operazione virtuale pura, per mezzo dello specificatore ‘= 0’:

class Shape {

Point position; public:

virtual void draw() = 0;

virtual void move(Point p) { position = p; }; //...

};

La classe Shape `e ora una classe astratta, cio`e non istanziabile a causa dell’incompletezza della sua implementazione (Fig. 5.5).

«use» «utility» Globals drawall() position: Point draw() Shape move() Circle Square draw() draw()

Figura 5.5: Esempio: polimorfismo (2)

Una struttura ancor pi´u modulare si pu`o ottenere rappresentando espli-citamente l’interfaccia, separandola dall’implementazione.

// file Shape.h class IShape { public:

virtual void draw() = 0;

virtual void move(Point p) = 0; };

class Shape : public IShape { Point position;

public:

virtual void draw() = 0;

5.1. EREDIT `A E POLIMORFISMO 151

};

class Circle : public Shape { //...

public:

void draw() { cout << "drawing Circle\n"; }; };

class Square : public Shape { //...

public:

void draw() { cout << "drawing Square\n"; }; };

void drawall(IShape** shps) {

for (int i = 0; i < 2; i++) shps[i]->draw();

} main() {

IShape* shapes[2]; shapes[0] = new Circle; shapes[1] = new Square; drawall(shapes);

}

In questa versione la classe IShape definisce l’interfaccia comune a tutti gli oggetti che possono essere disegnati o spostati, la classe Shape definisce la parte comune delle loro implementazioni (il fatto di avere una posizione e un’operazione che modifica tale posizione), e le classi rimanenti definiscono concretamente i metodi per disegnare le varie figure. Questa struttura si pu`o schematizzare come in Fig. 5.6.

5.1.4 Eredit`a multipla

L’eredit`a multipla si ha quando una classe eredita “in parallelo” da due o pi´u classi. Questa possibilit`a `e particolarmente utile per ottenere un’interfaccia come composizione di altre interfacce. Le interfacce delle classi base rappre-sentano diversi aspetti delle entit`a modellate dalla classe derivata. I clienti della classe derivata possono trattare separatamente questi diversi aspetti, ottenendo cos´ı un buon disaccopiamento fra le varie classi.

Circle draw() Square draw() «use» «utility» Globals drawall() draw() «interface» IShape move() position: Point Shape move()

Figura 5.6: Esempio: polimorfismo (3)

Nel seguente esempio (in Java) un videogioco deve simulare dei velivo-li2. Ciascuno di questi viene visto da due punti di vista: la simulazione del comportamento (Aircraft) e la raffigurazione grafica (Drawable). Que-sti due aspetti vengono geQue-stiti da due sottosistemi diQue-stinti, rispettivamente AirTrafficCtrl e DisplayMgr. Il sistema `e schematizzato in Fig. 5.7.

abstract class Aircraft { public double speed;

public abstract void fly(AirTrafficCtrl atc); }

interface Drawable { void draw(); }

class JetLiner extends Aircraft implements Drawable { public JetLiner() { speed = 850.0; }

public void fly(AirTrafficCtrl atc) { /* flight simulation */ } 2