domenica 10 febbraio 2008

Programmazione ad oggetti

La programmazione ad oggetti (OOP = Object Oriented Programming) rappresenta un paradigma di programmazione molto potente, e uno di quelli maggiormente affermati negli ultimi anni.

La programmazione ad oggetti si basa fondamentalmente su tre principi:
  1. incapsulamento: è la capacità di nascondere all'esterno dati e servizi, inglobandoli in un modulo che risulti inviolabile dall'esterno.
  2. ereditarietà: è la capacità di estendere un modulo aggiungendo o modificando alcuni dei suoi servizi.
  3. polimorfismo: è la capacità di variare il comportamento di un modulo a seconda di come viene acceduto.
L'idea attorno al quale ruota l'OOP è il concetto di classe, un'astrazione di dato con annessi relativi servizi. La classe supporta i tre principi dell'OOP sopra descritti, e infatti:
  1. può nascondere i dettagli implementativi dei propri dati all'esterno (incapsulamento).
  2. può essere estesa da una classe figlia, che implementi ulteriori servizi (ereditarietà).
  3. può venire acceduta con ciascuna interfaccia appartenente ad una delle sue superclassi, ma il suo comportamento dipenderà dalla sua implementazione concreta (polimorfismo).
E' importante notare come, grazie all'ereditarietà, il codice nuovo possa usare il codice vecchio. Ma grazie al polimorfismo si ha che il codice vecchio utilizza quello nuovo. Nel primo caso (ereditarietà) si ha che da una implementazione "vecchia" se ne può ottenere una nuova definendo (o ridefinendo) solo i servizi necessari (e quindi utilizzando i rimanenti servizi vecchi). Nel secondo caso (polimorfismo) si ha che il codice "vecchio" che accede tramite un'interfaccia di una superclasse ad una implementazione nuova, sfrutta la nuova implementazione senza rendersene conto.

Per meglio chiarire, si consideri il seguente esempio:


public class Automobile{
// incapsulamento: la velocità e' nascosta all'esterno e può essere
// acceduta solo tramite opportuni servizi
protected int speed = 0;

// servizio pubblico per variare la velocita', si noti che il chiamante
// non conosce nessun dettaglio implementativo circa la velocita'
public void accelera(){ this.speed++; }
}



public class AutomobileSportiva extends Automobile {

// l'automobile sportiva utilizza come base Automobile

// Un'auto sportiva ha una accelerazione forte. Si noti che si ridefinisce
// un servizio che si basa sulle proprieta' del codice vecchio, ossia la variabile
// speed.

public void accelera(){
for(int i=0; i<4;>
this.accelera();
}

}



Si supponga che in un programma ci sia un metodo che utilizza l'automobile:


public void corri(Automobile auto){ auto.accelera(); }


Ora, è evidente che se a questo metodo viene passata un'istanza di AutomobileSportiva, il codice in questione (vecchio) utilizza il nuovo comportamento (polimorfismo). Ecco quindi che senza alterare il codice vecchio si è inserito un comportamento nuovo, e quindi il codice vecchio usa il codice nuovo. Similarmente, AutomobileSportiva rappresenta codice nuovo che utilizza quello vecchio: il suo metodo accelera() richiama il codice vecchio della superclasse.

Da notare come il comportamento ottenuto dipenda dal tipo run-time effettivo, ossia il polimorfismo mostra sempre l'ultimo comportamento definito nel tipo run-time fra quelli disponibili a partire dal tipo statico.

Si noti che il polimorfismo da solo non basta: è indispensabile disporre anche del principio di sostituzione di Liskov, che consente di utilizzare l'interfaccia di una superclasse per riferirsi ad una sottoclasse.

Nessun commento: