Dr. Carlo Pescio
Oggetti e Quantità

Pubblicato su Computer Programming No. 72


Incapsulare il concetto di Quantità sembra semplice, ma una soluzione completa è tutt’altro che banale. Eppure, è anche la chiave per affrontare senza traumi un buon numero di problemi di manutenzione ed estendibilità del software.

L’anno duemila si avvicina, e con esso la scadenza per rimediare ai numerosi ed insidiosi bug nascosti nei miliardi di linee di codice sparsi per il mondo. Codice talvolta poco importante, talvolta assolutamente critico per l’infrastruttura economica e sociale del pianeta. In Europa, un’altra grande corsa si è resa necessaria per adattare una quantità di sistemi per gestire correttamente le transazioni commerciali espresse in Euro.
In entrambi i casi, la conversione richiede spesso una attenta e minuziosa ricerca dei punti critici (che è solo in minima parte automatizzabile, con buona pace dei venditori di tool), una modifica mirata (talvolta banale, altre volte meno) e naturalmente un testing accurato. In altre parole, richiede parecchio tempo (che questa volta non è facilmente negoziabile) e di conseguenza parecchio denaro.
Come sempre, in questi casi le reazioni eccessive si sprecano: si va da chi, in USA, si prepara al crollo della civiltà trasferendosi in comunità agricole autosufficienti, a chi propone sanzioni per i programmatori colpevoli di aver creato il caos, a chi come al solito sostiene che "con il linguaggio XYZ non ci sarebbero mai stati problemi con l’anno 2000".
Come informatici, quest’ultima affermazione è di particolare interesse, perché tradisce una comprensione molto parziale del problema. Solo in una minoranza dei casi la scelta di memorizzare gli anni con solo due cifre (ad esempio) è stata una conseguenza del linguaggio utilizzato. In moltissimi casi è stata una decisione deliberata del programmatore, che ha scelto di utilizzare solo due cifre per risparmiare una risorsa preziosa, quella memoria centrale e di massa che qualche decennio fa aveva costi ben diversi dagli attuali. Se il linguaggio li avesse costretti ad utilizzare più di due byte per memorizzare l’anno, i programmatori avrebbero scelto l’unica strada percorribile: usare un’altra struttura per rappresentare le date. La programmazione è una attività umana e non il risultato magico di tool e linguaggi sventolati sopra un problema.
Questo non significa che il problema fosse inevitabile. Né significa che non vi siano analoghi errori potenziali "addormentati" nel software in circolazione, in attesa che l’ennesima variazione nel dominio del problema li porti ad emergere. Se c’é un lato positivo nel problema dell’anno 2000 è di aver dimostrato, in modo inequivocabile, l’impatto economico che può avere la mancanza di una corretta struttura del software.
Corretta struttura? Ma il problema dell’anno duemila non è dovuto a tutti quei piccoli dettagli in cui si annidano assunzioni sul numero di cifre? Certamente, ancora una volta il diavolo è nei dettagli. Eppure, come vedremo, si tratta ancora una volta di un errore nella struttura del software. E naturalmente, cercheremo anche di capire come progettare i nostri programmi in modo da poter affrontare analoghe sfide senza batter ciglio, sia che si tratti di manipolare date, denaro, o altre quantità. Vedremo anche che una soluzione completa è tutt’altro che banale, e che coinvolge elementi di programmazione ad oggetti e di programmazione generica.

