martedì 18 settembre 2012

Programmazione ad oggetti in C (ereditarieta')

Qualche giorno fa ho ricevuto un commento via e-mail ad uno dei primi post su questo blog, quello relativo all'utilizzo di casting fra i puntatori per simulare la programmazione OOP in linguaggio C.
Il commento in questione era relativo all'introduzione di uno o piu' metodi pubblici nella classe, che facendo riferimento a due strutture in overlapping, poteva risultare impossibile.
Per comprendere bene come procedere occorre anzitutto considerare la struttura stessa del sistema:
  • il fatto che esistano due strutture, una privata e una pubblica, non significa che vi siano due "istanze" separate: si deve pensare alla parte pubblica come al file header di una classe C++ e alla struttura privata come alla sua implementazione;
  • l'introduzione di un metodo pubblico, come pure di un qualsiasi altro attributo, e' una operazione che non va fatta nella classe base, ma in eventuali sottoclassi. Se venisse infatti fatta sulla classe base si farebbe una "banale" ristrutturazione della classe stessa, che a meno di ovvi problemi di compatibilita' binaria, continuerebbe a funzionare come prima.

Ho quindi deciso di estendere l'esempio del conto corrente creando una sottoclasse, chiamata ContoCorrenteConCarta (nei file CCX_pub.h e CCX_private.c) che estende le informazioni di base del conto corrente aggiungendo una carta di credito. Ho anche colto l'occasione per estendere il conto corrente stesso con alcune ovvie informazioni che avevo tralasciato, come ad esempio il titolare del conto.
Tutti i sorgenti nella loro nuova versione sono disponibili su uno dei miei repository su github.

Come per la classe conto corrente di base, la classe con carta di credito deve definire due strutture sovrapposte, una privata che contiene l'implementazione e una pubblica che contiene l'interfaccia.
La parte pubblica e' definita come:


typedef struct CCX_PUB {

  // riferimento ad un conto corrente normale
  ContoCorrentePub* super;

  // metodo che ritorna il numero della carta di credito
  char* (*m_numero_carta)( struct CCX_PUB * );

} ContoCorrenteConCartaPub;



Si noti che la struttura contiene un puntatore alla sua superclasse, o meglio ad una istanza della superclasse. Il puntatore e' stato volutamente denominato "super" per meglio indicare il legame con una istanza della superclasse.
La parte implementativa risulta come segue:


typedef struct CCX_private{
 
  // riferimento alla parte pubblica
  ContoCorrenteConCartaPub pub;

  /*------ dati nascosti ------------*/

 
  // numero della carta di credito
  char* numero_carta;
 
} ContoCorrenteConCartaPrivate;



dove, come al solito, il primo campo e' una variabile di tipo "pubblico" per fare in modo che il casting di puntatori fra ContoCorrenteConCartaPub e ContoCorrenteConCartaPrivate possa funzionare come nell'esempio del conto corrente semplice.
Al solito si definiscono dei metodi di conversione da parte pubblica a privata per il nuovo tipo di conto corrente. Occorre anche definire un metodo di inizializzazione, che ad esempio puo' essere come il seguente:


ContoCorrenteConCartaPub* abilitaCarta( ContoCorrentePub* contoNormale, char* numero_carta ){

  // controlli di sicurezza
  if( contoNormale == NULL || numero_carta == NULL )
    return NULL;


  printf("\n\t[abilitaCarta] Abilitazione carta %s su conto %s",
         numero_carta,
         contoNormale->m_titolare( contoNormale ) );

  // costruisco un nuovo conto corrente esteso
  ContoCorrenteConCartaPrivate *ccpriv = malloc( sizeof( ContoCorrenteConCartaPrivate ) );


 
 
  // inserisco il conto normale nella struttura pubblica
  ContoCorrentePub* superClass = aperturaContoCorrente( 

                          contoNormale->m_numero_conto( contoNormale ),                           contoNormale->m_titolare( contoNormale )
                              );
  ccpriv->pub.super = superClass;

  // Metodo getter per la carta di credito
  ccpriv->pub.m_numero_carta = getNumeroCartaDiCredito;

  // inserisco il numero di carta nella struttura privata
  ccpriv->numero_carta = numero_carta;

  // oggetto costruito!
  return Private2Public( ccpriv );
 

}




Si noti che il metodo abilitaCarta funge da costruttore di copia: dato un conto corrente normale (senza carta), si costruisce un oggetto con carta di credito "attorno" a un clone del conto corrente.
Questa operazione di copia e' una pura scelta implementativa, sarebbe stato possibile usare il conto passato come argomento invece che crearne un clone. Si noti comunque che il clone ha dei puntatori alla stessa area dati (es. il titolare), e quindi questo costruttore effettua una shallow copy. Ovviamente questa implementazione non raffinata e' per mantenere l'esempio semplice.
L'uso della parola "superClass" all'interno del metodo non e' a caso: il conto normale di partenza contiene infatti i dati (e i metodi) che faranno si che il conto carta di credito possa essere trattato anche come conto normale. Inoltre il conto con carta prevede una variabile di tipo conto corrente normale al suo interno, creando quindi un link fra le due implementazioni che e' appunto il legame di ereditarieta'.
Volendo essere piu' precisi si dovrebbe sostituire il nome "superClass" con "super", per seguire una terminologia Java, ad ogni modo e' il concetto che conta.

Alla fine della costruzione si ha quindi un conto corrente con carta di credito che contiene un link ad un conto normale, inglobato in esso, e gli attributi aggiuntivi definiti nell'estensione della classe stessa.
L'utilizzo del nuovo tipo di conto e' semplice, ad esempio:


        printf( "\nCostruzione di un conto con carta di credito\n");
        ContoCorrenteConCartaPub *ccCarta = abilitaCarta( C1, "123456789" );

        ContoCorrentePub* ccpub = ccCarta->contoCorrente;
        printf( "\nCarta di credito per il conto %s = %s\n",
                ccCarta->super->m_titolare( ccCarta->super ),
                ccCarta->m_numero_carta( ccCarta ) );



Si noti che e' possibile "castare" il conto corrente con carta ad uno senza (ossia alla superclasse), accedendo al campo "super" della sua interfaccia.
Ne consegue che le due righe seguenti sono equivalenti:


ccCarta->super->m_titolare( ccCarta->super );
ccpub->m_titolare( ccpub );



E per il polimorfismo?
Avere un metodo polimorfico significa, lato pratico, che il puntato alla funzione contenuta nella implementazione (della superclasse) sia modificato.
In realtà la cosa e' leggermente piu' complessa: non deve essere infatti modificato il puntatore nella sueprclasse, ma un nuovo puntatore nella sottoclasse deve essere introdotto.
Infatti rimuovere/modificare il puntatore nella superclasse significa perdere ogni possibile legame con il metodo ridefinito, cosa che invece puo' tornare utile ad esempio se si deve invocare tale metodo.
Per semplicita' si supponga di voler sovrascrivere il metodo che fornisce il titolare di un conto corrente, in modo che nel caso ci sia una carta di credito venga stmapato il nome del titolare e il numero della carta.
La struttura pubblica del nuovo conto corrente con carta di credito viene quindi modificata come segue:


typedef struct CCX_PUB {

  // riferimento ad un conto corrente normale
  ContoCorrentePub* super;

  // metodo che ritorna il numero della carta di credito
  char* (*m_numero_carta)( struct CCX_PUB * );

  // overriding del metodo titolare di ContoCorrentePub
  char* (*m_titolare)( struct CCX_PUB * );

} ContoCorrenteConCartaPub;




La definizione del nuovo metodo e' la seguente, da inserire nella implementazione di ContoCorrenteConCartaPrivare:


static
char*
getTitolare( ContoCorrenteConCartaPub *ccpub ){
  if( ccpub == NULL )
    return NULL;

  ContoCorrenteConCartaPrivate *ccpriv = Public2Private( ccpub );
  ContoCorrentePub *super              = ccpub->super;
  char* titolare_super = super->m_titolare( super );
  char* titolare_desc  = malloc( sizeof( char ) * strlen( titolare_super ) * 2 );
  sprintf( titolare_desc, "%s (%s)", titolare_super, ccpriv->numero_carta );
  return titolare_desc;
}




Come si puo' notare il metodo combina della logica legata solo al conto con carta di credito a della logica della superclasse, andando infatti ad invocare il metodo della superclasse.
Il resto del codice viene modificato di conseguenza per inserire il nuovo puntatore a funzione e utilizzare il nuovo metodo.

Occorre fare una riflessione sul meccanismo del polimorfismo sopra descritto.
Anzitutto si noti come cambia l'argomento del metodo, che a seconda della posizione in cui si trova nella catena di ereditarieta' accetta un puntatore alla istanza di classe stessa. Questo e' anche il metodo con il quale molti linguaggi OOP implementano le chiamate di metodo.
Tuttavia questo rudimentale meccanismo non permette di effettuare una vera chiamata polimorfica: se si usa un puntatore alla superclasse (ContoCorrentePub) e si invoca il metodo si otterra' l'esecuzione del metodo della superclasse.
Affinche' anche la superclasse invochi il metodo definito per ultimo nella catena di ereditarieta' occorre modificare il puntatore alla funzione m_titolare di ContoCorrentePub affinche' punti alla nuova implementazione. All'interno del metodo si dovra' poi fare il cast esplicito da ContoCorrentePub a ContoCorrenteConCartaPub.
In questo modo pero' si rischia di perdere il legame al metodo precedente, e quindi di non poterlo piu' invocare. E' quindi compito della implementazione della sottoclasse mantenere i puntatori necessari e fare gli aggiustamenti ai pnntatori della superclasse stessa.

Nessun commento: