Newsgroup: it.comp.lang.c++
Titolo: Ereditarietà: problema di eleganza

Supponiamo di avere le seguenti classi
class RUOTA
{
 int ResistenzaInStrada;
}
class RUOTA_DA_JEEP : public RUOTA
{
  int ResistenzaFuoristrada;
}
class MACCHINA
{
RUOTA *pModelloRuota;
}
class JEEP : public MACCHINA
{
RUOTA_DA_JEEP *pModelloRuota;
}
La struttura deve essere questa per problemi di ereditarietá di funzioni e
membri perché le classi sono piú corpose di quelle che ho dichiarato.
Problemi:
Secondo me questa non è una buona definizione perché al suo interno un
oggetto di classe JEEP avrà sia un puntatore a MACCHINA::pRuota che una a
RUOTA_DA_JEEP.
Se io utilizzo un funzione di JEEP secondo me per un problema di scope
utilizza il puntatore a RUOTA_DA_JEEP, ma se io utilizzo una funzione
ereditata probabilmente utilizzerà il puntatore a MACCHINA::pRuota.
Potrei non definire in JEEP il puntatore a RUOTA_DA_JEEP *pModelloRuota e
utilizzare solo quello di macchina facendo attenzione che in realtà se la
classe è JEEP le sue ruote siano quelle opportune, ma tutte le volte che ho
bisogno dell'attributo ResistenzaFuoristrada devo fare un cast per accedere.
Esiste un modo più elegante ?
ciao e grazie
    stefano

 Esiste un modo più elegante ?