Incapsulazione, ancora lei
Basta poco a convincersi che la reale "colpa" dei programmatori non è stata di memorizzare gli anni usando due cifre. Anzi, come accennavo poco sopra, questa è stata probabilmente una soluzione ragionevole ad un problema concreto (la mancanza di memoria).
Il vero errore è stato propagare le conseguenze di questa scelta in un numero imprecisato di punti all’interno di ogni sistema, costringendoci ora a ricercarli minuziosamente. In breve, quindi, il "problema" dell’anno duemila non sta nella decisione di usare due cifre, ma nella totale mancanza di incapsulazione di questa decisione.
Pensiamo a quanto sarebbe diverso il problema se tutte (e dico proprio tutte) le manipolazioni di date all’interno di un programma fossero state scritte in funzione di una interfaccia stabile, dove gli anni hanno un range assolutamente ampio (un intero a 32 bit, ad esempio). L’interfaccia potrebbe facilmente nascondere ogni dettaglio di validazione, memorizzazione e recupero dei dati.
Convertire un simile programma sarebbe uno scherzo, e con un minimo di impegno non dovremmo neppure modificare la struttura dei database sottostanti: esistono numerose tecniche per estendere il range effettivo (o per shiftarlo) pur conservando una memorizzazione in pochi byte (si veda ad es. [GR98] per una panoramica).
Il problema della conversione ad Euro non è molto diverso: in entrambi i casi i programmi manipolano delle quantità (di tempo, di denaro) ma in molti casi reali non prevedono un meccanismo per isolare il resto del sistema da eventuali variazioni nel formato, o nel significato, di tali quantità.
Non si tratta, ovviamente, di casi unici ed irripetibili. In moltissimi programmi le quantità vengono memorizzate come attributi semplici, il cui tipo è scelto in base a considerazioni "ragionevoli" ma tutt’altro che stabili. Se il nostro programma di magazzino deve memorizzare degli ordini, può sembrare (appunto) ragionevole utilizzare delle semplici strutture, come le seguenti:

struct Elemento
  {
  int codice ;
  int prezzoUnitario ;
  char descrizione[ MAX_DESCR ] ;
  // ...
  } ;

struct RigaOrdine
  {
  int codice ;
  int quantita ;
  // ...
  } ;

In fondo, gli interi sono efficienti (in termini di occupazione di memoria e di velocità) e non danno problemi di arrotondamento come i floating point. Se poi il programma deve gestire altre valute, che magari hanno delle frazioni, o se deve gestire quantità non intere (ad esempio un peso), possiamo pur sempre cercare di adattare il codice, giusto? Basta memorizzare la quantità moltiplicata per il numero di cifre decimali massime, e così per il peso. Già. E basta anche adottare una delle tante tecniche di date compression per risolvere il problema dell’anno duemila. Il reale problema, come sempre, è che se non prevediamo delle barriere esplicite per l’informazione, questa verrà usata in punti ed in modi inattesi. E che di conseguenza, ogni modifica avrà un impatto potenzialmente globale anziché locale.

Un modello semplice
L’idea di incapsulare la nozione di quantità (di tempo, di denaro, di prodotto, ecc) è di per sé di semplice concezione, ed a prima vista la sua realizzazione non si dimostra molto più complicata. Una Quantità è data da un certo ammontare (tipicamente uno scalare) espresso in una certa unità di misura (un concetto che dovremo analizzare meglio).
In principio, quindi, una classe Quantity potrebbe avere una struttura come la seguente (scritta in C++ ma facilmente comprensibile anche per chi proviene da altri linguaggi):

class Quantity
  {
  public :
    Quantity( int n ) ;
    Quantity operator+( const Quantity& q ) ;
    Quantity operator-( const Quantity& q ) ;
    Quantity operator*( int n ) ;
    Quantity operator/( int n ) ;
    int operator/( const Quantity& q ) ;
    bool operator ==( const Quantity& q ) ;
    bool operator <( const Quantity& q ) ;
    // eccetera: validazione, store/retrieve
    // in un byte array per supporto alla
    // persistenza, ...
  private :
    int amount ;
    Unit unit ;
  } ;

Tuttavia qui abbiamo almeno due problemi da affrontare: abbiamo imposto ad "ammontare" di essere un intero, ed abbiamo aperto la porta a confronti tra quantità espresse in unità di misura diverse. Il secondo problema, in particolare, è una variazione di uno già preso in esame in una precedente puntata di P&T: la classe Quantity ha dei metodi binari [Pes97] ed è quindi aperta a possibili problemi di type safety se gli oggetti di classe Quantity non sono tutti omogenei.
Nell’articolo citato abbiamo visto come la programmazione generica possa essere utilmente adottata per risolvere i problemi legati ai metodi binari; prima, però, dovremo necessariamente raffinare la nostra idea di unità di misura. In effetti, sommare (ad es.) due lunghezze espresse in unità diverse dovrebbe essere possibile (con le ovvie conversioni del caso), mentre ovviamente non dovremmo essere in grado di sommare una lunghezza ed un volume. Questo ci porta a distinguere tra entità misurata ed unità di misura utilizzata.

