Cerca nel sito:
ricerca
avanzata

Frasi Celebri...

Il mondo ? una giungla senza alberi che sta aumentando.

Fenella Harrison 

Sondaggio:

Secondo voi quale squadra "ladra" di pi??

Juventus
Roma
Inter
Lazio
Chievo
Milan
Altro

visualizza risultati


 

Appendice B - Introduzione alla OOP

 

Nel corso degli anni sono stati proposti diversi paradigmi di programmazione, ovvero diversi modi di vedere e modellare la realta` (paradigma imperativo, funzionale, logico...).
Obiettivo comune di tutti era la risoluzione dei problemi legati alla manutenzione e al reimpiego di codice . Ciascun paradigma ha poi avuto un impatto differente dagli altri, con conseguenze anch'esse diverse. Assunzioni apparentemente corrette, si sono rivelate dei veri boomerang, basti pensare alla crisi del software avutasi tra la fine degli anni '60 e l'inizio degli anni '70.
In verita` comunque la colpa dei fallimenti non era in generale dovuta solo al paradigma, ma spesso erano le cattive abitudini del programmatore, favorite dalla implementazione del linguaggio, ad essere la vera causa dei problemi. L'evoluzione dei linguaggi e la nascita e lo sviluppo di nuovi paradigmi mira dunque a eliminare le cause dei problemi e a guidare il programmatore verso un modo "ideale" di vedere e concepire le cose impedendo (per quanto possibile e sempre relativamente al linguaggio) "cattivi comportamenti".

Di tutti i paradigmi proposti, uno di quelli piu` attuali e su cui si basano linguaggi nuovissimi come Java o Eiffel (e linguaggi derivati da altri come l'Object Pascal di Delphi e lo stesso C++), e` sicuramente il paradigma object oriented.
Ad essere precisi quello object oriented non e` un vero e proprio paradigma, ma un metaparadigma. La differenza sta nel fatto che un paradigma definisce un modello di computazione (ad esempio quello funzionale modella un programma come una funzione matematica), mentre un metaparadigma generalmente si limita a imporre una visione del mondo reale non legata ad un modello computazionale. Di fatto esistono implementazioni del metaparadigma object oriented basate sul modello imperativo (C++, Object Pascal, Java) o su modelli funzionali (CLOS ovvero la versione object oriented del Lisp).
Nel seguito, parleremo di paradigma ad oggetti (anche se il termine e` improprio) e faremo riferimento sostanzialmente al modello fornito dal C++; ma sia chiaro fin d'ora che non esiste un unico modello object oriented e non esiste neanche una terminologia universalmente accettata.