Si`, ma non certo entro l'assioma che tu inizi col porre, cioe` che "la
struttura dev'essere questa".  Se "la struttura deve essere questa",
allora non puo` essere simultaneamente "piu` elegante" di questa
(dove "piu`" sia inteso come strettamente-maggiore; se inteso come
maggiore-o-eguale, allora il problema e` triviale:-).
Senza ricorrere al "generic programming" (template), cioe` come si
dovrebbe fare ad es. in Java, l'approccio piu` elegante consiste in
una gerarchia di classi astratte (interfacce), separate dagli aspetti
implementativi di queste astrazioni (che in C++ si potrebbero tenere
come classi di implementazione da ereditare molteplicemente secondo
l'idioma mixin, o, piu` in generale e a prezzo di un certo "boilerplate"
nel linguaggi che non supportano il concetto di delegazione generica
[o "blind delegation", ecc], con incapsulamento e delegazione).
Nel caso generale (in cui le classi [astratte] derivate "nascono dopo"
quelle che estendono, e non possono dunque "ampliarle"), questa
impostazione non risparmia un qualche tipo di cast dinamico (che
eventualmente puo` essere consolidato in idiomi di tipo, e.g., la
QueryInterface del COM, ma senza grossi miglioramenti semantici).
(In alcuni casi particolari, in cui si possa contare su certi aspetti di
stabilita` del sistema, esistono idiomi, come "visitor", che possono
risparmiare questo indebolimento dei controlli compile-time; ma non
hanno applicabilita` generale, e in particolare non sarebbero, penso,
di grande aiuto nel tuo specifico caso).  Fra i migliori esempi di
questo stile vi e` la libreria InterViews (ora Fresco); esso e` alla
base della gran parte delle problematiche di progetto esaminate in
"Design Patterns" di Gamma, Vlissides, etc.
Aggiungere il "generic programming" (in C++, i template) risolve
la questione in modo molto potente ed elegante, almeno tutte le
volte che la "estendibilita`" del sistema non vada intesa come
_selvaggiamente_ dinamica (se occorre "estendere" il sistema
in modo _veramente_ dinamico, caricando a run-time delle nuove
librerie dinamiche senza alcuna interruzione o restart del funzionamento
del sistema stesso, l'utilita` dei template e` invece piu` modesta).
Fra i migliori esempi che conosco di sfruttamento di queste
potenzialita`, vi e` la libreria ATL della Microsoft (inclusa con
VC++6, ma anche, credo, scaricabile liberamente dal sito MS).
Fra i testi che iniziavano ad esplorare questo uso dei template,
suggerisco il testo Barton & Nackmann sull'uso del C++ per la
programmazione tecnica/scientifica (datato, ma molto valido).
Stringendo all'osso, la "chiave di volta" di questo uso dei template
e` che una classe-base, senza violare il principio Open-Closed,
puo` essere resa "cosciente" delle potenziali classe-derivate con
il semplice idioma di passare la classe-derivata come parametro
di template della classe base stessa (occorre dunque, ovviamente,
che la classe-base sia un template, per usare questo idioma).
Per esempio (giusto per dare un'idea)...:
    template<class TipoMacchina>
    class Macchina {
    protected:
        typedef RuotaGenerica TipoRuotaBase;
    public:
        typedef TipoMacchina::TipoRuota TipoRuota;
    private:
        TipoRuota* pRuota;
    public:
        TipoRuota& getTipoRuota() {
            return *pRuota;
        }
    };
    class Berlina: Macchina<Berlina> {
    public:
        typedef Macchina<Berlina> Base;
        typedef Base::TipoRuotaBase TipoRuota;
    };
    class Jeep: Macchina<Jeep> {
    public:
        typedef RuotaDaJeep TipoRuota;
    };
Si possono concepire diverse fattorizzazioni a seconda delle
esigenze (l'idioma "traits<...>", in particolare, puo` essere
molto importante in questi contesti).  Il riuso del codice, nel
senso di minimizzare la "impronta" del programma (quantita`
di codice eseguibile) puo` richiedere diversi accorgimenti, a
seconda delle possibilita` di ottimizzazione offerte dal proprio
ambiente di sviluppo (ad esempio, un compilatore moderno
puo` segnare esplicitamente nel codice che genera il fatto
che certi brani eseguibili vengono dall'espansione di certi
template, e, a tempo di link, "collassare" cosi`, in modo piu`
o meno automatico, eventuali duplicazioni di codice macchina
che possano essere riscontrate; se non si puo` far conto su
questo tipo di ottimizzazioni [che per fortuna stanno rapidamente
diventando abbastanza standard] puo` essere necessario piu`
lavoro da parte del programmatore per aiutare il compilatore).
Alex

 RUOTA_DA_JEEP.
Questo, secondo me, significa una sola cosa: che una JEEP non è una
MACCHINA. Potresti creare una classe base comune astratta (VEICOLO?) e
da questa ereditare sia per MACCHINA che per JEEP. Un VEICOLO non
avrebbe delle ruote ben definite ma sarebbe lasciato alle classi
derivate stabilire il tipo di ruote necessarie (una MACCHINA ha delle
ruote così come anche una JEEP).
EMG

Una soluzione si chiama polimorfismo.
Realizza una classe astratta Ruota con metodi virtuali da cui derivare
le varie tipologie di ruote. La classe Macchina dovra` mantenere un
puntatore (pRuota) alla classe Ruota, le classi che derivano da Macchina
devono solo assicurarsi che venga inserito il giusto modello
di ruota assegnando a pRuota un valore del tipo corretto:
Un esempio (sicuramente da adattare alle tue esigenze):
class Ruota {
   public:
      /* ... */
      // se proprio ti serve l'accessor:
      virtual int GetResistenzaInStrada() const;
};
class RuotaDaJeep {
   public:
      /* ... */
      virtual int GetResistenzaInStrada();
};
int RuotaDaJeep::GetResistenzaInStrada() const {
   return /* costante */
}
class Macchina {
  public:
      explicit Macchina(Ruota* ruota);
      /* ... */
  private:
     Ruota* pRuota;
};
Macchina::Macchina(Ruota* ruota): pRuota(ruota) {}
class Jeep : public Macchina {
   public:
      Jeep();
      /* ... */
};
Jeep::Jeep() : Macchina(new RuotaDaJeep) {}
Altre soluzioni potrebbero anche essere migliori, dipende dal tuo 
problema e dagli obiettivi da raggiungere. Ad esempio potrebbe
darsi che le differenze tra le varie tipologie di ruote sia
unicamente nel valore iniziale di parte (o tutti) i membri dati
e che il comportamento sia regolato unicamente da questi
parametri. In tal caso potresti realizzare una unica classe
Ruota con costruttore ad hoc ed utilizzare una classe
factoty che su richiesta ti crea il tipo di ruota da te desiderato.
Paolo
-----------------------------------------------
  Paolo Marotta
  Computer Science, University of Pisa
  home:   
http://www.cli.di.unipi.it/~marotta
-----------------------------------------------

 ruote così come anche una JEEP).
Usare solo classi astratte come basi e` in effetti una buona cosa.
Ma non mi sembra che qui sia questo il problema semantico profondo.
Supponiamo pure che da qualche parte, lontano nella gerarchia
delle classi antenate, ci sia un VEICOLO che di ruote non sa e non
vuole sapere.  Benissimo.  Ma, a un qualche livello intermedio, ci
potra` pur essere un VEICOLO_SU_RUOTA, no?  E allora, il buon
VEICOLO possiamo qui ignorarlo, e concentrarci invece sul
VEICOLO_SU_RUOTA.
Se un VEICOLO_SU_RUOTA determina "autonomamente" il
proprio tipo di ruote, e fornisce solo un accessore che _torna_
un RUOTA*, non abbiamo problemi.  Se invece esso permette
di _cambiare tipo di ruote_ (con un mutatore che _accetta_
un RUOTA*), allora i problemi nascono, perche` allora nessun
erede (classe che IS-A VEICOLO_SU_RUOTA) puo` imporre
vincoli sul tipo di ruote che puo` montare -- il che riduce la
utilita` di VEICOLO_SU_RUOTA stesso.
Un approccio e` quello di togliere del tutto i mutatori e averli
invece in un'_altra_ classe astratta "parallela".  Un altro e`
quello di avere solo _mutatori CONDIZIONALI_, cioe` metodi
come:
    void CambiaRuotaSeVaBene(RUOTA* pr,
        bool& hoCambiatoRuota);
con semantica "se *pr e` accettabile per il VEICOLO_SU_RUOTA,
essa viene posta come nuova RUOTA* del veicolo stesso, e
hoCambiatoRuota e` posto a true; altrimenti, hoCambiatoRuota
e` posto a false, e la RUOTA* del veicolo stesso non cambia".
Quest'ultima soluzione equivale ad un controllo di tipo "dinamico",
ma questo non deve necessariamente portare a strapparsi i
capelli -- certe volte, ci vuole (cfr. Coplien, "Advanced C++
Idioms"); ad esempio e` l'unico approccio sensato per un sistema
che debba essere estendibile in modo davvero dinamico (cioe`
caricando nuove DLL o .so e continuando a funzionare senza
interruzioni...!).  Certo, se si puo` evitare, quanti piu` controlli
si possono fare a tempo di compilazione, tanto meglio e` -- ma
i controlli di questo tipo ("gerarchie parallele") si incastrano
male nella logica matematica della programmazione OO (e in
particolare NON rispettano il principio di Liskov).  E` il problema
della covarianza, tanto per intenderci... duro anzicheno`.
Comunque, problemi analoghi li abbiamo se deriviamo
Quadrato da Rettangolo (dati ridondanti -- non e` un
problema se usiamo solo classi astratte; i mutatori invece
sono un problema eccome), ovvero SaccoDiBanane da
SaccoDiFrutti (dati ridondanti, ovvero dynamic_cast; e,
sempre li` a rompere, i mutatori, che devono essere, o
vincolati, o inesistenti...).
Accoppiare il generic programming all'OO aiuta moltissimo
con questi problemi profondi e radicati della semantica OO
(per il tipo di "estensioni" che si possono fare tutte a
compile-time -- non per il tipo che deve funzionare con
un semplice aggiornamento di librerie dinamiche in un
progetto che deve continuare in esecuzioni), come ho
gia` cercato di tratteggiare nel post precedente...
Alex

 Hmmm... non mi pare.  Dove sarebbe violata la relazione di IS-A?
Premetto che la mia competenza su questo tipo di problemi è molto bassa.
Comunque, a me sembrava che la JEEP fosse un altro tipo di VEICOLO e non
una MACCHINA in quanto non è completamente sostituibile ad essa. Mi
rendo ora conto però che con il termine MACCHINA intendevo più BERLINA o
AUTO_DA_CITTA.
EMG

 AUTO_DA_CITTA.
Ammappa... ai miei tempi, le _macchine_ erano: leva, carrucola, piano
inclinato, ...   :-)
Alex

 inclinato, ...   :-)
Hai ragione. Ma sai, nel linguaggio corrente, almeno dalle mie parti
(Roma), si usa quasi sempre macchina intendendo automobile. :-( Perň mai
intendendo anche jeep. Questo potrebbe significare qualcosa, non ti
pare?
EMG

 pare?
Che avete poche jeep (o che ne avevate poche quando si e`
formato il linguaggio che ancora oggi usate).  Qui, di jeep e
para-jeep in strada ce ne sono tantissime, ormai.
Alex

 para-jeep in strada ce ne sono tantissime, ormai.
E le chiamate "macchine" anch'esse? Comunque, tornando al problema, sei
d'accordo che una BERLINA e una JEEP non possono essere considerate
sostituibili?
EMG

 sostituibili?
D'accordissimo che non vale la relazione E`-UN, in nessuno
dei due versi (si tratta di "beni sostituibili" in un senso del
tutto diverso, quello tecnico usato dagli economisti, ma cio`
e` un diverso problema:-).
D'altra parte, filosoficamente e` sostenibile che E`-UN non
vale mai fra *oggetti concreti* (e` sostanzialmente il modo
in cui si e` risolta la ridda di paradossi che, nel periodo del
tumulto sulle fondazioni della matematica, rischiavano di
minare appunto queste fondazioni: la distinzione fra classe
e insieme risolve i paradossi, e il suo corrispondente in C++
e` il consiglio di Meyers, _mai ereditare da classi concrete_).
Se non mi sono spiegato...: un possibile e sensato modo
di modellare il mondo e` quello di dire che, se C e` una
classe che ha sottoclassi "istanziabili" (cioe` delle quali
si hanno istanze concrete), allora C non ha istanze concrete
che non siano anche istanze di una sua qualche sottoclasse.
Ad esempio: un "barboncino" e`-un "cane"; non esiste pero`
un concreto "generico cane" che non sia anche, o "barboncino", 
o "rotweiler", o "terranova", o ..., o "bastardino" naturalmente:-).
Cioe`, se il mio modello-del-mondo e` tale che uso come
classi istanziabili le specifiche razze dei cani, "non posso" allo
stesso tempo (cioe` chiaramente _posso_, ma non mi conviene,
perche` fa confusione) considerare "concreta", ovverossia
"istanziabile", la super-classe "cane"; sarebbe un "mischiare
livelli di discorso".  Una "specifica istanza di ``cane''" deve
in realta` essere, modellando a questo livello, una specifica
istanza di ``una qualche subclass di "cane"'' -- posso, ovvio,
ignorare la subclass e trattare polimorficamente la specifica
istanza usando solo i metodi che tutti i cani hanno in comune,
e questo e` correttissimo e sacrosanto, ma e` anche un
discorso del tutto diverso.
Se no si rischia di cadere nel famoso racconto di Borges
("i cani si dividono in: cani che abbaiano, cani gialli, cani
che appartengono all'Imperatore, ...":-).  Confondere
una partizione con un ricoprimento e` concettualmente
costoso benche` narrativamente gustoso:-)
Alex

 Se non mi sono spiegato...:
Ti sei spiegato benissimo, grazie. Non sai quanto sto imparando da te. E
fai benissimo a ribadire più e più volte concetti così fondamentali
dell'informatica e non solo di essa. Prima o poi mi (ci) entreranno in
testa... :-)
EMG

 testa... :-)
Chissa` che a furia di ripeterli non finisca per _capirli_ del
tutto anch'io...!-)
Alex

 tutto anch'io...!-)
Bravo. Questa è l'essenza dell'apprendimento.
EMG