Entità ed unità
Il cosiddetto Sistema Internazionale [ISO93] definisce otto "quantità base" (lunghezza, tempo, ecc) e le corrispondenti "unità di misura internazionali" (metro, secondo, ecc). In palese contraddizione con lo standard, io chiamerò le "quantità fondamentali" entità nel resto dell’articolo, per evitare la classica confusione terminologica dovuta alla consuetudine di indicare con "quantità" l’ammontare di una certa "quantità base" (entità) in una certa unità di misura.
Di fatto, esiste almeno un’altra entità molto importante (il denaro) non compresa nelle otto "quantità base" del S.I., e probabilmente a ben cercare ne possono emergere altre ancora, utili al di fuori del dominio della fisica. Il nostro sistema, comunque sia, dovrà consentire una estensione delle entità rappresentabili.
Per ogni entità, esistono diverse unità di misura. Poiché rappresentano tutte la stessa entità, la conversione tra l’una e l’altra dovrà essere di tipo lineare, ovvero data una quantità Q1 misurata con l’unità U1 possiamo convertirla in una quantità Q2 equivalente nell’unità U2 con una conversione del tipo Q2 = (Q1 + A) * B, date due opportune costanti A e B. Nuovamente, il modo migliore per esprimere il tipo di A e B non è fissato: in alcuni casi basta un intero, in altri è necessaria una frazione, in altri casi ancora dovremo usare un numero in virgola mobile.
Le entità base (e le loro unità) possono naturalmente essere composte per esprimere concetti più complicati (ad esempio una accelerazione in m/s2 o un costo unitario espresso in Lire/KWh). Per brevità e semplicità di esposizione, in questo articolo non prenderò in considerazione i criteri che regolano rappresentazione e conversione di unità composte. Chi è interessato, può trovare una buona panoramica in [Nov95].
Infine, in alcuni settori può essere utile aggiungere ulteriori informazioni ad una quantità: ad esempio, in una grande applicazione per il trattamento di dati fisiologici mi è risultato molto comodo caratterizzare (ad esempio) una Pressione come proveniente da una manometria esofagea piuttosto che come una Pressione diastolica. Nuovamente, si tratta di argomenti di confine che non prenderò ulteriormente in considerazione.

Verso un modello migliore
Da quanto sopra possiamo dedurre che la nostra classe Quantity deve sicuramente essere parametrizzata sul tipo da utilizzare per il campo amount: questo ci consente di utilizzare il tipo di dato più efficiente per rappresentare le quantità (con l’ovvia possibilità di cambiare idea in futuro), eventualmente anche in modo diverso all’interno dello stesso programma.
In teoria, Quantity dovrebbe essere parametrizzata anche sull’entità rappresentata: questo ci permetterebbe di distinguere tra entità diverse, consentendo però di confrontare quantità espresse in unità di misura diverse ma comunque relative alla stessa entità (es. una pressione in Kg/m2 ed una in Pascal, o un costo in Dollari ed uno in Euro). Come vedremo tra breve, esiste una strada alternativa che si presta ad una realizzazione più efficiente in termini computazionali.
Ovviamente, a questo punto sembra anche logico richiedere che sia lecito sommare una quantità di Pressione espressa in Kg/m2 e rappresentata con un intero ed una espressa in Pascal e rappresentata con un double, seguendo le normali regole di conversione tra i tipi.
I requisiti si stanno un po’ complicando, quindi non fa male riassumerli; il nostro modello di Quantità deve:

L’ultimo punto è particolarmente importante, perché significa poter scrivere un programma che opera con ogni valuta (dato che tutte le valute corrispondono all’entità Denaro) o su una qualunque unità utilizzata per il Peso, anche quelle non previste al momento della stesura del programma.
Ottenere qualcosa di funzionante che rispetti quanto sopra non è affatto banale. Tuttavia quanto richiediamo è pressoché indispensabile affinché l’utilizzatore della classe Quantity non cambi rapidamente idea e ricominci ad utilizzare i tipi base, "che saranno anche brutti, ma almeno si convertono tra loro senza problemi".
Di fatto, l’unico linguaggio che conosco in cui si può implementare (con il dovuto grado di sicurezza e di flessibilità) una libreria come quella che stiamo iniziando a delineare è il C++. Su questo argomento tornerò più avanti, riprendendo anche alcune riserve che ho spesso espresso sullo stile di programmazione generica comunemente adottato in C++, e facendo osservare come in questo caso specifico si possano prevenire eventuali problemi con uno sforzo addizionale minuscolo. In altri ambienti ragionevolmente diffusi (es. Ada, Java, Smalltalk), possiamo solo arrivare a modelli analoghi, che mancano però di flessibilità o che per contro richiedono controlli di consistenza a run-time.

Miglioriamo il modello
Riprendiamo l’idea di parametrizzare Quantity sull’entità misurata. Questo non ci esime dallo specificare, all’interno di ogni oggetto Quantity, l’unità di misura adottata (visto che ad ogni entità corrispondono più unità di misura). Se l’unità non è un parametro, allora deve essere un data member, che consuma spazio in RAM (a differenza del parametro di un template, che è codificato nel tipo). Inoltre, l’eventuale conversione di unità (necessaria ad esempio per sommare metri e pollici) dovrà avvenire attraverso un controllo a run-time sul valore di tale data member, perdendo anche in efficienza. Infine (ma si tratta di un problema risolvibile anche a compile-time) poiché specificando l’unità di misura specifichiamo anche l’entità, la specifica dell’entità diventa ridondante e dobbiamo garantire la consistenza del dato e del parametro.
Cosa succede se rinunciamo a parametrizzare sull’entità, e parametrizziamo invece sull’unità di misura? In teoria, perdiamo la possibilità di operare in modo polimorfo su oggetti aventi stessa entità ma unità di misura diversa, dato che ogni istanza di un template è un tipo separato. In realtà, come vedremo più avanti, possiamo recuperare il polimorfismo ad oggetti introducendo un’opportuna classe base.
Una prima versione (parziale) di Quantity potrebbe quindi essere la seguente:

template< class U, class A > class Quantity : 
  {
  public :
    Quantity( A a ) : amount( a )
      {
      }
    template< class U2, class A2 > 
    Quantity< ??, ?? > 
    operator+( const Quantity< U2, A2 >& q ) const
      {
      ?? a1 = Scale??( amount ) ;
      ?? a2 = Scale??( q.GetAmount() ) ;
      return( a1 + a2 ) ; 
      }
    A GetAmount() const 
      { 
      return( amount ) ; 
      }
    // ... ecc
  private :
    A amount ;
  } ;

I due parametri del template sono l’unità di misura ed il tipo da utilizzare per memorizzare l’ammontare. Il costruttore è banale, così come GetAmount. L’operatore di somma è già tutt’altro che banale, e come vedete ho lasciato diversi punti in sospeso (i vari ?? nel listato).
Innanzitutto dobbiamo subito osservare che l’operatore di somma è a sua volta un template (un member template), con i suoi parametri specifici. Questo è pressoché indispensabile se vogliamo sommare, in modo efficiente, una lunghezza espressa in metri e memorizzata in un double con una espressa in pollici e memorizzata in un int. Osserviamo che in teoria serve anche un controllo di consistenza sull’entità, per ora mancante.
Iniziamo quindi a risolvere i vari problemi: le variabili a1 ed a2 dovrebbero avere il tipo "a maggiore precisione" tra A ed A2. Questo può sembrare un ostacolo difficile da superare, perché apparentemente il linguaggio non ci consente di esprimere questo tipo di relazione tra i tipi. In realtà esiste una tecnica molto semplice, che utilizza classi template dette traits [Mye95] il cui unico scopo è fornire informazioni sui parametri (o sulle relazioni tra i parametri). Vediamo come implementare un trait Promote che ci fornisce il tipo a precisione maggiore:

// clausola generale, 
// tipo sconosciuto
template< class T1, class T2 > struct Promote
  {
  } ;

// specializzazione parziale // per identità template< class T > struct Promote< T, T > { typedef T Promoted ; } ;

// casi espliciti per // situazioni "miste" template<> struct Promote< int, double > { typedef double Promoted ; } ; template<> struct Promote< double, int > { typedef double Promoted ; } ; // ... ecc

In generale, per conoscere il tipo promosso tra T1 e T2 scriveremo Promote< T1, T2 >::Promoted. Se T1 e T2 coincidono, verrà scelta la specializzazione parziale per identità (che ci evita la faticaccia, in sviluppo e manutenzione, di definire una clausola per <int, int>, una per <double, double> e così via). Altrimenti, se T1 e T2 appaiono in una delle clausole esplicite, verrà ovviamente scelta la clausola corrispondente. Altrimenti verrà scelta la clausola generale, dove Promoted non appare neppure: errore di compilazione, come necessario.
Purtroppo su molti compilatori questo codice non funziona, e bisogna definire un Promoted non numerico (es. void) nella clausola generale. Sul Visual C++ 5, la specializzazione parziale non funziona a dovere e dobbiamo pertanto esplicitare tutte le identità. Fortunatamente le varie clausole si scrivono una volta per tutte, salvo voler considerare nuovi tipi, nel qual caso dobbiamo estendere il sistema, aggiungendo nuove clausole (senza toccare le vecchie).
Un problema analogo va risolto per la funzione Scale??: in generale, questa dovrà prendere un ammontare con tipo A nell’unità U e trasformarlo in un ammontare di tipo promosso nell’unità... promossa a sua volta! In effetti, così come promuoviamo il tipo dell’ammontare, dobbiamo promuovere le unità di misura. Dobbiamo quindi aggiungere clausole del tipo:

template<> struct Promote< Meter, Inch >
  {
  typedef Meter Promoted ;
  } ;

template<> struct Promote< Inch, Meter >
  {
  typedef Meter Promoted ;
  } ;
Dove Meter ed Inch sono ovviamente delle classi (che non abbiamo ancora visto in dettaglio). Nuovamente, il sistema è ragionevolmente estendibile: introdurre una nuova unità di misura significa definire il tipo Promoted tra essa e le altre misure relative alla stessa entità. Notiamo che avremmo potuto prendere una scorciatoia, e definire un’unica unità "di riferimento" per ogni entità: ad esempio, avremmo potuto risparmiare parecchie clausole di specializzazione utilizzando sempre Meter come unità di riferimento per le lunghezze. D’altra parte, se sommiamo Inch e Foot probabilmente vogliamo Foot, non Meter, come unità risultante. La soluzione qui delineata è più prolissa (ma si scrive una sola volta e si estende ragionevolmente di rado) ma sicuramente più potente.
Possiamo ora scrivere una versione completa dell’operatore di somma:

template< class U2, class A2 > 
Quantity< Promote< U, U2 >::Promoted, 
          Promote< A, A2 >::Promoted > 
operator+( const Quantity< U2, A2 >& q ) const
  {
  typedef Promote< U, U2 >::Promoted PromotedUnit ;
  typedef Promote< A, A2 >::Promoted PromotedAmount ;
  PromotedAmount a1 = 
    Scale< PromotedAmount, PromotedUnit, U >( amount ) ;
  PromotedAmount a2 = 
    Scale< PromotedAmount, PromotedUnit, U2 >( q.GetAmount() ) ;
  return( Quantity< PromotedUnit, 
                    PromotedAmount >( a1 + a2 ) ) ; 
  }
La funzione template Scale ha un compito non semplice, ovvero la trasformazione di unità di misura e contemporaneamente anche di precisione. Una possibile implementazione è la seguente:

template< class A, class U1, class U2 > A Scale( A a )
  {
  return( ( a + Scaling< U1, U2 >::ofs ) * 
           Scaling< U1, U2 >::gain ) ;
  }