Il paradigma ad oggetti tende a modellare una certa situazione (realta`) tramite un insieme di entita` attive (che cioe` svolgono azioni) piu` o meno indipendenti l'una dall'altra, con funzioni generalmente differenti, ma cooperanti per l'espletamento di un compito complessivo. Tipico esempio potrebbe essere rappresentato dal modello doc/view in cui un editor viene visto come costituito piu` o meno da una coppia: un gestore di documenti il cui compito e` occuparsi di tutto cio` che attiene all'accesso ai dati e ad eseguire le varie possibili operazioni su di essi, ed un modulo preposto alla visualizzazione dei dati ed alla interazione con chi usa tali dati (mediando cosi` tra utente e gestore dei documenti).
Possiamo tentare un parallelo tra gli oggetti della OOP (Object Oriented Programming) e le persone che lavorano in una certa industria... ci saranno diverse tipologie di addetti ai lavori con mansioni diverse: operai piu` o meno specializzati in certi compiti, capi reparto, responsabili e dirigenti ai vari livelli. Svalgono tutti compiti diversi, ma insieme lavorano per realizzare certi prodotti ognuno occupandosi di problemi diversi direttamente connessi alla produzione, altri col compito di coordinare le attivita` (interazioni).
Comunque sia chiaro che gli oggetti della OOP sono in generale diversi da quelli del mondo reale (siano esse persone, animali o cose).

Le entita` attive della OOP (Object Oriented Programming) sono dette oggetti. Un oggetto e` una entita` software dotata di stato, comportamento e identita`. Lo stato viene generalmente modellato tramite un insieme di attributi (contenitori di valori), il comportamento e` costituito dalle azioni (metodi) che l'oggetto puo` compiere e infine l'identita` e` unica, immutabile e indipendente dallo stato, puo` essere pensata in prima approssimazione come all'indirizzo fisico di memoria in cui l'oggetto si trova (in realta` e` improprio identificare identita` e indirizzo, perche` generalmente l'indirizzo dell'oggetto e l'indirizzo del suo stato, mentre altre informazioni e caratteristiche dell'oggetto generalmente stanno altrove).
Vediamo come tutto questo si traduca in C++:


  class TObject {
    public:
      void Foo();
      long double Foo2(int i);

    private:
      const int f;
      float g;
  };


In questo esempio lo stato e` modellato dalle variabili f, g . Il comportamento e` invece modellato dalle funzioni Foo() e Foo2(int).

Gli oggetti cooperano tra loro scambiandosi messaggi (richieste per certe operazioni e risposte alle richieste). Ad esempio un certo oggetto A puo` occuparsi di ricevere ordini relativi all'esecuzione di certe operazioni aritmetiche su certi dati, per l'espletamento di tale compito puo` affidarsi ad un altro oggetto Calcolatrice fornendo il tipo dell'operazione da realizzare e gli operandi; l'oggetto Calcolatrice a sua volta puo` smistare le varie richieste a oggetti specializzati per le moltiplicazioni o le addizioni.
L'insieme dei messaggi cui un oggetto risponde e` detto interfaccia ed il meccanismo utilizzato per inviare messaggi e ricevere risposte e` quello della chiamata di procedura; nell'esempio di prima, l'interfaccia e` data dai metodi void Foo() e long double Foo2(int).

Ogni oggetto e` caratterizzato da un tipo; un tipo in generale e` una definizione astratta (un modello) per un generico oggetto. Non esiste accordo su cosa debba essere un tipo, ma in generale e accettata l'idea secondo cui un tipo debba definire almeno l'interfaccia di un oggetto.
In C++ il tipo di un generico oggetto si definisce tramite la realizzazione di una classe. Una classe (termine impropriamente utilizzato dal C++ come sinonimo di tipo) in C++ non definisce solo l'interfaccia di un oggetto, ma anche la struttura del suo stato (vedi esempio precedente) e l'insieme dei valori ammissibili.
Ogni tipo (non solo in C++) deve inoltre fornire dei metodi speciali il cui compito e` quello di occuparsi della corretta costruzione e inizializzazione delle singole istanze (costruttori) e della loro distruzione quando esse non servono piu` (distruttori).

Quando lo stato di un oggetto non e` direttamente accessibile dall'esterno, si dice che l'oggetto incapsula lo stato, taluni linguaggi (come il C++) non costringono a incapsulare lo stato, in questi casi gli attributi accessibili dall'esterno divengono parte dell'interfaccia.
L'incapsulazione ha diverse importanti conseguenze, in particolare forza il programmatore a pensare e realizzare codice in modo tale che gli oggetti siano in sostanza delle unita` di elaborazione che ricevono dati in input (i messaggi) e generano altri messaggi (generalmente diretti ad altri oggetti) in output che rappresentano il risultato della loro elaborazione. In tal modo un applicativo assume la forma di un insieme di oggetti che comunicando tra loro risolvono un certo problema.

Altro punto fondamentale del paradigma ad oggetti e` l'esplicita presenza di strumenti atti a conseguire un facile reimpiego di codice precedentemente prodotto. L'obiettivo puo` essere raggiunto in diversi modi, ciascuna modalita` e` spesso legata a caratteristiche intrinseche di un certo modello di programmazione object oriented. In particolare attualmente le metodologie su cui si discute sono:

  1. Reimpiego per composizione, distinguendo tra
    • contenimento diretto
    • contenimento indiretto

  2. Reimpiego per ereditarieta`, distinguendo tra:
    • ereditarieta` di interfaccia
    • ereditarieta` di implementazione

  3. Delegation
ciascuna con i suoi vantaggi e suoi svantaggi.
Nel reimpiego per composizione, quando si desidera estendere o specializzare le caratteristiche di un oggetto, si crea un nuovo tipo che contiene al suo interno una istanza del tipo di partenza (o in generale piu` oggetti di tipi anche diversi tra loro). L'oggetto composto fornisce alcune o tutte le funzionalita` della sua componente facendo da tramite tra questa e il mondo esterno, mentre le nuove funzionalita` sono implementate per mezzo di metodi e attributi propri dell'oggetto composto.
Un oggetto composto puo` contenere l'oggetto (e in generale gli oggetti) piu` piccolo direttamente (ovvero tramite un attributo del tipo dell'oggetto contenuto) oppure tramite puntatori (contenimento indiretto):


  class Lavoro {
    public:
      Lavoro(/* Parametri */);

      /* ... */

    private:
      /* ... */
  };

  class Lavoratore {
    public:
      /* ... */

    private:
      Lavoro Occupazione;   // contenimento diretto
      /* ... */
  };

  class LavoratoreAlternativo {
    public:
      /* ... */

    private:
      Lavoro* Occupazione;   // contenimento indiretto
      /* ... */
  };


Il contenimento diretto e` in generale piu` efficiente per diversi motivi:

  • Non si passa attraverso puntatori ogni qual volta si debba accedere alla componente;
  • Nessuna operazione di allocazione o deallocazione da gestire e semplificazione di problematiche legate alla corretta creazione e distruzione delle istanze;
  • Il tipo della componente e` completamente noto e sono possibili tutta una serie di ottimizzazioni altrimenti non fattibili.
Il contenimento per puntatori per contro ha i seguenti vantaggi:
  • La costruzione di un oggetto composto puo` avvenire per gradi, costruendo le sottocomponenti in tempi diversi;
  • Una componente puo` essere condivisa da piu` oggetti;
  • Come vedremo utilizzando puntatori possiamo riferire a tutto un insieme di tipi per quella componente, ed utilizzare di volta in volta il tipo che piu` ci fa comodo (anche cambiando a run time la componente stessa);
  • In linguaggi come il C++ in cui un puntatore e` molto simile ad un array, possiamo realizzare relazioni in cui un oggetto puo` avere da 0 a n componenti, con n determinabile a run time (la composizione diretta richiederebbe di fissare il valore massimo per n).
Concettualmente la composizione permette di modellare facilmente una relazione Has-a in cui un oggetto piu` grande possiede uno o piu` oggetti tramite i quali espleta determinate funzioni (il caso dell'esempio del Lavoratore che possiede un Lavoro). Tuttavia e` anche possibili simulare una relazione di tipo Is-a:


  class Persona {
    public:
      void Presentati();

    /* ... */
  };

  class Lavoratore {
    public:
      void Presentati();
      /* ... */
    private:
      Persona Io;
      char* DatoreLavoro;
      /* ... */
  };

  void Lavoratore::Presentati() {
    Io.Presentati();
    cout << "Impiegato presso " << DatoreLavoro << endl;
  }


Molte tecnologie ad oggetti (ma non tutte) forniscono un altro meccanismo per il reimpiego di codice: l'ereditarieta`.
L'idea di base e` quella di fornire uno strumento che permetta di dire che un certo tipo (detto sottotipo o tipo derivato) risponde agli stessi messaggi di un altro (supertipo o tipo base) piu` un insieme (eventualmente vuoto) di nuovi messaggi.
Quando si eredita solo l'interfaccia di un tipo (ma non la sua implementazione, ne l'implementazione dello stato e/o di altre caratteristiche del supertipo) si parla di ereditarieta` di interfaccia:


  class Interface {
    public:
      void Foo();
      double Sum(int a, double b);
  };

  class Derived: public Interface {
    public:
      void Foo();
      double Sum(int a, double b);
      void Foo2();
  };

  void Derived::Foo() {
    /* ... */
  }

  double Derived::Sum(int a, double b) {
    /* ... */
  }

  void Derived::Foo2() {
    /* ... */
  }


Si noti che quando si e` in presenza di ereditarieta` di interfaccia, la classe derivata ha l'obbligo di implementare tutto cio` che eredita (a meno che non si voglia derivare una nuova interfaccia), poiche` l'unica cosa che si eredita e` un insieme di nomi (identificatori di messaggi) cui non e` associata alcuna gestione. Infine (almeno in C++) per (ri)definire un metodo dichiarato in una classe base, la classe derivata deve ripetere la dichiarazione (ma cio` potrebbe non essere vero in altri linguaggi).
Alcuni modelli di OOP consentono l'ereditarieta` dell'implementazione (es. il C++), che puo` essere vista come caso generale in cui si eredita tutto cio` che definiva il supertipo; dunque non solo l'interfaccia ma anche la gestione dei messaggi che la costituiscono e pure l'implementazione dello stato del supertipo. Il vantaggio dell'ereditarieta` di implementazione viene fuori in quelle situazioni in cui il sottotipo esegue sostanzialmente gli stessi compiti del supertipo allo stesso modo (cambiano al piu` poche cose). Qualora il sottotipo dovesse gestire un messaggio in modo differente, viene comunque data la possibilita` di ridefinirne la politica di gestione:


  class Base {
    public:
      void Foo() { return; }
      double Sum(int a, double b) { return a+b; }
      /* ... */
    private:
      /* ... */
  };

  class Derived: public Base {
    public:
      void Foo();
      double Sum(int a, double b);
      void Foo2();
  };

  void Derived::Foo() {
    Base::Foo();
    /* ... */
  }

  void Derived::Foo2() {
    /* ... */
  }


Nell'esempio appena visto la classe Derived eredita da Base tutto cio` che a quest'ultima apparteneva (interfaccia, stato, implementazione dell'interfaccia); Derived aggiunge nuove funzionalita` (Foo2()) e ridefinisce alcune di quelle ereditate (ridefinizione di Foo()), mentre altre funzionalita` vanno bene cosi` come sono (Sum()) e dunque la classe non le ridefinisce.
In alcuni sistemi potrebbe essere fornita la sola ereditarieta` di interfaccia, cosi` che le sole possibilita` sono ereditare da una interfaccia per definire una nuova interfaccia, oppure utilizzare le interfacce per definire le operazioni che possono essere compiute su un certo oggetto (in questo caso si definisce la struttura di un certo insieme di oggetti dicendo che essi rispondono a quella interfaccia utilizzando una certa implementazione).
L'ereditarieta` modella in generale una relazione di tipo Is-a poiche` un sottotipo rispondendo ai messaggi del supertipo potrebbe essere utilizzato in sostituzione di quest'ultimo. La sostituzione di un supertipo con un sottotipo comunque non e` di per se garantita dalla ereditarieta`, perche` cio` avvenga deve valere il principio di sostituibilita` di Liskov.
Tale principio afferma che la sostituibilita` e` legata non (solo) all'interfaccia dell'oggetto, ma al comportamento; nulla infatti vieta in molti linguaggi OO (C++ compreso) di fare in modo che un sottotipo risponda ad un messaggio con un comportamento non coerente a quello del supertipo (ad esempio il metodo Presentati() del tipo Lavoratore potrebbe fare qualcosa totalmente diversa dalla versione del tipo Persona come visualizzare il risultato di una somma).
E` anche possibile utilizzare l'ereditarieta` per modellare relazioni Has-a, ma si tratta spesso (praticamente sempre) di un grave errore e quindi tale caso non verra` preso in esame poiche` un linguaggio (o una tecnologia) OO fornisce sempre almeno il contenimento (o una qualche sua espressione).

Infine la delegation e` un meccanismo che tenta di mediare composizione e ereditarieta`. L'idea di base e` quella di consentire ad un oggetto di delegare dinamicamente certi compiti a altri oggetti.
Esprimere tale possibilita` in C++ non e` semplice, perche` dovremmo ricorrere comunque al contenimento per implementare tale meccanismo:


  class TRectangle {
    public:
      int GetArea();
    /* ... */
  };

  class TSquare {
    public:
      int GetArea() {
        return RectanglePtr -> GetArea();
      }
    private:
      TRectangle* RectanglePtr;
  };


Tuttavia in un linguaggio con delegation potrebbero essere forniti strumenti opportuni per gestire dinamicamente problemi di delega e probabilmente essere soggetti a vincoli di natura diversa da quelli imposti dal C++.

In presenza di ereditarieta` (sia essa di interfaccia che di implementazione), viene spesso fornito un meccanismo che permette di lavorare uniformemente con tutta una gerarchia di classi astraendo dai dettagli specifici della generica classe e sfruttando solo una interfaccia comune (quella della classe base da cui deriva la gerarchia). Tale meccanismo viene indicato con il termine polimorfismo e viene implementato fornendo un meccanismo di late binding, ovvero ritardando a tempo di esecuzione il collegamento tra un generico oggetto della gerarchia e i suoi membri.
Per una piu` approfondita discussione sul polimorfismo si rimanda al capitolo IX, paragrafo 9.

 
   
–«  INDICE  »–

 

 

 

 
Powered by paper&pencil (carta&matita ) - Copyright © 2001-2022 Cataldo Sasso