Introduzione al Polimorfismo
“One day Trurl the constructor put together a machine that could create anything starting with n.” — Stanislaw Lem, The Cyberiad
Il Polimorfismo (dal greco “molte forme”) consiste nello scrivere codice una volta sola e poterlo riutilizzare con tipi di dato diversi. Questo permette di creare programmi altamente modulari (loosely coupled), evitando copiature ripetitive di codice e facilitando la manutenzione.
In C++ esistono due macro-categorie:
- Polimorfismo a Compile-Time (Tempo di Compilazione): tramite i Template, dove il compilatore conosce a priori tutti i tipi in gioco (ne parleremo nel Capitolo 6).
- Polimorfismo a Runtime (Tempo di Esecuzione): tramite Ereditarietà e Interfacce, dove il tipo esatto dell’oggetto viene determinato dinamicamente mentre il programma sta girando.
Un Esempio Motivazionale
Section titled “Un Esempio Motivazionale”Immagina di programmare il sistema centrale di una Banca. La classe `Bank` effettua trasferimenti di denaro, ma deve obbligatoriamente tenere traccia di tutte le transazioni tramite un Logger (registratore).
Inizialmente, creiamo un semplice `ConsoleLogger`:
struct ConsoleLogger { void log_transfer(long from, long to, double amount) { printf("[cons] %ld -> %ld: %f\\n", from, to, amount); }};Il problema emerge quando il nostro manager ci chiede di supportare anche un `FileLogger`, un `NetworkLogger` e così via, e di poterli scambiare mentre il programma gira (ad esempio se la rete cade, passiamo al file system).
Una prima, debole soluzione potrebbe essere l’uso di un `enum` interno alla Banca e di un enorme costrutto `switch` per decidere chi chiamare:
Qual è il problema di questo approccio?
Section titled “Qual è il problema di questo approccio?”Sebbene funzioni, presenta disastrosi problemi di design:
- Devi scrivere una nuova classe logger.
- Devi aggiungere un nuovo valore all’`enum`.
- Devi aggiungere un nuovo `case` allo `switch`.
- Devi istanziare tutti i possibili logger come variabili membro dentro `Bank`!
È un sacco di lavoro per una modifica banale. Quello che vorremmo veramente è che `Bank` possieda un singolo puntatore generico a un Logger, e che a prescindere da quale logger effettivo stiamo usando, chiami la funzione `log_transfer`. La Banca non deve sapere come il logger funziona, le basta sapere come chiamarlo.
Questo è il concetto di Interfaccia (Interface).