Dove Scaling è nuovamente un template che utilizza la tecnica dei traits per definire le costanti di scalatura:

// clausola generale
template< class U1, class U2 > struct Scaling
  {
  } ;

// identità
template< class U > struct Scaling< U, U >
  {
  static const int ofs = 0 ;
  static const int gain = 1 ;
  } ;

// singoli casi
template<> struct Scaling< Meter, Inch >
  {
  static const int ofs = 0 ;
  static const double gain = 0.0254 ;
  } ;

// ecc

Notiamo che questa non è l’implementazione più efficiente: nel caso di identità, Scale deve comunque sommare zero e moltiplicare per uno. Usando la specializzazione parziale è possibile scrivere una funzione Scale più intelligente: nella migliore tradizione, questo è lasciato come esercizio per i lettori interessati (suggerimento: probabilmente Scale dovrà diventare una struttura...).
Con la stessa tecnica, dobbiamo poi definire le altre operazioni, nonché un operatore di conversione template a Quantity< U2, A2 > per U2, A2 "compatibili". Il che ci ricorda che non abbiamo affatto preso in considerazione la compatibilità tra unità di misura diverse, né definito con precisione cosa sia un’entità e cosa sia un’unità di misura. Dobbiamo anche apportare qualche modifica alla soluzione abbozzata, altrimenti possiamo dire addio al polimorfismo ad oggetti ed ai programmi estendibili.

Completiamo il modello
Se vogliamo distinguere tra le unità relative ad entità diverse, dobbiamo introdurre un concetto di entità sinora assente dal modello. Le varie unità di misura potrebbero poi essere istanze di un template Unit, avente un parametro che specifica l’entità cui appartiene. Vediamo una possibile implementazione:

class Entity
  {
  public :
    typedef void IsEntity ;
  } ;

class Length : public Entity
  {
  } ;

class Power : public Entity
  {
  } ;

template< class E > class Unit
  {
  public :
    typedef E Entity ;
  private :
    typedef typename 
    E::IsEntity CheckEntity ;
  } ;

class Meter : public Unit< Length >
  {
  } ;

class Inch : public Unit< Length >
  {
  } ;

class Joule : public Unit< Power >
  {
  } ;

Il listato è banale, salvo per un paio di considerazioni. La prima è che la classe template Unit utilizza un typedef pubblico per rendere disponibile la propria entità. Ovvero, se U è una [classe derivata da] Unit, allora U::Entity è l’entità relativa. Questo è un idioma molto utile quando si utilizza la programmazione generica, e ci tornerà utile più avanti.
La seconda considerazione riguarda il typedef IsEntity all’interno della classe base Entity, ed il corrispondente typedef privato CheckEntity all’interno del template Unit. Lo scopo è di ottenere una restrizione sul tipo di parametri utilizzabili: in altre parole, vogliamo che Unit venga istanziato usando delle entità, non tipi qualunque come in Unit< int >. Questo va contro lo stile "implicito" comunemente adottato in C++ dai "generic programmers", ma come ho già fatto notare nel succitato [Pes97], gli elementi impliciti sono piccole bombe in attesa di esplodere.
Una restrizione analoga va operata sui parametri del template Quantity. In realtà, una prima restrizione sul parametro U (o U2) è già presente, poiché richiediamo che esista una versione opportuna del traits Promote. Vedremo che completando il modello si introducono nuove restrizioni su U. Analogamente, il parametro A viene in qualche modo ristretto dall’uso di Promote (oltre, naturalmente, dalla richiesta implicita degli operatori +, -, * ecc).
Volendo rendere espliciti anche in questo caso i vincoli sul parametro, possiamo usare una tecnica analoga a quanto sopra per U: basta definire un typedef IsUnit nel template Unit. Notiamo che un typedef non occupa spazio a run-time e che il controllo avviene a compile-time. Purtroppo, non possiamo usare la stessa tecnica per il parametro A, in quanto non possiamo modificare i tipi base (ed A può essere ad es. int o double). In questo caso, interviene di nuovo a salvarci la tecnica dei traits: possiamo definire un trait ValidAmountType e fornire delle specializzazioni per int, double, e per ogni altro tipo che vogliamo adottare. Nuovamente, la tecnica è semplice ed estendibile, ed evita sorprese dovute ai dettagli impliciti.

Polimorfismo ad oggetti
Arriviamo ora all’ultimo punto: il polimorfismo ad oggetti su Quantity differenti. Indubbiamente, poter prendere (ad esempio) una lista di Quantity con entità = Money e unità (ovvero valuta) qualunque e trattarla in modo polimorfo può essere di grande utilità. Un programma scritto in tal modo accetta una nuova valuta (come l’Euro) senza colpo ferire. D’altra parte, come accennavo poco sopra, la soluzione sinora delineata non si presta al polimorfismo ad oggetti: ogni istanza di un template è un tipo separato.
Possiamo però rimediare apportando alcune modifiche a Quantity, ovvero rendendolo parte di una gerarchia di ereditarietà come la seguente:

template< class E > class QE
  {
  public :
    typedef E Entity ;
  } ;

template< class E, class A > class QEA : public QE< E >
  {
  public :
    typedef A BaseType ;
    QEA( A a ) : amount( a )
      {
      }
    A GetAmount() const 
      { 
      return( amount ) ; 
      }
  protected :
    A amount ;
  } ;

template< class U, class A = int > class Quantity : 
public QEA< typename U::Entity, A >
  {
  public :
    typedef U Unit ;
    // .. circa come prima...
  } ;

I nomi sono volutamente brevi e poco espressivi: vi invito a pensare a fondo ai nomi migliori da adottare al posto di QE e QEA. In ogni caso, QE rappresenta la base di tutte le Quantità relative ad una certa entità. QEA aggiunge il concetto di tipo utilizzato per l’ammontare. Quantity aggiunge il concetto di unità di misura ed una serie di operazioni. Notiamo che la dichiarazione di Quantity "estrae" l’entità dal parametro U usando il typedef Entity descritto al paragrafo precedente.
La nuova gerarchia ci permette peraltro di riconsiderare la posizione di alcune member function: ad esempio, GetAmount può essere correttamente posizionata a livello di QEA. Funzioni come Validate potrebbero finire nuovamente in QEA (dipende da cosa intendiamo usare come parametro, se il solo valore numerico o una stringa più complessa come 5", che richiede un posizionamento in Quantity). Funzioni di supporto alla persistenza potrebbero essere definite come virtuali pure in QE (per sfruttare al meglio il polimorfismo) ed essere implementate in QEA, con la possibilità di essere ulteriormente ridefinite in altre classi derivate.
Una volta modificata la gerarchia come sopra, è facile scrivere una funzione che prende un generico reference ad una QEA< Length, double > ed usa in qualche modo il parametro, senza sapere la reale unità di misura.
In principio, non manca neppure la possibilità di usare in modo polimorfo quantità di lunghezza con tipi di ammontare diverso: basta usare QE< Length > come parametro. Di fatto, ricordiamo sempre che il polimorfismo ad oggetti richiede che le funzioni siano presenti nell’interfaccia della classe base. Se vogliamo (ad esempio) sommare tra loro tutti gli importi (entità Money) espressi in unità di misura diverse e con tipi di ammontare diversi, non possiamo farlo direttamente: QE< Money > non ha una funzione GetAmount, in quanto il tipo del risultato sarebbe diverso nelle varie classi derivate. In questi casi (come avviene quando manipoliamo tipi base diversi) dobbiamo costruire una classe adapter, oppure convertire tutti gli importi in (esempio) QEA< Money, double > e poi trattarli in modo polimorfo sulla sola unità di misura.
Va detto che, poiché il C++ permette al tipo del risultato di variare in modo covariante nelle classi derivate, se rinunciassimo all’uso dei tipi base per l’ammontare, ed utilizzassimo invece una nostra gerarchia di ereditarietà, potremmo superare anche questo problema. La soluzione, a mio avviso, risulterebbe però troppo macchinosa rispetto a quella che abbiamo sinora discusso.
Infine, vi ricordo che il polimorfismo ad oggetti è anche la chiave per risolvere alcuni problemi di manutenzione: se abbiamo scritto il nostro codice in termini di una classe Date (basata a questo punto su una istanza di Quantity) non ci vuole poi molto a derivare una classe WindowedDate, che usa una tecnica di windowing per shiftare il range delle date, o una classe CompressedDate che usa lo stesso spazio su disco per rappresentare un range maggiore. L’importante, come sempre, è gestire in modo opportuno le dipendenze di creazione (chi ha seguito i miei corsi o worklab sul Systematic OOD sa bene di cosa parlo) perché se il nostro codice continua a creare oggetti di classe Date, estendere la gerarchia serve a poco e dobbiamo mettere le mani nel sorgente originale. Operazione, comunque, ben più semplice che non cercare accuratamente i punti da modificare sparsi per i listati...

Considerazioni finali
Passare dalla semplice idea di "incapsulare le quantità" ad una versione funzionante non è proprio banale. Questo ci può portare a molte considerazioni.
La prima è che la complessità del mondo reale è grande, e non possiamo pretendere che soluzioni complete a problemi reali siano semplici. Il punto focale è dove viene posizionata la complessità: in questo caso, l’implementazione di Quantity è piuttosto complessa, ma il suo utilizzo è semplice e naturale, pur rimanendo flessibile. Quantity è flessibile come i tipi base (anzi di più), ma molto più potente. In realtà, buona parte della complessità interna deriva proprio dal desiderio di fornire una semplicità esterna notevole, unita a buone prestazioni ed alla sicurezza sui tipi.
Una seconda considerazione riguarda la complessità del linguaggio. Spesso il C++ viene criticato per la sua grande complessità: sicuramente si tratta di uno dei linguaggi più complessi in circolazione. D’altra parte, l’uso corretto del C++ sta proprio nello sfruttare a fondo i meccanismi sofisticati per implementare astrazioni semplici da usare. Se il linguaggio è troppo semplice, il rischio è di dover esporre la complessità intrinseca del modello all’utilizzatore della libreria, perché non siamo in grado di nasconderla.
Una terza considerazione riguarda il salto concettuale tra analisi e design. In un ottimo libro di Analysis Patterns [Fow97], viene discusso un pattern Quantity che si propone, per l’appunto, di incapsulare le quantità. A livello di analisi, tutto si risolve in un paio di pagine, con un semplice diagramma UML dove Quantity ha gli operatori +, -, ecc. Passare al design significa pensare all’estendibilità, alla riusabilità, alla sicurezza sui tipi, ecc. Non è un caso che la fase di design sia così critica in ogni progetto. Un progetto senza progettisti è un progetto nei guai. Una azienda di software senza un architetto del software è (nel migliore dei casi) un’azienda che marcia a regime ridotto.

Reader's Map
Molti visitatori che hanno letto
questo articolo hanno letto anche:

Bibliografia
[Fow97] MartinFowler, "Analysis Patterns", Addison-Wesley, 1997.
[GR98] William Gothard, Les Rodner, "Strategies for Solving the Y2K Problem", Dr. Dobb’s Journal, May 1998.
[ISO93] "Quantities and Units", ISO Standards Handbook, International Organization for Standardization, 1993.
[Mye95] Nathan Myers, "Traits: a new and useful template technique", C++ Report, June 1995.
[Nov95] G. S. Novak Jr., "Conversion of Units of Measurement", IEEE Transactions on Software Engineering, August 1995.
[Pes97] Carlo Pescio, "Programmazione ad Oggetti e Programmazione Generica", Computer Programming No. 62, Ottobre 1997.

Biografia
Carlo Pescio (pescio@eptacom.net) svolge attività di consulenza in ambito internazionale nel campo delle tecnologie Object Oriented. Ha svolto la funzione di Software Architect in grandi progetti per importanti aziende europee e statunitensi. È incaricato della valutazione dei progetti dal Direttorato Generale della Comunità Europea come Esperto nei settori di Telematica e Biomedicina. È laureato in Scienze dell'Informazione ed è membro dell'ACM, dell'IEEE e della New York Academy of Sciences.