Università degli Studi Mediterranea di Reggio Calabria Dipartimento di Ingegneria dell’Informazione, delle Infrastrutture e dell’Energia Sostenibile Corso di Laurea in Ingegneria dell’Informazione Tesi di Laurea Riuscirà Java a non farsi “rottamare” da Go? Relatore Candidato Prof. Domenico Ursino Francesco Surace Anno Accademico 2015-2016 Indice Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Caratteristiche generali di Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.1 Storia del linguaggio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Caratteristiche principali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3 La programmazione orientata ad oggetti . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.1 Classi, oggetti e metodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.2 Incapsulamento, ereditarietà e polimorfismo . . . . . . . . . . . . . . . 1.4 La piattaforma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.4.1 Java Virtual Machine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.4.2 La documentazione API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 8 9 10 11 12 12 15 Caratteristiche generali di Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1 Storia del linguaggio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Caratteristiche principali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.1 La compilazione e le librerie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.2 Go è un linguaggio OOP? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3 La sintassi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.1 Case sensitive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.2 Le variabili . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.3 I punti e virgola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.4 Strutture di controllo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 17 19 20 20 23 23 24 25 25 Java vs Go: gestione della concorrenza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1 Presentazione del case study . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.1 Descrizione del contesto di riferimento . . . . . . . . . . . . . . . . . . . . 3.1.2 Diagramma dei casi d’uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.3 Progettazione concettuale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.4 Progettazione della componente applicativa . . . . . . . . . . . . . . . . 3.2 La programmazione concorrente in Java . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.1 Introduzione ai thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.2 Implementazione del case study . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3 La programmazione concorrente in Go . . . . . . . . . . . . . . . . . . . . . . . . . . 27 27 27 28 29 31 36 36 39 43 IV Indice 3.3.1 Goroutine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.2 Channel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.3 Implementazione del codice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4 Conclusioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 45 46 49 Java vs Go: la gestione delle funzioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1 Il concetto di sottoprogramma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Gestione delle funzioni in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2.1 I metodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2.2 Polimorfismo per metodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3 Gestione delle funzioni in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3.1 Le funzioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3.2 Le funzioni anonime . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3.3 Il return multiplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4 Conclusioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 51 51 52 54 58 58 60 60 62 Java Vs Go: la gestione delle interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1 La gestione delle interfacce in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.1 Le interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.2 L’ereditarietà multipla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.3 Classi astratte ed interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2 La gestione delle interfacce in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2.1 Le interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2.2 Strutture, puntatori e interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . 5.3 Conclusioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 63 63 67 71 72 72 74 76 Java vs Go: Riuso del software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.1 Il concetto di riuso di software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2 Riuso del software in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2.1 Ereditarietà . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2.2 Valutazioni sul riuso del software . . . . . . . . . . . . . . . . . . . . . . . . . 6.3 Riuso del software in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3.1 Embedding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3.2 Valutazioni sul riuso del software . . . . . . . . . . . . . . . . . . . . . . . . . 6.4 Conclusioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 79 80 80 85 86 86 89 90 Conclusioni e uno sguardo sul futuro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 Ringraziamenti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 Riferimenti bibliografici . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 Elenco delle figure 1.1 1.2 1.3 1.4 1.5 1.6 Logo di Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Risultato del Listato 1.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funzionamento della Java Virtual Machine . . . . . . . . . . . . . . . . . . . . . . . Schematizzazione della Java Virtual Machine . . . . . . . . . . . . . . . . . . . . . Documentazione API: lista dei package . . . . . . . . . . . . . . . . . . . . . . . . . . . Documentazione API: contenuto dei package . . . . . . . . . . . . . . . . . . . . . . 8 11 13 14 15 16 2.1 2.2 2.3 2.4 Interesse per Golang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Interesse per Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Risultato del Listato 2.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Risultato del Listato 2.3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 19 22 22 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 3.12 Specifiche tecniche del calcolatore utilizzato per l’analisi . . . . . . . . . . . . Diagramma dei casi d’uso per il nostro test . . . . . . . . . . . . . . . . . . . . . . . Il modello Entità-Relazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dizionario delle Entità . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dizionario delle Relazioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Diagramma delle classi relativo al test di nostro interesse . . . . . . . . . . . Deposito: Diagramma di sequenza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ritiro: Diagramma di sequenza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bonifico: Diagramma di sequenza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Esecuzione del programma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Risultato del Listato 3.12 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Risultato del Listato 3.17. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 29 30 31 31 32 33 34 35 43 45 48 4.1 4.2 4.3 Risultato ottenuto dal Listato 4.4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 Polimorfismo di tipo overload per i metodi println() . . . . . . . . . . . . . 56 Risultato del metodo in override . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 5.1 5.2 Risultato ottenuto dall’esecuzione del programma del Listato 5.8 . . . . 67 Risultato ottenuto dall’esecuzione del programma del Listato 5.18 . . . 71 6.1 6.2 Esempio di gerarchizzazione errata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 Esempio di gerarchizzazione corretta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 VI Elenco delle figure 6.3 6.4 Risultato del programma riportato nel Listato 6.7 . . . . . . . . . . . . . . . . . 88 Risultato del programma riportato nel Listato 6.8 . . . . . . . . . . . . . . . . . 89 Elenco dei listati 1.1 1.2 1.3 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 2.10 2.11 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 Creazione di una classe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Creazione di un oggetto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Esempio di importazione di una libreria . . . . . . . . . . . . . . . . . . . . . . . . . . Esempio di una struct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La funzione new . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Istanziazione consueta di una struct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Struct e func . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Esempio di case sensitive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Operatore di assegnazione diretta in Go . . . . . . . . . . . . . . . . . . . . . . . . . . Variabile non inizializzata in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Assegnazione esplicita di un tipo ad una variabile in Go . . . . . . . . . . . . Variabile non inizializzata in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Istruzione if-else in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ciclo for in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Esempio di thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Metodo start() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Metodi per la gestione del flusso di un thread . . . . . . . . . . . . . . . . . . . . . Il metodo synchronized . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Esempio di utilizzo dell’interfaccia Runnable . . . . . . . . . . . . . . . . . . . . . . La classe ReadRun per la lettura del saldo . . . . . . . . . . . . . . . . . . . . . . . . La classe DepositRun per la gestione del deposito . . . . . . . . . . . . . . . . . La classe WithdrawRun per la gestione dei prelievi . . . . . . . . . . . . . . . . . La classe TransferRun per la gestione dei bonifici . . . . . . . . . . . . . . . . . La classe CurrentAccount . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . La classe Main() per l’avvio di tutte le attività . . . . . . . . . . . . . . . . . . . . Esempio di goroutine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Esempio di canale sincrono . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Esempio di canale asincrono . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gestore di un canale sincrono . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Direzione del canale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implementazione del codice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Algoritmo per il calcolo del tempo di esecuzione in Java . . . . . . . . . . . . Algoritmo per il calcolo del tempo di esecuzione in Go . . . . . . . . . . . . . 10 10 16 21 21 21 23 23 24 24 24 24 25 25 36 37 37 38 39 39 40 40 40 41 41 44 46 46 46 46 47 49 49 VIII Elenco dei listati 4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 4.10 4.11 4.12 4.13 4.14 4.15 4.16 4.17 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 5.9 5.10 5.11 5.12 5.13 5.14 5.15 5.16 5.17 5.18 5.19 5.20 5.21 5.22 5.23 5.24 5.25 5.26 5.27 6.1 6.2 6.3 Classe CurrentAccount . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Definizione del metodo Withdraw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implementazione del metodo Withdraw . . . . . . . . . . . . . . . . . . . . . . . . . . . Invocazione del metodo Withdraw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . CurrentAccount senza polimorfismo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Esempio di polimorfismo di tipo overload . . . . . . . . . . . . . . . . . . . . . . . . . Classe InterestBearginAccount . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Metodi ereditati dalla superclasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Override dei metodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Override: main() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Definizione e implementazione di una func . . . . . . . . . . . . . . . . . . . . . . . . Modalità con cui si può richiamare una funzione in Go . . . . . . . . . . . . . Struct “collegata” a una func . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Valori e puntatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Variabile e funzione anonima . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Massimo e minimo in Go, return multipli . . . . . . . . . . . . . . . . . . . . . . . . . Esempio di massimo e minimo in Java, return singolo . . . . . . . . . . . . . . Esempio di metodo astratto in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dichiarazione di interfaccia in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implementazione nella classe Company . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implementazione nella classe Private . . . . . . . . . . . . . . . . . . . . . . . . . . . . Interfaccia Sortable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Classe CurrentAccount . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Classe SortingAlgorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Il main() del programma che si occupa dell’ordinamento . . . . . . . . . . . Interfaccia che estende altre interfacce in Java . . . . . . . . . . . . . . . . . . . . . Interfaccia CanRead . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Interfaccia CanDeposit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Interfaccia CanWithdraw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Interfaccia CanTransfer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Interfaccia CanInterest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Classe CurrentAccount che implementa le interfacce . . . . . . . . . . . . . . . Classe InterestBearginAccount che estende CurrentAccount . . . . . . Classe SubordinatedBond che implementa le interfacce . . . . . . . . . . . . . Metodo main() che implementa le interfacce . . . . . . . . . . . . . . . . . . . . . . Esempio di classe astratta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Esempio di Interfaccia in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implementazione di una interfaccia in Go . . . . . . . . . . . . . . . . . . . . . . . . . Istruzione equivalente in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Istruzione equivalente in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Interfaccia vuota . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Interfaccia vuota e puntatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Implementazione di una interfaccia in Go . . . . . . . . . . . . . . . . . . . . . . . . . type e interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Superclasse Customer in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sottoclasse Private . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sottoclasse Company . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 52 53 53 54 55 56 56 57 58 58 59 59 60 60 61 61 63 64 64 64 65 65 66 66 67 68 68 68 68 68 68 69 70 70 71 72 73 73 73 73 74 75 75 82 82 83 Elenco dei listati 6.4 6.5 6.6 6.7 6.8 6.9 Metodi della classe Object in override . . . . . . . . . . . . . . . . . . . . . . . . . . . Esempio di embedding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Instaziazione di struct embedding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dati principali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Embedding e interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Composizione in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 84 86 87 87 88 90 Introduzione Le ragioni principali che hanno spinto l’umanità al progresso tecnologico sono state la facilitazione e l’alleggerimento del lavoro in modo da poter aumentare l’efficienza produttiva. L’invenzione del calcolatore non si sottrae a tale principio; pensato in origine per semplificare l’esecuzione di complicate operazione numeriche, è ormai in grado di rispondere, se opportunamente orientato, alla maggior parte di problemi che si possono porre davanti attraverso l’uso di un linguaggio di programmazione. Lady Ada Lovelace realizzò, nel lontano 1837, quello che può essere definito come il primo linguaggio di programmazione; sviluppato per la macchina analitica di Charles Babbage, il linguaggio di Lovelace introdusse i concetti di ciclo ripetuto e di variabile indicizzata. Tuttavia, tale linguaggio non mostrò le sue innovazioni in ambito tecnologico poichè la macchina analitica su cui doveva “girare” non venne mai portata a termine. A rigore, i primi linguaggi di programmazione presero vita più di un secolo dopo; essi erano scritti in short code, da cui prese ispirazione l’odierno assembly. La sola struttura di controllo implementata nei primi linguaggi è il salto condizionato. La maggior parte dei linguaggi di programmazione successivi cercarono di “astrarsi” dal linguaggio di macchina, ottenendo le potenzialità di rappresentare strutture di controllo e di dati più generali in modo da avvicinarsi a un linguaggio a più alto livello di “astrazione”, e quindi al linguaggio “umano”. Nasce, allora, l’esigenza di definire un insieme di procedure in modo da veicolare la percezione che ha il programmatore nella stesura del linguaggio; nasce, cioè, l’esigenza di stabilire un paradigma di programmazione. Ogni linguaggio di programmazione è riconducibile ad almeno un particolare paradigma di programmazione; alcuni tra i principali paradigmi sono: • • • il paradigma della programmazione modulare: si prefigge lo sviluppo di programmi raggrupati in moduli, tali per cui ogni modulo abbia una sua precisa funzione; il paradigma della programmazione funzionale: il flusso d’esecuzione è costituito da una serie di funzioni che ricordano molto quelle matematiche; il paradigma della programmazione procedurale: impone la creazione di blocchi di codice individuati da un identificativo e definiti attraverso dei deliminatori; tali blocchi di codice sono anche conosciuti come sottoprogrammi; 4 Introduzione • il paradigma della programmazione strutturata: nato nell’ambito della programmazione procedurale, propone strutture di dati e di controllo invocabili non solo con i salti incodizionati; • il paradigma della programmazione orientata agli oggetti (Object Oriented Programming - OOP): migliora il paradigma della programmazione strutturata attraverso la definizione di oggetti in grado di interagire fra loro. In particolare, consideriamo il paradigma della programmazione orientata agli oggetti; esso prevede di raggruppare le funzionalità e gli attributi che interessano una particolare porzione di codice in un modello “astratto” chiamato classe. All’interno di una classe possiamo instanziare gli oggetti, descritti mediante gli attributi e le funzionalità presenti nella classe stessa. Il concetto di classe deriva dal concetto di tipo di dato astratto, tipico del paradigma della programmazione procedurale. Il primo linguaggio a supportare il paradigma della programmazione orientata agli oggetti è stato il Simula, sviluppato da Ole-Johan Dahl e Kristen Nygaard tra il 1960 e il 1967; il linguaggio Simula ispirò fra i tanti, C#, C++, Ada, Effeil, Java, etc. Proprio con Java, il paradigma della programmazione orientata agli oggetti è diventato quello dominante; contando, infatti, un numero di sviluppatori intorno ai 9 milioni, circa il 20% di quelli totali, e conquistando nel 2005 e nel 2015 il titolo di “Language of the Year” per TIOBE (una famosissima compagnia che si occupa di valutazione della qualità del software), Java è diventato, di fatto, il linguaggio più conosciuto e affermato al mondo. Tuttavia, il paradigma OOP impone al linguaggio Java una complessità strutturale rispetto ad altri linguaggi procedurali accompagnata ad una serie di limitazioni. Ad esempio, cambiare le definizioni nelle superclassi può portare a una ridefinizione a cascata nelle eventuali sottoclassi. Per cercare di superare tali limiti di Java, Google chiese a Robert Griesemer, Rob Pike e Ken Thompson di sviluppare un linguaggio di programmazione capace di supportare, anche se in maniera atipica, il paradigma di programmazione orientata agli oggetti, cercando, però, di snellirne il più possibile la complessità, attraverso una sintassi essenziale e l’uso dei puntatori, nonchè, di superare i limiti dell’ereditarietà tramite una gerarchia basata sulla composizione. Nasce, pertanto, il progetto Go. Nelle intenzioni iniziali di Google, Go dovrebbe essere il linguaggio del futuro. Ma è veramente plausibile che Go “rottami” Java? La risposta non è assolutamente immediata, e necessita, pertanto, di un’analisi approfondita. Go, infatti, è un linguaggio recentissimo; si pensi che la Versione 1 è stata rilasciata appena quattro anni fa, ossia il 28 Marzo 2012; non ha avuto, pertanto, il tempo di affermarsi nel mondo della programmazione. Tuttavia, Google è una delle più grandi e importanti aziende al mondo; è plausibile, pertanto, che, sotto la spinta, anche economica, di questo colosso, Go possa diventare in pochi anni un forte concorrente del linguaggio Java. La presente tesi parte proprio dalle premesse appena descritte e si pone come obiettivo quello di capire se, e fino a che punto, Go possa sostituire Java. Data l’estrema complessità del compito, e per una maggiore obiettività, si è scelto di dividere l’investigazione (svolta presso il laboratorio Barbiana 2.0) in tesi Introduzione 5 e in antitesi; a noi è toccata la parte di “avvocato difensore” di Java, mentre un altro collega si è assunto l’onere di “difendere” Go. Lo studio dei due linguaggi prevede, inizialmente, una descrizione sintetica della storia e delle caratteristiche principali da essi possedute. Java è stato sviluppato per essere un linguaggio capace di supportare tutte le sfaccettature del paradigma OOP e che possa, inoltre, garantire l’indipendenza dalla piattaforma, tramite la Java Virtual Machine. Go, invece è un linguaggio compilato, che fa della velocità il suo punto di forza; esso, pertanto, non prevede nè macchine virtuali nè la complessità sintattica dei tipici linguaggi orientati agli oggetti. Dopo un primo studio generale dei due linguaggi, la presente tesi si prefigge l’obiettivo di descrivere la gestione della programmazione concorrente tramite i thread e le goroutine. Lo studio include, inoltre, la leggibilità del software e il riuso dello stesso, attraverso un’elaboratà analisi della gestione delle funzioni, che ci porterà a introdurre il concetto di polimorfismo, di ereditarietà singola, multipla e simulata, di interfaccia e del suo utilizzo nella programmazione, etc. Per studiare tutti questi aspetti, si modellerà un caso di studio basato sulle transazioni bancarie e si analizzerà, volta per volta, il comportamento dei due linguaggi di programmazione, Java e Go. Nel particolare, la tesi sarà strutturata come di seguito specificato: • • • • • • • Il Capitolo 1 descrive la storia e le caratteristiche principali di Java, con una particolare attenzione sulla programmazione orientata ad oggetti e sulla piattaforma. Il Capitolo 2 illustra la storia e le caratteristiche principali di Go. Il Capitolo 3 presenta il case study e analizza il comportamento dei due linguaggi nella gestione della programmazione concorrente, tramite l’utilizzo dei thread e delle goroutine. Il Capitolo 4 descrive la gestione dei metodi e delle funzioni, dedicando una maggiore attenzione al polimorfismo e ai tipi di ritorno. Il Capitolo 5 tratta l’implementazione delle interfacce. Il Capitolo 6 descrive il riuso del software, concentrandosi sulle gerarchizzazioni tipiche dell’ereditarietà e sulla composizione tipica dell’embedding. Infine, nel Capitolo 7, vengono tratte le conclusioni del lavoro svolto e viene dato uno sguardo a possibili sviluppi futuri. 1 Caratteristiche generali di Java Nel presente capitolo verranno trattate la storia e le caratteristiche principali di Java. La tesi non si prefigge come obiettivo la realizzazione di un manuale e, di conseguenza, le caratteristiche generali saranno trattate in maniera sintetica; in particolare, verrà data maggiore attenzione alla programmazione ad oggetti e alla piattaforma. 1.1 Storia del linguaggio Nel 1991 il “Green Team” guidato da James Gosling della Sun Microsystems realizzò un linguaggio di programmazione per il controllo di elettrodomestici chiamato Oak (“quercia”in inglese), capace di essere eseguito su diversi tipi di processori indipendentemente dall’architettura. In fase di progettazione, la Sun Microsystem decise di abbandonare la logica di controllo degli elettrodomestici per creare un’azienda costola denominata FirstPerson Inc, con l’obiettivo di conquistare, attraverso la piattaforma Oak, il mercato della TV interattiva. Tuttavia, i tempi non erano ancora maturi, il progetto non ricevette finanziamenti adeguati e la FirstPerson cosı̀ come nacque morı̀. Si rendeva, pertanto, necessaria l’elaborazione di una nuova strategia. Fu solo nel 1994, con l’esplosione di Internet, che “the team returned to work up a Java technology-based clone of Mosaic they named “WebRunner” (after the movie Blade Runner), later to become officially known as the HotJavaTM browser”. 1 Il 23 maggio 1995 John Gage, direttore dell’Ufficio della Scienza per la Sun Microsystems, e Marc Andreessen, cofondatore e vice presidente esecutivo di Netscape, annunciarono al pubblico che il linguaggio di programmazione Java era realtà e che sarebbe stata incorporato in Netscape Navigator, all’epoca il portale di Internet più famoso al mondo. La scelta del nome è tutt’oggi un mistero. 1 J.Byous, Java Technology: An early history, 2003, p.4 8 1 Caratteristiche generali di Java Secondo una leggenda metropolitana, mai confermata, Java sarebbe l’acronimo di Just Another Vacuum Acronym ossia “soltanto un altro vuoto acronimo”. Secondo un’altra leggenda, più accreditata, il nome di Java deriva dall’omonimo caffè, del quale i tecnici al lavoro abusarono in fase di realizzazione (questo spiegherebbe il logo del linguaggio, riportato in Figura 1.1). Figura 1.1. Logo di Java La Sun Microsystems nel 1995 decise, inoltre, di mettere subito disposizione il kit di sviluppo JDK (Java Development Kit) e di definire il linguaggio da un documento chiamato The Java Language Specification. La prima edizione del documento (Java SE 1) è stata pubblicata nel 1996 e, ad oggi, la versione più recente delle specifiche è la Java SE 8 Edition. Grazie alle collaborazioni con Netscape, e successivamente con Oracle e IBM, e grazie alla sua filosofia “write once, run everywhere”2 Java è riuscito a conquistare un numero di sviluppatori intorno ai 9 milioni ed ad aggiudicarsi, nel 2005 e nel 2015, il titolo “Language of the Year”, imponendosi, quindi, come il linguaggio informatico più conosciuto e più utilizzato al mondo. La tecnologia Java ha, ormai, invaso anche le nostra vita quotidiana, essendo presente, per esempio, nei nostri smartphone, computer, tablet, smartcard, etc. 1.2 Caratteristiche principali La sintassi di base di Java è basata principalmente sui linguaggi C e C++. Non sono state, tuttavia, inseriti particolari elementi poiché considerati non “performanti” a causa dei diversi errori che potrebbero portare in fase di programmazione. Ad esempio, 2 http://www.computerweekly.com/feature/Write-once-run-anywhere 1.3 La programmazione orientata ad oggetti 9 “unlike C and C++, the Java programming language has no goto statement; identifier statement labels are used with break or continue statements appearing anywhere within the labeled statement.”3 Java è un linguaggio ad alto livello basato sul paradigma di programmazione orientata agli oggetti (di seguito, Object-Oriented-Programming OOP). Java, grazie anche alla OOP, risulta essere più semplice rispetto a linguaggi quale il C a differenza del quale evidenzia una gestione delle eccezioni molto chiara ed efficiente e un sistema di gestione della memoria comodo e pratico, coordinato dal Garbage Collector. Quest’ultimo è un meccanismo che gestisce la memoria in maniera automatica, mappandone le porzioni precedentemente allocate per poi renderle libere quando non sono più utilizzabili. Si solleva, quindi, il programmatore dalla deallocazione manuale, eliminando, di fatto, l’aritmetica dei puntatori, garantendo, sicuramente una robustezza elevata che, tuttavia, verrà pagata in efficienza e spreco di risorse. Punto di forza di Java è l’essere gratuito e possedere una vastissima libreria di classi standard organizzata in vari package. Un altro punto di forza di Java è l’indipendenza della piattaforma, ossia la possibilità di eseguire ogni applicazione su sistemi operativi radicalmente diversi. Ciò è reso possibile grazie alla Java Virtual Machine. Le facilitazioni della programmazione ad oggetti e l’indipendenza della piattaforma hanno permesso a tutti i programmatori, anche principianti, di cimentarsi nello sviluppo di applicazioni Java, riuscendo ad ottenere, in breve tempo, buoni risultati. Tali peculiarità hanno agevolato notevolmente la diffusione del linguaggio. Data l’importanza, descriveremo, in maniera più approfondita, la piattaforma e la OOP in due sezioni a sè stanti. 1.3 La programmazione orientata ad oggetti La OOP è un insieme di strumenti concettuali forniti dal linguaggio che consente di definire oggetti in grado di interagire fra loro, permettendo di descriverne le modalità di interazione e le relazioni di interdipendenza. Gli obiettivi principali della OOP sono l’aumento della leggibilità del codice e la facilitazione della gestione, specie di progetti di grandi dimensioni. Questo tipo di approccio garantisce la modularità e il riuso del codice anche in programmi molto differenti fra loro, facilitando notevolmente il lavoro degli sviluppatori. I linguaggi OOP possiedono caratteristiche identiche e si basano sui concetti fondanti di classi, oggetti, metodi, incapsulamento, ereditarietà e polimorfismo. Nelle prossime sottosezioni esamineremo in dettaglio tali concetti. 3 R Language J. Gosling - B. Joy - G. Steele - G. Bracha - A. Buckley, The Java⃝ Specification Java SE 8 Edition, p.419 10 1 Caratteristiche generali di Java 1.3.1 Classi, oggetti e metodi “Una classe è un’astrazione indicante un insieme di oggetti che condividono le stesse caratteristiche e le stesse funzionalità. Un oggetto è un’istanza (ovvero, una creazione fisica) di una classe.”4 La creazione di una classe avviene tramite la parola chiave class seguita dal nome della classe. All’interno della classe verranno inseriti gli attributi e le funzionalità che compongono la stessa. Per poter meglio spiegare questi concetti consideriamo l’esempio riportato nel Listato 1.1. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class CurrentAccount { private String iban; private String number; private float balance; public CurrentAccount() {} public CurrentAccount(String iban, String number, float balance) { this.iban = iban; this.number = number; this.balance = balance;} public String getIban() { return iban;} public void setIban(String iban) { this.iban = iban;} public String getNumber() { return number;} public void setNumber(String number) { this.number = number;} public float getBalance() { return balance;} public void setBalance(float balance) { this.balance = balance;} public float Deposit (float amount){ System.out.println("Conto corrente n."+getNumber()+"; Saldo "+getBalance()+"; Deposito: "+amount+" euro."); setBalance(getBalance()+amount); return getBalance();}} Listato 1.1. Creazione di una classe In tale listato abbiamo inserito come attributi il saldo, il numero di conto e l’IBAN e come funzionalità il costruttore e il deposito. Il costruttore è un metodo che viene eseguito ogni volta che sarà istanziato un oggetto. Con il codice scritto finora non abbiamo definito nessun conto corrente, ma solo l’“astrazione” dello stesso, e, quindi, non è possibile terminare con successo l’esecuzione del programma. Occorre, pertanto, definire le creazioni fisiche realizzate a partire dall’astrazione. Tale operazione viene mostrata nel Listato 1.2. 1 2 3 4 5 public class Main { public static void main(String[] args) { CurrentAccount CA1 = new CurrentAccount("ITkk", "12345",200); float tmp=CA1.Deposit(500); System.out.println("Nuovo Saldo: "+tmp+" euro"); }} Listato 1.2. Creazione di un oggetto L’istanza CA1 è proprio l’oggetto della classe CurrentAccount; potremmo, quindi, eseguire il metodo Deposit definito precedentemente nella classe. 4 C. De Sio Cesari, Manuale di Java 7: Programmazione orientata agli oggetti con Java Standard Edition 7, Hoepli Editore, 2012, p.32 1.3 La programmazione orientata ad oggetti 11 Compilando il programma, avremo il risultato riportato nella Figura 1.2. Figura 1.2. Risultato del Listato 1.2 Possiamo, quindi, affermare che “ogni oggetto appartiene a una classe e una classe definisce i metodi per i suoi oggetti.” 5 Il metodo è, pertanto, una funzione associata in modo esclusivo ad una classe e rappresenta un’operazione eseguibile sugli oggetti della stessa. 1.3.2 Incapsulamento, ereditarietà e polimorfismo L’incapsulamento è la tecnica di nascondere il funzionamento di una classe in modo da proteggerla dai cambiamenti che si produrrebbero nel caso di errato funzionamento o errata implementazione. Come detto nella sezione precedente, una classe è caratterizzata dai suoi attributi e dalle sue funzionalità. L’incapsulamento prevede, proprio, la suddivisione di una classe in due parti: • • Interfaccia: descritta dall’insieme dei metodi dichiarati pubblici (set e get) e che, quindi, sono invocabili dall’utente e accessibili alle altre classi; Implementazione: descritta dall’insieme degli attributi di una classe, dichiarandoli privati, e quindi inaccessibili, all’utente e alle altre classi. L’incapsulamento favorisce il riuso e la manutenzione del codice, migliorando l’utilizzo delle classi, in quanto eventuali modifiche non coinvolgeranno le parti non visibili all’esterno della classe. È fondamentale, pertanto, utilizzare l’incapsulamento se vogliamo garantire al nostro programma caratteristiche di robustezza, riusabilità e indipendenza del codice. L’ereditarietà consiste in una relazione per estensione o specializzazione, che si stabilisce tra due classi aventi caratteristiche comuni. Se bisogna implementare una classe B particolare, e abbiamo già creato una classe A che descrive un concetto più generico, possiamo far ereditare alla classe B il codice della classe A, facendo diventare, quindi, A superclasse di B. L’ereditarietà è parte principale della OOP poiché permette una strutturazione gerarchica nel programma garantendo leggibilità, riduzione dei tempi di sviluppo e, soprattutto, riutilizzo del codice. Senza l’ereditarietà classi con caratteristiche comuni andrebbero riscritte dall’inizio, appesantendo notevolmente il programma. 5 Cay Horstmann, Concetti di Informatica e fondamenti di Java (IV Edizione) Edizione italiana a cura di Marcello Delpasso, Milano, Apogeo s.r.l., 2007, p.35 12 1 Caratteristiche generali di Java Utilizzando l’ereditarietà potremmo, invece, evitare di scrivere il codice in comune tra la classe e la superclasse. In Java non è presente l’ereditarietà multipla poiché ritenuta problematica dal punto di vista implementativo. Tale scelta avvantaggia la robustezza, limitando, però, le potenzialità del linguaggio di programmazione. È, comunque, presente una “scappatoia” per superare questo limite tramite le interfacce, simulando il meccanismo di ereditarietà multipla. In Java è, inoltre, presente una classe Object, che viene pensata come “astrazione” di un oggetto generico. Possiamo dire che in Java, la classe Object è la superclasse di tutte le classi. Il polimorfismo permette, invece, di raggruppare in un unico termine entità differenti e aventi caratteristiche comuni. È un concetto potentissimo che ci permette di scrivere del codice generico che si specializzerà in funzione dell’oggetto che lo ha invocato. Attraverso l’utilizzo del polimorfismo il sistema è in grado di capire automaticamente cosa fare, e si comporterà in maniera diversa per oggetti di tipo diverso, garantendo, ancora una volta, il riuso del codice. Se non si utilizzasse il polimorfismo dovremmo implementare all’interno di una classe un costrutto del tipo switch-case che appesantirà e aumenterà la complessità del codice. 1.4 La piattaforma La piattaforma Java è un software realizzato dalla Sun Microsystems che rende possibile la scrittura e l’esecuzione di programmi Java su diversi hardware, indipendentemente dalla loro architettura, rendendoli, pertanto, portabili su ciascun dispositivo. La piattaforma mette a disposizione il compilatore Javac, la macchina virtuale Java Virtual Machine e il set di librerie della documentazione API. 1.4.1 Java Virtual Machine La Java Virtual Machine (JVM) è una macchina virtuale multithreading, “type safety” (ovvero, previene gli errori di tipo) e con strutture dati basate sugli stack. La JVM deve essere installata su una macchina reale per garantire il funzionamento di qualsiasi programma Java, indipendentemente dall’architettura. Il codice sorgente (scritto in .java) viene, inizialmente, analizzato dal compilatore del linguaggio, il Javac, il quale produce il bytecode in stretta relazione con il sistema operativo e con l’hardware in uso. Il bytecode viene salvato in un file con estensione .class. La JVM legge il bytecode e lo interpreta secondo le caratteristiche della macchina sul quale è installato ed esegue il programma. Possiamo, quindi, riassumere il funzionamento della JVM con lo schema mostrato in Figura 1.3. 1.4 La piattaforma 13 Figura 1.3. Funzionamento della Java Virtual Machine I file class vengono, poi, caricati nell’area dati di runtime da un meccanismo chiamato class loader ogni volta è necessario utilizzare una specifica classe. Per eseguirli, Java utilizza i componenti mostrati in Figura 1.4. 14 1 Caratteristiche generali di Java Figura 1.4. Schematizzazione della Java Virtual Machine Esaminiamo, ora, più in dettaglio tali componenti: • Stack : è un’area di memoria con modalità di accesso LIFO (Last In, First Out). Lo stack immagazzina le variabili locali di ogni metodo e tiene traccia di ogni invocazione ai metodi stessi. Esso utilizza tre registri per effettuare le sue operazioni, ovvero: – Vars register : memorizza tutte le variabili locali dei metodi invocati; – Frame register : memorizza le operazioni della pila stessa; – Optop register : memorizza le istruzioni per le operazioni in bytecode. • Area dei metodi : quando viene caricato un class file, la JVM elabora le sue informazioni e le pone nell’area dei metodi. In essa risiede quindi il bytecode da eseguire. L’indirizzo della successiva istruzione da prelevare è contenuto nel registro Program Counter; • Heap: appena il programma viene eseguito, la JVM carica nell’Heap, man mano che vengono creati, tutti gli oggetti che il programma istanzia. A differenza di C ++, Java non ha i puntatori per liberare la memoria precedentemente allocata. Questo meccanismo viene attuato automaticamente attraverso il Garbage Collector. Quest’ultimo è deputato alla rimozione degli oggetti non più utilizzati dal programma man mano che questo è in esecuzione. L’area di dati runtime, infine, interagisce con l’Execution Engine. Quest’ultimo è responsabile dell’esecuzione delle istruzioni su diverse piattaforme. 1.4 La piattaforma 15 Le specifiche della JVM non pongono nessun vincolo sul procedimento, lasciando, dunque, ampia libertà sull’algoritmo risolutivo. Il grande vantaggio ottenuto dall’interdipendenza dell’architettura viene, però, pagato in termini di ottimizzazione. Non è, ad esempio, possibile sfruttare le caratteristiche peculiari di un particolare sistema operativo, e la traduzione del bytecode potrebbe causare lentezza nell’ esecuzione del programma Java. 1.4.2 La documentazione API Java gode di una vastissima e preziosa libreria di classi standard. Il set delle librerie disponibili al programmatore è contenuto nella API (Application Programming Interface), consultabile al sito https://docs.oracle.com/javase/7/docs/api/. Lo scopo dell’API è quello di cercare di assicurarsi l’obiettivo di effettuare un’“astrazione” a più alto livello, mettendo in comunicazione la parte hardware con il programmatore, semplificando, cosı̀, lo sviluppo di programmi Java. Le API consentono, quindi, di evitare allo sviluppatore di scrivere da zero tutte le funzioni necessarie al programma, permettendo quindi, di default, un ampio riutilizzo del codice. Vediamo come è strutturata nel dettaglio la documentazione. L’API è suddivisa in vari package che contengono le varie classi raggruppate per campo di utilizzo; per ogni package viene specificata una breve descrizione degli stessi(Figura 1.5). Figura 1.5. Documentazione API: lista dei package Selezionando un package si avrà accesso a tutte le entità da esso previste, ordinate per tipo, come, ad esempio, interfacce, classi, eccezioni, etc. (Figura 1.6). A ciascuna entità sarà allegata una breve descrizione contenente informazioni su come poterla utilizzare. 16 1 Caratteristiche generali di Java Figura 1.6. Documentazione API: contenuto dei package Per poter utilizzare il set di librerie presenti nella documentazione API, basta “importarle” con il comando import, come mostrato nel Listato 1.3. 1 import java.util.Scanner; Listato 1.3. Esempio di importazione di una libreria Attraverso questo comando viene importata la classe Scanner della libreria util, permettendo di utilizzare l’inserimento da tastiera. La libreria più importante presente nella documentazione è la java.lang.*. Essa permette, ad esempio, di utilizzare i comandi String e System. Data la presenza di classi fondamentali allo sviluppo, Java importa, automaticamente, tale libreria, facilitando, ancora di più, il compito degli sviluppatori. L’API racchiude in sé migliaia di librerie, ed è in continuo aggiornamento. Questo è un grandissimo punto di forza e rappresenta uno dei fattori chiave che ha permesso a Java di raggiungere il suo straordinario successo. 2 Caratteristiche generali di Go In questo capitolo si illustreranno, allo stesso modo di come è stato fatto con Java, la storia e le caratteristiche principali di Golang. Tuttavia, verrà data maggiore attenzione alla sintassi. 2.1 Storia del linguaggio Golang, conosciuto meglio con il nome Go, è un linguaggio di programmazione realizzato da Robert C. Pike (sviluppatore di Unix e del linguaggio Limbo, collaboratore per la creazione dei sistemi Plan 9 e Inferno), Ken Thompson (inventore del linguaggio B) e Robert Griesemer (collaboratore allo sviluppo di V8 JavaScript per Google), su incarico della Google Inc. Scopo del linguaggio di programmazione era quello di risolvere alcuni problemi di sviluppo che la stessa azienda stava affrontando: “The first problem is that networked systems are getting larger, with more interaction between more pieces, and existing languages were not making those interactions as smooth to express as they could be.[...] The second problem is that programs are getting larger and development more distributed. A language that works very well for a small group may not work as well for a large company with thousands of engineers working on many millions of lines of code.” 1 I tre programmatori si riunirono il 21 Settembre 2007 per stilare gli obiettivi che il linguaggio doveva raggiungere. Decisero, quindi, di basarsi su un precedente lavoro correlato con Inferno per creare un linguaggio che riuscisse ad avere le seguenti caratteristiche: • • • 1 compilazione efficiente; velocità di esecuzione; facilità di programmazione. http://www.pl-enthusiast.net/2015/03/25/interview-with-gos-russ-cox-and-sameerajmani/ 18 2 Caratteristiche generali di Go Nel Gennaio 2008, Ken Thompson iniziò a lavorare sul compilatore. Nel Maggio dello stesso anno la Google incaricò, indipendentemente dal team originario, Ian Taylor per la realizzazione di un prototipo riguardante lo standard per le librerie e Russ Cox per implementare le stesse librerie. Go era diventato, finalmente, un progetto a tempo pieno. Non solo i programmatori della Google parteciparono alla realizzazione del progetto, ma anche “many people from the community have contributed ideas, discussions, and code”. 2 Il linguaggio di programmazione fu annunciato il 10 Novembre 2009 e venne subito eletto “Program Language of the Year” dalla Tiobe nello stesso anno. Google ha affrontato, inizialmente, grossi problemi di copyright per il nome scelto. Esiste infatti, già dal 2003, un linguaggio di programmazione chiamato “Go!”, sviluppato da McCabe e Keith Clark. Nonostante il dissenso dei due sviluppatori, la questione non giunse mai in tribunale. La Versione 1.0 di Golang è stata rilasciata, ufficialmente, nel 2012 e ha avuto negli anni successivi e numerosi aggiornamenti, fino ad arrivare alla Versione 1.6 nel Febbrario 2016. Dalla Versione 1.5 è, inoltre, possibile includere nelle applicazioni dei sistemi IOS e Android le librerie di Go. Sembra chiaro, anche dalla conferenza GopherCon del 2015, che Google voglia puntare su Go per conquistare il mercato delle app per gli smartphone. Dal grafico in Figura 2.1 si può notare che, dopo una iniziale e tiepida accoglienza, negli ultimi due anni Go ha avuto una crescita di interesse esponenziale. Figura 2.1. Interesse per Golang Nel grafico, l’asse delle ascisse misura un intervallo di tempo, espresso in anni, mentre l’asse delle ordinate misura il valore relativo alla keyword “Golang”. 2 https://golang.org/doc/faq#history 2.2 Caratteristiche principali 19 Il valore massimo riscontrato nell’intervallo viene riportato pari a 100, mentre gli altri valori vengono calcolati e visualizzati in modo relativo a questo picco. Il grafico risulta di impatto se, utilizzando lo stesso metro di ricerca, si inserisce nell’asse delle ordinate la keyword “Java”(Figura 2.2). Figura 2.2. Interesse per Java La crescente attenzione e la spinta economica del colosso Google potrebbero portare Go a raggiungere, nel prossimo futuro, lo stesso livello di mercato di Java. 2.2 Caratteristiche principali Go è stato rilasciato dalla Google Inc. come progetto open source. La finalità del progetto è ambiziosa; l’azienda di Montain View tenta di creare, attraverso il linguaggio Golang, uno strumento general-purpose, adattato alla programmazione di sistema, capace di combinare la semplicità di programmazione di un linguaggio interpretato, tipica dei linguaggi dinamici, con la sicurezza e le funzionalità di un linguaggio compilato, esibite, tipicamente, in un linguaggio statico. Go è stato pensato, pertanto, come un linguaggio che deve offrire sia alta velocità di esecuzione e di compilazione, sia facilità di programmazione. Go supporta, a livello nativo, la programmazione concorrente, grazie all’implementazione delle goroutine e dei channel. Esse sono funzioni progettate per ottimizzare i tempi di compilazione che, se eseguite in maniera corretta, promettono un apporto fondamentale per l’esecuzione e la comunicazione concorrente. Il linguaggio, tuttavia, non è stato ottimizzato per la programmazione in parallelo. Lo stesso Rob Pike intitolò il suo intervento alla conferenza Waza, tenuta l’11 Gennaio 2012, “Concurrency is not parallelism”3 . 3 https://vimeo.com/49718712 20 2 Caratteristiche generali di Go L’assenza di parallelismo reale non deve essere pensata, necessariamente, come un limite. Un sistema multiprocessore, mentre esegue un programma, eseguirà sempre altre istruizioni, all’infuori dello stesso, rendendo, di fatto, impossibile il parallelismo reale all’interno di una stessa applicazione. Golang non presentava, in origine, l’intercettazione delle eccezioni. Tuttavia, sotto la spinta della community furono inseriti i comandi: • panic: la funzione che ha causato lo stato di panic viene, istantaneamente, bloccata; • defer: ogni funzione deferred opera come se la funzione panic fosse terminata correttamente. Il blocco panic/defer è assolutamente assimilabile al blocco try/catch presente in Java. Altri aspetti del linguaggio Go verranno approfonditi nelle sottosezioni successive. 2.2.1 La compilazione e le librerie Go è un linguaggio compilato. Esso non supporta la presenza di una macchina virtuale con le funzioni di interprete; l’assenza di tale meccanismo, pur non permettendo l’indipendenza dell’architettura, consente una conversione in codice binario del codice sorgente, in modo più o meno istantaneo, rendendo possibile la realizzazione di applicazioni con una velocità di compilazione eccellente. Tali caratteristiche promettono di ottenere buone prestazioni anche su hardware caratterizzati da performance basse. Di notevole interessante è la presenza nel file binario compilato della maggior parte delle librerie necessarie alla realizzazione dei programmi. Non è necessario, pertanto, l’installazione di librerie aggiuntive, se non quelle minime necessarie al sistema. Tale proprietà, però, è la causa della creazione di file binari compilati di dimensioni anche importanti. Go possiede, infatti, nativamente, una vasto quantitativo di librerie, ciascuna delle quali è ben fornita. Seppur non paragonabile alla documentazione API di Java, Go presenta librerie che rispondo alle più bizzarre richieste dell’utenza. Il linguaggio, per esempio, implementa la libreria net, fornita, come suggerisce il nome, di tutte le primitive di rete, o, per esempio, la libreria zip, che supporta la scrittura o la lettura degli archivi zip. Come per Java, le librerie possono essere consulatibili sul sito ufficiale all’indirizzo https://golang.org/pkg/ 2.2.2 Go è un linguaggio OOP? Alla domanda diretta il sito ufficiale risponde: “Yes e no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different or approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something, but not identical, to subclassing.Moreover, methods in Go 2.2 Caratteristiche principali 21 are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs. Also, the lack of a type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java.” 4 Da tale risposta si può concludere che Go non è tipicamente un linguaggio OOP. Esso permette lo stile OOP, ma, presentando un sistema di tipizzazione non gerarchico, non ne consente un approccio standard, come in Java. Tale caratteristica velocizza la stesura del codice, poichè non è necessario riflettere sulle gerarchie tra tipi di dati, consentendo una “tipizzazione leggera” che non è facilmente riscontrabile negli altri linguaggi OOP. Anche in Go, infatti, è possibile definire dei tipi da parte dell’utente. Tale pratica, comune nei linguaggi basati sul paradigma di OOP, si ottiene utilizzando le struct. Le struct sono entità simili alle classi, precedentemente trattate nella sezione dedicata a Java. Da un punto vista prettamente morfologico, le struct sono costituite dai campi, ossia una formazione di valori di tipo eterogeneo, accessibili singolarmente. Le struct sono definite completamente per mezzo della funzione new. Pragmaticamente, ogni struct è costruita come un blocco contiguo di memoria che descrive i suoi elementi costitutivi. L’esempio riportato nel Listato 2.1 descrive l’aspetto formale di una struct. 1 2 3 4 5 6 7 package main import "fmt" type CurrentAccount struct { iban string number string balance float64 } Listato 2.1. Esempio di una struct Le struct sono, essenzialmente, tipi valore; pertanto, la funzione new restituisce un puntatore ad un valore. L’esempio nel Listato 2.2 descrive come poter utilizzare la funzione new. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package main import "fmt" type CurrentAccount struct { iban string number string balance float64} func main() { CA1 := new(CurrentAccount) CA1.balance=1000 CA1.iban="ITkkABBBBBCCCCCXXXXXX" CA1.number="12345" fmt.Print(CA1)} Listato 2.2. La funzione new Tuttavia, difficilmente una struct viene instanziata come riportato nel Listato 2.2. L’istanziazione consueta è, infatti, quella riportata nel Listato 2.3. 1 2 3 4 4 package main import "fmt" type CurrentAccount struct { https://golang.org/doc/faq#Is Go an object-oriented language 22 2 Caratteristiche generali di Go 5 6 7 8 9 10 11 iban string number string balance float64} func main() { CA1 :=CurrentAccount{iban:"ITkkABBBBBCCCCCXXXXXX",number:"12345",balance:1000} fmt.Print(CA1) } Listato 2.3. Istanziazione consueta di una struct I due listati produrrano un output identico, a meno di un carattere “&” (indica il riferimento). I risultati dei due listati sono riportati, rispettivamente, nelle Figure 2.3 e 2.4. Figura 2.3. Risultato del Listato 2.2 Figura 2.4. Risultato del Listato 2.3 Interessante è notare come nelle struct non trovino spazio i metodi, che sono, invece, parte integrante delle classi dei linguaggi tipicamente OOP. Il linguaggio Go, pur non permettendo l’implementazione di tale caratteristica, rende possibile il “collegamento” fra i metodi e un qualsiasi tipo di struct, tramite l’utilizzo di puntatori. I puntatori, familiari nei linguaggi C e C++, risultano del tutto estranei agli sviluppatori di programmi che hanno sacrificato l’efficienza della programmazione in favore della facilità. I puntatori, infatti, garantiscono performance elevate, soprattutto nell’ambito del controllo e della gestione della memoria. Go, pur utilizzando i puntatori, cerca di trovare il giusto compromesso fra la facilità e l’efficienza, allontanando il programmatore dalla loro aritmetica. La memoria in Go, infatti, è gestita completamente dal Garbage Collector in maniera analoga a Java. Tale meccanismo, pertanto, terrà traccia delle aree di memoria non più referenziate e le libererà automaticamente. Come detto precedentemente, i puntatori permettono di “allegare” una struct a un metodo. Il ricevente di una funzione ha bisogno di essere asserito sia nel package della funzione stessa e che nel package di tutte le funzioni cui afferisce. Inoltre, il suo nome deve essere utilizzato all’interno della funzione. Una struct può, di conseguenza, essere arricchita tramite una funzione. Tale meccanismo è notevolmente simile ai meccanismi delle classi, con l’eccezione che la funzione non è inclusa nel corpo della struct. Per poter meglio capire come “collegare” una funzione a una struct, si consideri il Listato 2.4. Come si può notare, la func Deposit riferisce ad un puntatore alla struct CurrentAccount. Nel main possiamo, quindi, utilizzare la funzione allo stesso modo in cui può essere utilizzato un metodo nei linguaggi OOP. 2.3 La sintassi 23 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package main import "fmt" type CurrentAccount struct { iban string number string balance float64} func (CA1 *CurrentAccount) Deposit(amount float64){ fmt.Println("Deposito:", amount) CA1.balance+=amount fmt.Println("Conto corrente n.",CA1.number,",IBAN:",CA1.iban,",Saldo:",CA1.balance ) } func main() { CA1 :=CurrentAccount{"ITkkABBBBBCCCCCXXXXXX","12345",1000} fmt.Println("Conto corrente n.",CA1.number,",IBAN:",CA1.iban,", Saldo:",CA1.balance ) var amount float64 amount=2000 CA1.Deposit(amount) } Listato 2.4. Struct e func Dai listati presentati si osserva che la sintassi di base di Go, seppur simile a quella di C++, C e Java, presenta meccaniche abbastanza differenti e offre discreti vantaggi. Tratteremo la sintassi in maniera approfondita in una sezione a sè stante. 2.3 La sintassi Dal punto vista sintattico Golang è stato pensato in maniera tale da alleggerire sia la stesura di un programma sia il compito del programmatore. 2.3.1 Case sensitive Come la maggior parte dei linguaggi di programmazione, Go è “case sensite”. Le dichiarazioni di variabili omonime, che differiscono dell’iniziale maiuscola o minuscola, sono considerate elementi completamente diversi. Ad esempio, consideriamo il Listato 2.5; esso produrrà come risultato: Amount= 3000 ; amount= 2000 1 2 3 4 5 6 7 8 9 package main import "fmt" func main() { var amount float64 amount=2000 var Amount float64 Amount=3000 fmt.Println("Amount=",Amount,"; amount=", amount)} Listato 2.5. Esempio di case sensitive L’identificatore di una variabile non è da scegliere a cuor leggero. Se esso inizia con la lettera maiuscola, come, ad esempio, Amount del Listato 2.5, la variabile sarà visibile anche all’esterno del package. Viceversa, la visibilità di una variabile il cui nome inizia con la lettere minuscola sarà limitata al package di appartenenza. I package sono concetti sostanzialmente simili a quelli di Java. 24 2 Caratteristiche generali di Go 2.3.2 Le variabili Per definire le variabili, Go utilizza, essenzialmente, le keyword: • var: descrive le variabili, il cui valore può essere modificato; • const: descrive le variabili, il cui valore deve rimanere costante. Il compilatore Go presenta la caratteristica dell’Interferenza di tipo, ossia un meccanismo che permette di dedurre, automaticamente, il tipo di una var o di una const, basandosi, esclusivamente, sul valore attribuito in sede di dichiarazione. Tale meccanismo si avvia attraverso l’operatore “:=”, chiamato operatore di assegnazione diretta. Consideriamo l’esempio riportato nel Listato 2.6; il compilatore riuscirà a decifrare il tipo della variabileamount assegnando ad essa il tipo float64. 1 2 3 4 5 package main import "fmt" func main() { amount:=2000.6} Listato 2.6. Operatore di assegnazione diretta in Go A differenza di Java, se in fase di dichiarazione nessun valore viene dato alla variabile, essa verrà inizializzata automaticamente al valore nullo (Listato 2.7). 1 2 3 4 5 6 package main import "fmt" func main() { var amount float64 fmt.Println("il valore di amount è",amount) } Listato 2.7. Variabile non inizializzata in Go Il Listato 2.8 è equivalente al Listato 2.7 visto in precedenza. 1 2 3 4 5 6 7 package main import "fmt" func main() { var amount float64 amount = 0 fmt.Println("il valore di amount è", amount) } Listato 2.8. Assegnazione esplicita di un tipo ad una variabile in Go In Java, invece, se proviamo a implentare il codice riportato nel Listato 2.9, sarà segnalato l’errore “Exception in thread ‘‘main java.lang.Error: Unresolved compilation problem: The local variable amount may not have been initialized at Main.main(Main.java:4)” 1 2 3 4 public class Main { public static void main(String[] args){ float amount; System.out.println(amount); }} Listato 2.9. Variabile non inizializzata in Java In Go, di default, tutte le variabili sono inizializzate. Tale meccanismo viene utilizzato sia per ottimizzare i tempi di programmazione e sia per evitare le criticità relative a eventuali errori dello sviluppatore, facilitando, quindi, la programmazione. 2.3 La sintassi 2.3.3 25 I punti e virgola La grammatica formale dei linguaggi di programmazione usa concludere una sequenza di codice adoperando i punti e virgola “;”. Come si può notare dai listati precedenti, i programmi Go possono omettere la maggior parte di questi punti e virgola. La sintassi del codice detta i due assunti che regolano la possibilità di poter omettere tale segni di interpunzione: • • 1 : La serie di istruzioni deve essere suddivisa in token (una serie di caratteri individuata da caratteri di delimitazione), e il carattere di delimitazione della riga deve essere un identificatore, un int, un float, etc., o ancora una keyword come, ad esempio, return, break, etc.; 2 : prima della chiusura di una parentesi tonda, graffa o quadra. 2.3.4 Strutture di controllo Le strutture di controllo sono costrutti sintattici la cui semantica afferisce al controllo del flusso di esecuzione di un programma, ovvero servono a specificare se, quando, in quale ordine e quante volte devono essere eseguite le istruzioni che compongono il codice sorgente in base alle specifiche di progetto del software da realizzare. Go, naturalmente, presenta tali costrutti. Prendiamo, ad esempio, il costrutto if-else (Listato 2.10). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package main import "fmt" type CurrentAccount struct { iban string number string balance float64 } func (CA1 *CurrentAccount) Withdraw(amount float64){ if amount>CA1.balance{ fmt.Println("Si tenta di ritirate ",amount,", ma il saldo è minore dell’importo.") }else{ fmt.Println("Ritiro:", amount) CA1.balance-=amount fmt.Println("Saldo:",CA1.balance ) }} func main() { CA1 := CurrentAccount{"ITkkABBBBBCCCCCXXXXXX","12345",1000} var amount float64 amount=2000.8 CA1.Withdraw(amount) } Listato 2.10. Istruzione if-else in Go Rispetto a Java, la coppia di parentesi graffe è obbligatoria, anche in presenza di una sola istruzione, mentre non è obbligatoria la coppia di parentesi tonde. Ciò detto vale anche per altri costrutti, come, ad esempio, il ciclo for (Listato 2.11). 1 2 3 4 5 6 package main import "fmt" func main() { for j := 0; j <= 5; j++ { fmt.Println("il valore di j è ", j) }} Listato 2.11. Ciclo for in Go 26 2 Caratteristiche generali di Go Il codice di Go, a livello puramente sintattico, si dimostra molto più leggero della concorrenza. Tale scelta di implementazione cerca rispettare l’obiettivo di perseguire l’effecienza di compilazione e la velocità di esecuzione. Il guadagno che si potrebbe ottenere, seppure trascurabile su operazioni semplici, potrebbe risultare fondamentale per programmi di elevate dimensioni. 3 Java vs Go: gestione della concorrenza Nel presente capitolo viene presentato, dapprima, il case study che si andrà ad analizzare, descrivendone il diagramma dei casi d’uso, quello entità-relazione, quello delle classi e quello di sequenza. In seguito, verrà presentata un’introduzione generale sulla gestione della concorrenza nei linguaggi di programmazione Java e Go, per poi implementare in codice il case study e, infine, trarre le conclusioni. 3.1 Presentazione del case study 3.1.1 Descrizione del contesto di riferimento In questa sezione verrà presentato il case study di interesse. Analizzandolo, potremmo studiare, pertanto, i punti di forza e di debolezza dei due linguaggi Java e Go. In questa analisi si utilizzerà un computer con le specifiche tecniche riportate in Figura 3.1. Figura 3.1. Specifiche tecniche del calcolatore utilizzato per l’analisi Il case study di base riguarderà un tipico esempio di sistema bancario. Esso verrà strutturato come di seguito specificato: • Customer : gli utenti che possono accedere alla banca sono di due tipi: 28 3 Java vs Go: gestione della concorrenza – Private: rappresentano i privati cittadini. Di ogni Private interesserà nome, cognome, codice fiscale e documento d’identità; – Company: rappresenta le aziende. Di ogni Company interessano partita IVA e nome dell’azienda. Di ogni Customer saranno, inoltre, memorizzati l’identificativo, l’indirizzo, la città, il Codice di Avviamento Postale, l’email, il telefono fisso e quello mobile. Ogni utente è in possesso di uno o più conto correnti. • Current Account: di ogni conto corrente interesserà l’IBAN, il numero di conto e il saldo. Il conto corrente dà accesso alle transazioni bancarie; • Transaction: ogni transazione sarà caratterizzata dall’identificativo e dall’importo. Le transazioni possono essere, essenzialmente, di due tipi: – Internal : rappresentano le transazioni interne al conto, ossia: · Deposit: rappresentano i depositi bancari effettuati sul conto; · Withdraw : rappresentano i ritiri bancari effettuati dal conto; – Transfer : descrivono i bonifici bancari effettuati da un conto all’altro. Dei trasferimenti interessa inoltre l’IBAN del destinatario. 3.1.2 Diagramma dei casi d’uso Il diagramma dei casi d’uso (UCD, Use Case Diagram) è lo schema grafico che rappresenta sinteticamente l’andamento delle funzioni fornite dal sistema, cosı̀ come vengono utilizzate dagli attori che interagiscono col sistema stesso. Un attore, di conseguenza, specifica il ruolo assunto dall’utente che interagisce con l’argomento del diagramma nell’ambito del caso d’uso del sistema. Nel diagramma, generalmente, si possono notare le seguenti relazioni: • Inclusione: la relazione di inclusione viene rappresentata da una linea tratteggiata con indicazione dello stereotipo ≪include≫ e indica che la funzione rappresentata dal caso d’uso alla base delle freccia include completamente la funzione rappresentata del caso d’uso alla punta della freccia. • Estensione: La relazione di estensione è rappresentata da una linea tratteggiata con indicazione dello stereotipo ≪extend≫, indica che la funzione rappresentata dal caso d’uso estendente alla base della freccia può essere impiegata nel contesto della funzione estesa alla punta, rappresentandone, quindi, un arricchimento. Nel contesto di riferimento, tuttavia, trova spazio solo la relazione di inclusione. Il diagramma è stato realizzato secondo lo standard UML tramite il tool Enterprise Architect (Figura 3.2). 3.1 Presentazione del case study 29 Figura 3.2. Diagramma dei casi d’uso per il nostro test 3.1.3 Progettazione concettuale Dopo aver definito il diagramma dei casi d’uso è possibile dedicarsi alla progettazione concettuale. Essa permette di rappresentare le entità e le relazioni del case study. Tale progettazione deve essere indipendente dai dettagli dell’implementazione, come, ad esempio, la concorrenza, e deve chiarire il significato dei termini, spesso confondibili, assicurando l’interpretazione corretta di quelli specificati. Tali concetti ambigui, infatti, potrebbero portare ad errori in fase di progettazione del software. Una volta modellati i termini, la progettazione diviene un modello stabile per l’implementazione successiva del case study. I concetti espressi nella progettazione concettuale sono utili, soprattutto, per la realizzazione di un software che sfrutta la OOP. Ci serviremo del modello Entità/Relazione (E/R) per descrivere la realtà di interesse. Il modello Entità/Relazione Il modello E/R fornisce una serie di strutture che permettono di descrivere una qualsiasi realtà, a prescindere dai criteri di organizzazione dei dati, in modo chiaro e comprensibile. Il modello è chiamato Entità/Relazione per i suoi costrutti fondamentali, che sono: • • Entità: rappresenta una classe di oggetti che hanno proprietà comuni ed esistenza autonoma; Relazione: rappresenta i legami logici intercorrenti fra due o più entità. Accanto a questi due costrutti il modello presenta: 30 3 Java vs Go: gestione della concorrenza • Attributi : struttura semplice che descrivere una proprietà di un’entità o di una relazione; • Cardinalità delle relazioni : indica il minimo e il massimo numero di occorrenze di un’entità che possono partecipare ad una determinata relazione; • Cardinalità degli attributi : indica il minimo e il massimo numero di occorrenze di un attributo che possono partecipare ad un’entità; • Identificatori : sono attributi che identificano in maniera univoca ogni occorrenza di un’entità; • Generalizzazioni : rappresentano legami logici esistenti fra due o più entità. Le entità coinvolte si distinguono in entità padre ed entità figlie. Lo schema Entità/Relazione della realtà di interesse è rappresentato in Figura 3.3. Figura 3.3. Il modello Entità-Relazione 3.1 Presentazione del case study 31 Dizionario dei dati Una volta effettuata la creazione dello schema Entità/Relazione, è necessario stilare una documentazione di supporto per descrivere i termini rappresentati nello schema stesso. Viene redatto, pertanto, il dizionario dei dati, suddiviso per entità e relazioni. Il dizionario delle entità e quello delle relazioni, relativi al test di nostro interesse sono, rispettivamente, rappresentati in Figura 3.4 e in Figura 3.5 Figura 3.4. Dizionario delle Entità Figura 3.5. Dizionario delle Relazioni 3.1.4 Progettazione della componente applicativa La progettazione della componente applicativa è lo stadio di progettazione dedicato allo sviluppo delle componenti software del case study. Utilizzando lo standard UML, la progettazione della componente applicativa, nel contesto di riferimento, consisterà in due fasi fondamentali, ossia diagramma delle classi e diagrammi di sequenza. 32 3 Java vs Go: gestione della concorrenza Definizione del diagramma delle classi Descrive il tipo degli oggetti che compongono il sistema e le relazioni statiche esistenti tra esse. Per ciascuna classe, inoltre, si specificano metodi e attributi. In Figura 3.6 è riportato il diagramma delle classi del caso di studio di interesse. Figura 3.6. Diagramma delle classi relativo al test di nostro interesse 3.1 Presentazione del case study 33 Definizione dei diagrammi di sequenza Descrive una sequenza di azioni dove tutte le scelte sono prestabilite e, di conseguenza, dove non trovano spazio flussi alternativi di esecuzione. Il diagramma, quindi, descrive le relazioni che intercorrono, in termini di messaggi, tra gli attori e gli oggetti del sistema. Realizziamo, ora, i diagrammi di sequenza. I diagrammi di sequenza relativi alle attività di Deposito, Ritiro e Trasferimento sono rappresentati, rispettivamente, nelle Figure 3.7-3.9. Figura 3.7. Deposito: Diagramma di sequenza 34 3 Java vs Go: gestione della concorrenza Figura 3.8. Ritiro: Diagramma di sequenza 3.1 Presentazione del case study 35 Figura 3.9. Bonifico: Diagramma di sequenza La “sezione critica” è la porzione di codice che dà accesso a una risorsa condivisa fra i flussi di esecuzione. 36 3 Java vs Go: gestione della concorrenza 3.2 La programmazione concorrente in Java La programmazione, idealmente, si struttura seguendo un standard di esecuzione sequenziale. Tale modello è applicabile a un programma eseguito da un singolo processo. Nella realtà, diversi processi potrebbero dover essere in esecuzione contemporaneamente. Risulta, pertanto, necessario che i processi stessi agiscano in parallelo. Fintantoché i processi non interferiscono fra loro, se in presenza di un sistema multi-processore, non è necessario prestare una considerevole attenzione, e, pertanto, l’esistenza di più di processi in esecuzione non è condizione sufficiente per alterare il paradigma della programmazione sequenziale. Il problema si complica se le attività dei processi, eseguite in parallelo, si sovrappongono accedendo a risorse software, o hardware, condivise. I processi sono, allora, in concorrenza. La programmazione concorrente è proprio l’oggetto del presente capitolo. 3.2.1 Introduzione ai thread Definizione di thread Ogni processo effettua più attività, sia in foreground, che in background. L’esecuzione di tali attività potrebbe causare lunghi tempi di attesa. Risulta, pertanto, necessario “parallelizzare” il processo, costruendo “processi leggeri” detti thread. Poichè i thread associati ad uno stesso processo condividono la stessa porzione di codice, per poter effettuare il contest switch, è necessaria la presenza di tre elementi fondamentali, ovvero: • Program Counter ; • Registri ; • Stack. Vediamo, nel dettaglio, come si presentano i thread nel linguaggio di programmazione Java. In Java l’esecuzione dei thread è a carico della JVM. Per eseguire il thread bisogna seguire le istruzioni riportate nel Listato 3.3. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class EsempioThread extends Thread{ private static final int rpt=2000; private static final int delay=1; public void run () { try { for (int i=1;i<=rpt;i++){ //azione da eseguire sleep(delay); }} catch (InterruptedException exception){ //azione di pulizia}} } Listato 3.1. Esempio di thread Il corpo del thread è costituito dal metodo run(); all’interno del metodo, infatti, occorre definire il codice che si vuole fare eseguire. 3.2 La programmazione concorrente in Java 37 Si implementa un ciclo for all’interno del quale è definito il comportamento del thread e la variabile static final int rpt descrive il numero di volte che si vuole provare ad eseguire il thread stesso. Alla fine del token, è inserito un metodo sleep. Esso pone in uno stato dormiente il thread attuale per un numero di millisecondi specificato nella variabile delay. Tuttavia, far “dormire” un thread, seppur necessario alla risoluzione del programma, può rivelarsi un’azione potenzialmente fatale, portando in deadlock tutto il sistema. Si inserisce, pertanto, il corpo del codice in un blocco try-catch. Il thread in attesa viene interrotto, generando una eccezione, che, dopo le opportune azioni di pulizia, farà terminare il thread stesso. Il thread, per essere eseguito, deve, naturalmente, essere avviato, tramite il metodo start(), cosı̀ come mostrato nel Listato 3.2. 1 2 3 4 public class Main { public static void main(String[] args){ EsempioThread e1=new EsempioThread(); e1.start()}} Listato 3.2. Metodo start() Cosı̀ come è stato avviato, è possibile fermare, sospendere e riprendere il flusso di esecuzione di un thread, attraverso i metodi riportati nel Listato 3.3. Tuttavia, tali metodi sono altamente sconsigliati, poichè agiscono in maniera “brutale” sul thread stesso, causando, nella maggior parte dei casi, il deadlock del sistema, a causa, ad esempio, del verificarsi di situazioni di “possesso e attesa”. 1 2 3 4 5 6 7 public class Main { public static void main(String args[]){ EsempioThread e1 = EsempioThread(); e1.suspend(); e1.resume(); e1.stop(); }} Listato 3.3. Metodi per la gestione del flusso di un thread Multithreading Un programma Java, quando viene avviato, presenta l’esecuzione del codice all’interno del metodo main(). La JVM istanzia un processo cui è associato un solo thread, ovvero il thread responsabile dell’esecuzione dello stesso metodo main(). Da tale considerazione risulta chiaro che se all’interno del metodo main() viene mandato in esecuzione anche solo un thread, in realtà, ne avremo in esecuzione almeno due. Il programma Java in esecuzione è, pertanto, un programma multithreading. Tali thread vengono eseguiti in parallelo, come se fossero due processi distinti. Come è semplice immaginare, la JVM permette l’esecuzione non solo di due singoli thread, ma anche, di più thread in parallelo. Su un singolo processore, il parallelismo è, in realtà, virtuale. I thread, infatti, vengono eseguiti dalla CPU uno alla volta. L’esecuzione in ordine di thread multipli su una singola CPU è detto scheduling. L’algoritmo di scheduling utilizzato da Java è il “fixed priority”. 38 3 Java vs Go: gestione della concorrenza I thread vengono, pertanto, schedulati in base alla loro priorità. Java permette, inoltre, la modifica di tali priorità per mezzo del metodo setPriority. Di conseguenza, se più thread sono pronti per l’esecuzione, la CPU viene occupata dal thread con priority maggiore. Se due o più thread hanno stessa priorità, l’algoritmo di scheduling segue un modello di tipo round-robin, ossia un algoritmo dove la coda è trattata in maniera circolare. L’algoritmo di scheduling in Java, inoltre, è di tipo preemptive: se, in un determinato istante, il thread con priorità massima diventa eseguibile, allora lo stesso viene scelto per occupare la CPU. Se i thread in parallelo non hanno accesso a risorse condivise, essi sono “asincroni” e possono, quindi, evolvere indipendentemente l’uno dall’altro. Ma se i thread hanno accesso una risorsa condivisa come, ad esempio, il case study di interesse, bisogna sincronizzare l’accesso alla sezione critica, ponendo la mutua esclusione e garantendo, pertanto, l’atomicità delle operazioni nella sezione critica stessa. Il problema può essere risolto attraverso il metodo synchronized: “A synchronized statement acquires a mutual-exclusion lock on behalf of the executing thread, executes a block, then releases the lock. While the executing thread owns the lock, no other thread may acquire the lock.”1 Il Listato 3.4 descrive un esempio di implementazione del metodo synchronized. 1 2 3 4 5 6 7 8 9 10 11 12 public synchronized void Deposit (float amount){ System.out.println(Deposito: "+amount+" euro."); setBalance(getBalance()+amount); notifyAll();} public synchronized void Withdraw (float amount)throws InterruptedException{ while(getBalance()<amount){ System.out.println(Ritiro: "+amount+" euro. SALDO INSUFFICIENTE!"); wait();} System.out.println(Ritiro: "+amount+" euro."); setBalance(getBalance()-amount); }} Listato 3.4. Il metodo synchronized All’interno di un oggetto che contiene delle sezioni sincronizzate è possibile notare i metodi wait(), notify() e notifyAll(), implementati nativamente nella classe Object. A gestire le code wait set ed entry set è, ancora una volta la JVM. • wait(): il thread che lo invoca viene sospeso, rilasciando il lock, e viene inserito nella coda wait set; • notify(): quando viene invocato, un thread viene estratto dalla wait set e inserito in entry set; • notifyAll(): tutti i thread della wait set vengono spostati nell’entry set. Tali metodi permettono la cooperazione dei thread, garantendo, pertanto, le operazioni multithreading. 1 R Language J. Gosling - B. Joy - G. Steele- G. Bracha - A. Buckley, The Java⃝ Specification Java SE 8 Edition, p.446 3.2 La programmazione concorrente in Java 39 Implementazione dell’interfaccia Runnable Esistono due metodi per implementare i thread. Il primo, trattato nelle sezioni precedenti, prevede di estendere la classe Thread. Tuttavia, l’estensione a tale classe può risultare problematica poichè Java non supporta l’ereditarietà multipla. Se, ad esempio, anzichè considerare le classi Deposit e Withdraw come classi separate, le considerassimo come sottoclassi della superclasse Transaction, non potremmo utilizzare i thread a loro associati. Java, pertanto, pone una “scappatoia” implementando i thread nell’interfaccia Runnable. Nel Listato 3.5 viene descritto un esempio. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class Transaction implements Runnable { private CurrentAccount CA; private float amount; private String type private static final int rpt=2000; private static final int delay=1; public Transaction(CurrentAccount cA, float amount, String type) { super(); CA = cA; this.amount = amount; this.type = type; } public void run () { try { for (int i=1;i<=rpt;i++){ if (type="Deposito) CA.Deposit(amount, i); if (type="Withdraw") CA.Withdraw sleep(delay); }} catch (InterruptedException exception){ } }} Listato 3.5. Esempio di utilizzo dell’interfaccia Runnable Per far partire l’interfaccia Runnable si procede in maniera simile a come si è visto con i metodi che estendono la classe Thread. 3.2.2 Implementazione del case study Dato il diagramma delle classi, per studiare il comportamento della programmazione concorrente si è deciso di considerare soltanto la classe CurrentAccount e le sue transazioni Read, Deposit, Withdraw e Transfer. Per prima cosa costruiamo le classi per la gestione delle transazioni relative alla lettura del saldo, al deposito, al ritiro, e al trasferimento, descritte nei Listati 3.6-3.9. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class ReadRun extends Thread{ private CurrentAccount CA; private int rpt; private int delay; public ReadRun(CurrentAccount cA, int rpt, int delay) { CA = cA; this.rpt = rpt; this.delay = delay; } public void run () { try { for (int i=1;i<=rpt;i++){ CA.ReadBalance(i); sleep(delay); }} catch (InterruptedException exception){ } }} 40 3 Java vs Go: gestione della concorrenza Listato 3.6. La classe ReadRun per la lettura del saldo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class DepositRun extends Thread{ private CurrentAccount CA; private float amount; private int rpt; private int delay; public DepositRun(CurrentAccount cA, float amount, int rpt, int delay) { CA = cA; this.amount = amount; this.rpt = rpt; this.delay = delay; } public void run () { try { for (int i=1;i<=rpt;i++){ CA.Deposit(amount, i); sleep(delay); }} catch (InterruptedException exception){ } }} Listato 3.7. La classe DepositRun per la gestione del deposito 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class WithdrawRun extends Thread{ private CurrentAccount CA; private float amount; private int rpt; private int delay; public WithdrawRun(CurrentAccount cA, float amount, int rpt, int delay) { CA = cA; this.amount = amount; this.rpt = rpt; this.delay = delay; } public void run () { try { for (int i=1;i<=rpt;i++){ CA.Withdraw(amount, i); sleep(delay); }} catch (InterruptedException exception){ } }} Listato 3.8. La classe WithdrawRun per la gestione dei prelievi 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class TransferRun extends Thread{ private CurrentAccount CAw; private CurrentAccount CAd; private float amount; private int rpt; private int delay; public TransferRun(CurrentAccount cAw, CurrentAccount cAd, float amount, int rpt, int delay) { CAw = cAw; CAd = cAd; this.amount = amount; this.rpt = rpt; this.delay = delay; } public void run (){ try { for (int i=1;i<=rpt;i++){ CAw.Withdraw(amount, i); CAd.Deposit(amount, i); sleep(delay); }} catch (InterruptedException exception){} }} Listato 3.9. La classe TransferRun per la gestione dei bonifici Successivamente, costruiamo la classe relativa al conto corrente (Listato 3.10). 3.2 La programmazione concorrente in Java 41 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class CurrentAccount { private String iban; private String number; private float balance; public CurrentAccount() {} public CurrentAccount(String iban, String number, float balance){ this.iban = iban; this.number = number; this.balance = balance; } public String getIban() { return iban;} public void setIban(String iban) { this.iban = iban;} public String getNumber() { return number;} public void setNumber(String number) { this.number = number;} public float getBalance() { return balance;} public void setBalance(float balance) { this.balance = balance;} public synchronized void ReadBalance (int i){ System.out.println(Thread.currentThread().getName()+"i="+i+ "}:Conto corrente n."+number+". Saldo: "+getBalance()+" euro."); } public synchronized void Deposit (float amount, int i){ ReadBalance(i); System.out.println(Thread.currentThread().getName()+"i="+i+ "}:Deposito: "+amount+" euro."); setBalance(getBalance()+amount); ReadBalance(i); notifyAll();} public synchronized void Withdraw (float amount, int i)throws InterruptedException{ while(getBalance()<amount){ ReadBalance(i); System.out.println(Thread.currentThread().getName()+"i="+i+ "}:Ritiro: "+amount+" euro. SALDO INSUFFICIENTE!"); wait();} ReadBalance(i); System.out.println(Thread.currentThread().getName()+"i="+i+ "}:Ritiro: "+amount+" euro."); setBalance(getBalance()-amount); ReadBalance(i);}} Listato 3.10. La classe CurrentAccount Infine, nel Listato 3.11, viene descritto il metodo main(). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class Main { public static final int rpt=2000; public static final int delay=1; public static void main(String[] args) { CurrentAccount CA1 = new CurrentAccount("ITK","A",10600); CurrentAccount CA2 = new CurrentAccount("SHK","B",11500); System.out.println("Conto corrente n."+CA1.getNumber()+". Saldo iniziale: "+CA1.getBalance()+ "euro\n"); System.out.println("Conto corrente n."+CA2.getNumber()+". Saldo iniziale: "+CA2.getBalance()+ "euro\n"); TransferRun t12=new TransferRun (CA1,CA2,200, rpt,delay); TransferRun t21=new TransferRun (CA2,CA1,1200, rpt,delay); ReadRun r1=new ReadRun(CA1, rpt, delay); ReadRun r2=new ReadRun(CA2, rpt, delay); DepositRun d1=new DepositRun(CA1,350, rpt, delay); DepositRun d2=new DepositRun(CA2,400, rpt, delay); WithdrawRun w1=new WithdrawRun(CA1,300, rpt, delay); WithdrawRun w2=new WithdrawRun(CA2,700, rpt, delay); t12.setName("{Bonifico, Conto corrente n."+CA1.getNumber()+"-> n."+CA2.getNumber()+", "); t21.setName("{Bonifico, Conto corrente n."+CA2.getNumber()+"-> n."+CA1.getNumber()+", "); r1.setName("{Lettura Saldo, Conto corrente n."+CA1.getNumber()+", "); r2.setName("{Lettura Saldo, Conto corrente n."+CA2.getNumber()+", "); d1.setName("{Deposito, Conto corrente n."+CA1.getNumber()+", "); d2.setName("{Deposito, Conto corrente n."+CA2.getNumber()+", "); 42 3 Java vs Go: gestione della concorrenza 34 35 36 37 38 39 40 w1.setName("{Ritiro, Conto corrente n."+CA1.getNumber()+", "); w2.setName("{Ritiro, Conto corrente n."+CA2.getNumber()+", "); r1.start();r2.start(); d1.start();d2.start(); w1.start();w2.start(); t12.start();t21.start(); } } Listato 3.11. La classe Main() per l’avvio di tutte le attività Andiamo a vedere nel dettaglio il comportamento delle classi riportate nei listati: • public class Main: Nel Listato 3.11 vengono, inizialmente, istanziati due oggetti della classe CurrentAccount. Tali oggetti possiedono come attributi iban, number (ovvero, il numero di conto corrente) e balance (ovvero, il saldo iniziale). Successivamente, si stampano gli oggetti per dare una prima lettura. Fatto ciò, si creano gli oggetti delle sottoclassi che estendono il thread e che servono per gestire le transazioni. In tali oggetti vengono inseriti, come attributi, rispettivamente: – In TransferRun: i due oggetti della classe CurrentAccount, l’importo da trasferire e le variabili static final rpt e delay, ossia il numero di ripetizioni e il tempo di sleep, espresso in millisecondi per ogni thread; – In DepositRun, WithdrawRun: un oggetto della classe CurrentAccount, l’importo e le variabili static final rpt e delay; – In ReadRun: Un oggetto della classe CurrentAccount e le variabili static final rpt e delay. Vengono, poi, rinominati i thread per avere una migliore lettura in fase di esecuzione. Infine i thread vengono avviati tramite il metodo .start(). • public class ReadRun extends Thread: nel Listato 3.6, dichiarate le variabili di ingresso, viene implementato il costruttore. Successivamente, viene inserito il codice nel metodo run(). Si è pensato di inserire il codice di esecuzione del thread in un ciclo for per far ripetere l’esecuzione rpt volte. Tutto il blocco viene poi inserito nel consueto try - catch, in modo da far dormire il thread per delay millisecondi. Quando il thread viene interrotto, l’eccezione viene catturata e il thread viene terminato. Nel metodo run() è presente, inoltre, un’invocazione al metodo ReadBalance della classe CurrentAccount, tramite l’oggetto CA, al quale verrà passata la variabile i per una migliore lettura del codice in fase di esecuzione. • public class DepositRun extends Thread: il funzionamento è pressochè identico alla ReadRun. Il metodo invocato è, in questo caso, il DepositRun, al quale verranno passate le variabili amount e i (Listato 3.7); • public class WithdrawRun extends Thread: identico al DepositRun. Il metodo invocato è il Withdraw, al quale saranno passati sia l’amount che i (Listato 3.8); • public class TransferRun extend Thread: In questo caso i metodi invocati saranno due. Verranno, infatti, invocati, rispettivamente, il metodo Withdraw per il mittente del bonifico e il metodo Deposit per il destinatario dello stesso. (Listato 3.9); • public class CurrentAccount: il Listato 3.10 descrive la classe CurrentAccount. Il saldo del conto corrente è l’oggetto della concorrenza. Implementati i co- 3.3 La programmazione concorrente in Go 43 struttori e i metodi get e set di tale classe, passiamo ad analizzare gli altri metodi. – ReadBalance: riceve in ingresso la variabile i, sia dalla classe ReadRun che dai metodi Deposit e Withdraw. In seguito, stampa il saldo del conto corrente; – Deposit: invoca il metodo ReadBalance per una prima stampa, facendo visualizzare all’utente lo stato iniziale. Successivamente, effettua il versamento tramite l’istruzione setBalance(getBalance()+amount); in seguito, reinvoca il ReadBalance. Infine, attraverso il metodo notifyAll(), risveglia i thread in attesa. – Withdraw: inizialmente vengono dichiarate le eccezioni del metodo, attraverso il comando throws InterruptedException. Finchè il saldo è minore dell’importo, il thread che lo ha invocato viene messo in attesa, tramite il metodo wait(). Viene in seguito risvegliato, attraverso il notifyAll() del metodo Deposit. Se il saldo è maggiore rispetto all’importo viene effettuato il ritiro tramite l’istruzione setBalance(getBalance()+amount). Analogalmente a quanto avviene per il metodoDeposit, il metodo Withdraw invoca, al bisogno, la lettura del saldo, attraverso il metodo ReadBalance. I metodi ReadBalance, Deposit e Withdraw sono sincronizzati tramite la presenza del metodo synchronized. Esso garantisce, pertanto, la mutua esclusione all’accesso alla sezione critica. Eseguendo il programma, avremo il risultato indicato in Figura 3.10 Figura 3.10. Esecuzione del programma 3.3 La programmazione concorrente in Go 3.3.1 Goroutine La programmazione concorrente, nel linguaggio di programmazione Go, si incentra sulle goroutine. Esse sono un’“astrazione” di un thread gestito dal Runtime di Go. Pur essendo i costrutti che garantiscono la programmazione concorrente, le goroutine sono completamente diverse rispetto ai thread visti in Java. Esse sono, sostanzialmente, funzioni in grado di agire in maniera concorrente ad altre funzioni. Infatti è, addirittura, possibile avere in esecuzione più goroutine su un singolo thread. Le goroutine sono assimilabili più al concetto di coroutine che di thread. 44 3 Java vs Go: gestione della concorrenza Tuttavia, a differenza delle coroutine, garantiscono la realizzazione di concorrenza e parallelismo virtuale e comunicano tramite i channel, meccanismo che verrà illustrato in dettaglio nella prossima sottosezione In generale, i thread vengono schedulati dal Sistema Operativo a livello di kernel. Tale operazione provoca in sequenza l’interruzione del thread corrente, il salvataggio del corrispettivo valore nei registri, l’applicazione del contest switch, etc.; tutto ciò comporta un notevole rallentamento del flusso di esecuzione. Il Go runtime implementa il proprio scheduler specializzato nella gestione delle goroutine concorrenti. A differenza dello scheduler a livello di kernel, il Go runtime non invoca, periodicamente, la schedulazione dei “processi leggeri”, ma ha in sè dei meccanismi che vengono attivati da alcune “keyword” del linguaggio di programmazione. È importante sottolineare che la concorrenza di cui parliamo non implica necessariamente il parallelismo; nel caso “base”, non abbiamo un uso di più core simultaneamente, ma solo uno è fisicamente dedicato al programma, e solo una funzione per volta può essere eseguita; tuttavia, esiste una concorrenza nella loro esecuzione. Come detto nel capitolo precedente, Go non è ottimizzato per la programmazione in parallelo. “The number of CPUs available simultaneously to executing goroutines is controlled by the GOMAXPROCS shell environment variable. In earlier releases of Go, the default value was 1, but as of Go 1.5 the default value is the number of cores available. Therefore programs compiled after 1.5 should demonstrate parallel execution of multiple goroutines.”2 Affinchè sia realizzato pienamente il parallelismo è necessario agire, pertanto, sulla variabile GOMAXPROCS che gestisce il numero di threads a livello di sistema operativo in grado di eseguire codice Go a livello utente. La programmazione concorrente, in Go, viene effettuata, come detto in precedenza, tramite le goroutine e i channel. L’implementazione delle goroutine, nel codice, è abbastanza semplice (Listato 3.12). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 2 package main import ( "fmt" "time" ) type CurrentAccount struct { iban string number string balance float64 } func Deposit(S string, amount float64, balance float64) { for i := 0; i < 5; i++ { time.Sleep(1 * time.Millisecond) balance += amount fmt.Print(S, ": Il saldo è ", balance, "\n") } } func main() { CA1 := CurrentAccount{"ITkkABBBBBCCCCCXXXXXX", "12345", 1000} fmt.Println("Conto corrente n.", CA1.number, ",IBAN:",CA1.iban, ", Saldo:", CA1.balance) var amount float64 amount = 5000 var Go string Go = "Esempio Goroutine" var No string No = "Senza Goroutine" var tmp = CA1.balance https://golang.org/doc/faq/#Concurrency 3.3 La programmazione concorrente in Go 28 29 go Deposit(Go, amount, tmp) Deposit(No, amount, tmp) } 45 Listato 3.12. Esempio di goroutine Formalmente le goroutine vengono “lanciate”, semplicemente, dalla keyword go seguita dall’invocazione di una funzione. Nel Listato 3.12, la funzione Deposit esegue la somma algebrica fra l’importo e il saldo del conto corrente CA1. In questo listato la funzione viene invocata due volte, poichè le goroutine, come detto precedentemente, devo essere eseguite in concorrenza con altre funzioni. Quando l’esecuzione della funzione è ultimata, la goroutine termina semplicemente. Il risultato finale è indicato in Figura 3.11 Figura 3.11. Risultato del Listato 3.12 Le goroutine hanno, inoltre, un’altra particolare proprietà. A differenza dei thread di Java, quando il main ha terminato il suo compito, il programma non attende la terminazione delle goroutine e, di conseguenza, esse vengono chiuse “brutalmente”. 3.3.2 Channel Per avere un’architettura concorrente sono necessari metodi di comunicazione tra i vari processi. Per tale motivo si è resa necessaria l’implementazione dei channel. I channel sono, essenzialmente, dei condotti tipati che mettono in comunicazione due funzioni, in esecuzione contemporanea, sia per sincronizzarne l’esecuzione stessa, sia per la ricezione o l’invio di variabili. Il canale viene inizializzato attraverso la primitiva make, che specifica il canale in base al tipo di valori che è in grado di veicolare. Opzionalmente, può essere specificata una capacità, ossia un buffer di memoria che può contenere delle variabili. L’accesso al buffer segue una modalità di tipo FIFO (First In, First Out). Se la capacità è maggiore di zero, il channel sarà, allora, asincrono; in caso contrario, esso sarà sincrono (Listati 3.13, 3.14). Nel caso di canale asincrono e nelle condizioni di buffer non pieno e non vuoto, le operazioni di comunicazione hanno esito positivo senza bloccarsi. Se la capacità è pari a zero o, in maniera del tutto equivalente, è assente, la comunicazione ha successo solo quando mittente e destinatario sono, effettivamente, sincronizzati. 46 3 Java vs Go: gestione della concorrenza 1 depositchan:= make(chan Deposit) Listato 3.13. Esempio di canale sincrono 1 cj := make(chan int, 110) Listato 3.14. Esempio di canale asincrono Nel Listato 3.15, il canale riferisce a un particolare tipo di dato, nel nostro caso, al dato Deposit 1 2 3 4 5 6 7 func PipeDeposit(depositchan chan Deposit){ var val Deposit for{ val= <-depositchan val.conto.deposito(val.amount) } } Listato 3.15. Gestore di un canale sincrono Quando un channel viene “passato” come parametro di funzione, è possibile specificare se il esso è specializzato soltanto nella ricezione o nella trasmissione di variabili. L’operatore, a forma di freccia, riportato nel listato 3.16 specifica la direzione del canale. Se nessuna direzione viene specificata, allora il canale è considerato bidirezionale. L’utilizzo dell’operatore aumenta notevolmente la type-safety del programma in esecuzione. Possiamo, pertanto, immaginare la func dove viene mandato in ingresso un canale come un oggetto che prevede, essenzialmente, i metodi di ricezione e tramissione. I channel supportano anche l’operazione di chiusura. 1 2 depositchan<- val // invio di un valore val=<-depositchan //ricezione di un valore Listato 3.16. Direzione del canale 3.3.3 Implementazione del codice Nel Listato 3.17 viene riportata una possibile implementazione, in codice, del case study di interesse. Come spiegato in precedenza, le struct non contengono al loro interno metodi e funzioni; pertanto, possiamo definire, inizialmente, la struct CurrentAccount esplicitando solo gli attributi. Nel main() saranno subito instanziate le struct CA1 e CA2. Subito dopo si “lanciano” legoroutine tramite il comando go func(); l’istruzione è inserita in un ciclo for in modo da tentare di ripetere l’operazione un numero specificato di volte (nel caso considerato, 2000 per ogni transazione). Per poter creare i canali relativi alle transazioni, affianchiamo al CurrentAccount le struct DepositStruct, WithdrawStruct e TransferStruct. Esse conterranno le informazioni sui conto correnti che hanno avviato la procedura e sull’importo da versare. La creazione di tali struct garantirà che i dati non verranno mai corrotti da accessi concorrenti. 3.3 La programmazione concorrente in Go 47 All’interno del ciclo sono racchiuse le invocazioni di funzioni relative alle transazioni, ossia: • • • • DepositRun: essa è la func relativa al deposito; ricevuti come parametri formali le variabili w, per poter tenere traccia dell’operazione, e amount, per indicare l’importo da depositare, crea un canale di DepositChannel attinente alla struct DepositStruct. Successivamente, stampa il saldo e istanzia la struct relativa alla transazione da eseguire. Avvia, pertanto, la goroutine della PipeDeposit; essa è una funzione intermedia che funge da collegamento fra la struct DepositStruct e quella CurrentAccount. Allo stesso tempo si permette il passaggio dei valori, specificando la direzione del canale. La PipeDeposit, infine, invoca la funzione Deposit , per poter eseguire la stessa transazione. WithdrawRun: il funzionamento è identico alla DepositRun; TransferRun: questa func riceve come parametri formali sia le variabili w e amount che la struct CurrentAccount. La PipeTransfer, in questo caso, invoca le funzioni Deposit e Withdraw relative, rispettivamente, al beneficiario e al donatore. ReadRun: è la func relativa alla lettura del saldo. Essa non riceve parametri formali, ad eccezione della variabile w, per una maggiore lettura del risultato. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 package main import ( "fmt" "time" ) func (Car *CurrentAccount) ReadRun(w int) { ReadChannel := make(chan CurrentAccount) fmt.Println("GoSaldo - n◦ ", w) Car.ReadBalance() go PipeRead(ReadChannel) } func (CAw *CurrentAccount) WithdrawRun(a float64, w int) { WithdrawChannel := make(chan WithdrawStruct) fmt.Println("GoRitiro - Conto corrente n.", CAw.number, "n◦ ", w, "importo:", a) CAw.ReadBalance() withdraw := WithdrawStruct{amount: a, CA: CAw} go PipeWithdraw(WithdrawChannel) WithdrawChannel <- withdraw CAw.ReadBalance() } func (CAd *CurrentAccount) DepositRun(a float64, w int) { DepositChannel := make(chan DepositStruct) fmt.Println("GoDeposito - Conto corrente n.", CAd.number,"Deposito n◦ ", w, "importo:", a) CAd.ReadBalance() deposit := DepositStruct{amount: a, CA: CAd} go PipeDeposit(DepositChannel) DepositChannel <- deposit CAd.ReadBalance() } func (CAwt *CurrentAccount) TransferRun(a float64, CAdt *CurrentAccount, w int) { TransferChannel := make(chan TransferStruct) fmt.Println("GoBonifico - Conto corrente n.", CAwt.number,"-> n.", CAdt.number, "n◦ ", w, "importo", a) transfer := TransferStruct{CAw: CAwt, CAd: CAdt, amount: a} go PipeTransfer(TransferChannel) TransferChannel <- transfer CAwt.ReadBalance() CAdt.ReadBalance() } type CurrentAccount struct { iban string number string balance float64 } type TransferStruct struct { CAw *CurrentAccount CAd *CurrentAccount amount float64 } type DepositStruct struct { amount float64 CA *CurrentAccount } type WithdrawStruct struct { amount float64 CA *CurrentAccount } 48 3 Java vs Go: gestione della concorrenza 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 func PipeRead(CAChannel chan CurrentAccount) { var val CurrentAccount for { val = <-CAChannel val.ReadBalance() } } func PipeDeposit(DepositChannel chan DepositStruct) { var val DepositStruct for { val = <-DepositChannel val.CA.Deposit(val.amount) } } func PipeWithdraw(WithdrawChannel chan WithdrawStruct) { var val WithdrawStruct for { val = <-WithdrawChannel val.CA.Withdraw(val.amount) } } func PipeTransfer(TransferChannel chan TransferStruct) { var val TransferStruct for { val = <-TransferChannel val.CAw.Transfer(val.CAd, val.amount) } } func (CA *CurrentAccount) Withdraw(amount float64) { fmt.Println("Ritiro:", amount) if amount > CA.balance { fmt.Println("SALDO INSUFFICIENTE") } else { CA.balance -= amount } } func (CA *CurrentAccount) ReadBalance() { fmt.Println("Conto corrente n.", CA.number, "; Saldo:", CA.balance) } func (CA *CurrentAccount) Deposit(amount float64) { fmt.Println("Deposito:", amount) CA.balance += amount } func (CAw *CurrentAccount) Transfer(CAd *CurrentAccount, amount float64) { if amount > CAw.balance { fmt.Println("SALDO INSUFFICIENTE") } else { CAw.Withdraw(amount) CAd.Deposit(amount) } } func main() { CA1 := CurrentAccount{iban: "ITK1", number: "A", balance:10000} CA2 := CurrentAccount{iban: "EH21", number: "B", balance:20000} for w := 1; w < 2000; w++ { go func() { for { CA1.WithdrawRun(500.0, w) CA2.WithdrawRun(200.0, w) CA2.ReadRun(w) CA1.DepositRun(400.0, w) CA2.DepositRun(300.0, w) CA2.TransferRun(10.0, &CA1, w) CA1.ReadRun(w) CA1.TransferRun(60.0, &CA2, w) } }() time.Sleep(1 * time.Millisecond) } } Listato 3.17. Implementazione del codice Il listato produrrà il risultato riportato in Figura 3.12 Figura 3.12. Risultato del Listato 3.17. 3.4 Conclusioni 49 3.4 Conclusioni Grazie agli esempi mostrati è possibile effettuare alcune considerazioni sulla programmazione concorrente. Go, pur essendo stato principalmente creato per velocizzare questo meccanismo, non sembra garantire le prestazioni migliori. All’aumentare delle goroutine e, quindi, dei “processi leggeri”concorrenti è possibile accorgersi che la programmazione concorrente mostra un significativo degrado delle prestazioni quando il numero delle goroutine supera il numero di processori che il calcolatore mette a disposizione. Il problema principale sembra essere lo scheduler. Esso è gestito dal Go Runtime, il quale, non presentando un algoritmo ben definito come quello presente nella schedulazione dei thread, non garantisce la gestione ottimale delle potenzialmente numerosissime goroutine. La Versione 1.5 ha provato a migliorare le prestazioni dello scheduler, implementando la variabile di ambiente GOMAXPROCS, in modo tale da impostare uno scheduler su un numero di processori pari a quelli posseduti, effettivamente, dal calcolare. Lo scheduling, tuttavia, per stessa ammissione del sito ufficiale: “is not as good as it needs to be”3 Nel case study considerato si è, infatti, cercato di stressare il più possibile il sistema, svolgendo 8000 ripetizioni del processo e un tempo di riposo pari a 1 ms. Le goroutine non hanno ancora espresso le loro potenzialità e sembrano essere ben lontani dal mantenere le promesse degli sviluppatori del linguaggio. Pertanto, sembra inutile poter far “girare” decine di goroutine su ogni thread, se già, per le specifiche tecniche del calcolatore utilizzato, poche decine di esse possono rallentare notevolmente le prestazioni del sistema. Sfruttando il semplice algoritmo riportato nel Listato 3.18, si è potuto notare, infatti, come Java, a differenza di Go, riesca ad ottenere un tempo d’esecuzione inferiore ai 2,8 secondi, pur non essendo un linguaggio compilato, e pur presentando la JVM e un contest-switch dei processi invadente a livello di I/O. 1 2 3 4 long inizio = System.currentTimeMillis(); long fine = System.currentTimeMillis(); long tempo=(fine-inizio); System.out.println ("Tempo di esecuzione ="+tempo+ " ms\n"); Listato 3.18. Algoritmo per il calcolo del tempo di esecuzione in Java Utilizzando un algoritmo simile in Go (Listato 3.19), abbiamo ottenuto risultati ben diversi. 1 2 3 start := time.Now() elapsed := time.Since(start) fmt.Println("Tempo di esecuzione=:", elapsed) Listato 3.19. Algoritmo per il calcolo del tempo di esecuzione in Go 3 https://golang.org/doc/faq#Why GOMAXPROCS 50 3 Java vs Go: gestione della concorrenza Il tempo di esecuzione del programma è riuscito a raggiungere un minimo di 3,5 secondi, ossia un tempo maggiore del 25% rispetto a quello registrato da Java. Seppur nel case study considerato, Golang non si dimostra “performante” nei tempi di esecuzione, lo è sicuramente nelle tecniche di implementazione. Interessantissima sembra la gestione dei channel condivisi. Essi, oltre a permettere la comunicazione fra le goroutine, pongono, in backgroud, meccanismi di lock per serializzare l’accesso alle sezioni critiche e garantire, pertanto, la thread safety. Siamo sicuri che, in futuro, risolti i problemi dello scheduler, Go possa soddisfare le aspettative richieste. 4 Java vs Go: la gestione delle funzioni In questo capitolo si illustrerà la gestione delle funzioni in Java e in Go. In particolare, verranno trattati il polimorfismo dei metodi, le funzioni anonime e i return multipli. 4.1 Il concetto di sottoprogramma Nelle stesura di un programma è possibile riscontrare problemi simili in porzioni del codice diverse. Una soluzione potrebbe essere replicare l’algoritmo risolutivo nelle porzioni di codice dove è presente il medesimo problema. Tale soluzione, seppur appare la più ovvia, non si rivela essere la più efficiente. Un eventuale errore nella stesura dell’algoritmo potrebbe portare, infatti, in “tilt” tutto il sistema. Inoltre, un eventuale aggiornamento dell’algoritmo renderebbe obbligatorio l’aggiornamento di tutte le sue repliche. Una soluzione migliore è quella di realizzare un sottoprogramma costituito da un algoritmo, separato dal flusso di istruzioni principale, dedicato alla risoluzione di sottoproblemi simili o identici. Il programma, pertanto, potrà invocare il sottoprogramma qualora siano necessarie le funzionalità di quest’ultimo. Tramite tale meccanismo è possibile garantire l’efficienza, la manutenzione, il riuso e, soprattutto, la facilità di programmazione. Per poter definire la funzione del sottoprogramma è necessario, innanzitutto, dichiarare il nome della funzione, le variabili che devono essere poste in ingresso e il risultato che si vuole restituire. Una volta dichiarata la funzione, quest’ultima deve essere implementata; si stila, pertanto, l’algoritmo di risoluzione del sottoproblema. La gestione di tali funzioni è proprio l’argomento principale del presente capitolo. 4.2 Gestione delle funzioni in Java Nei linguaggi che supportano il paradigma OOP, la gestione dei sottoproblemi viene affidata ai concetti di oggetti e di classe. Questi ultimi, a loro volta, utilizzano i metodi per risoluzione degli stessi sottoproblemi. 52 4 Java vs Go: la gestione delle funzioni 4.2.1 I metodi Come già accennato, nel primo capitolo, i metodi sono le funzionalità che vengono messe a disposizione da una particolare classe. Per poter meglio comprendere il concetto di metodo, si consideri la classe CurrentAccount riportata nel Listato 4.1. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class CurrentAccount { private String iban; private String number; private float balance; public CurrentAccount() {} public CurrentAccount(String iban, String number, float balance) { this.iban = iban; this.number = number; this.balance = balance; } public String getIban() { return iban; } public void setIban(String iban) { this.iban = iban; } public String getNumber() { return number; } public void setNumber(String number) { this.number = number; } public float getBalance() { return balance; } public void setBalance(float balance) { this.balance = balance; } } Listato 4.1. Classe CurrentAccount All’interno della stessa classe viene definito il metodo Withdraw (Listato 4.2). La definizione di un metodo, in Java, è molto simile alla definizione di variabile, con la quale ha in comune numerose caratteristiche. 1 public float Withdraw (float amount) Listato 4.2. Definizione del metodo Withdraw Nella definizione, il termine Withdraw è detto identificatore. I parametri racchiusi tra parentesi tonde, subito dopo l’identificatore, sono definiti parametri formali ; essi descrivono l’insieme di variabili che il blocco di codice riceverà in ingresso dal programma chiamante, per poter portare a termine l’algoritmo risolutivo. Nell’esempio considerato, il metodo riceve in ingresso la variabile amount di tipo float. L’ identificatore e i parametri formali costituiscono la cosiddetta “firma” del metodo. Dopo l’elaborazione delle variabili formali, il metodo restituirà, una variabile d’uscita. Il tipo di variabile d’uscita è detta tipo di ritorno; esso è il termine prima della “firma” e nel caso considerato il tipo di ritorno è float. Terminata la fase di definzione, è possibile passare alla fase di implementazione, ossia alla stesura del codice che elaborerà i parametri formali (Listato 4.3). In tale listato è possibile notare la dichiarazione, all’interno del metodo stesso, della varibiale tmp di tipo float. Le variabili dichiarate all’interno del metodo vengono definite parametri attuali. L’algoritmo del metodo si arresta quando esso incontra la keyword return. La variabile specificata subito dopo la parola chiave deve essere dello stesso tipo del tipo di ritorno, specificato in fase di definizione. 4.2 Gestione delle funzioni in Java 53 1 2 3 4 5 6 7 8 9 public float Withdraw (float amount){ System.out.println("Ritiro: "+amount+" euro."); float tmp; if (getBalance()<amount){ System.out.println("Il saldo è insufficiente. Operazione annullata"); tmp=getBalance();} else {tmp = getBalance()-amount; setBalance(tmp);} return tmp;} Listato 4.3. Implementazione del metodo Withdraw Esaminando il Listato 4.3 è possibile effettuare un’altra considerazione; nella struttura di controllo if, il metodo assegna al parametro tmp un valore ottenuto invocando il metodo getBalance() della classe CurrentAccount, riportato, precedentemente, nel Listato 4.1. Il metodo get(), a differenza del metodo Deposit, non riceve in ingresso alcun parametro formale restituendo, in uscita, il float desiderato, ossia il saldo del conto corrente. Viene invocato, successivamente, il metodo setBalance(tmp). Quest’ultimo riceve in ingresso il parametro formale tmp, ma non presenta variabili in uscita. Oltre a metodi dove sono assenti i parametri formali, è, pertanto, possibile, definire dei metodi che non presentano tipo di ritorno, ma eseguono semplicemente delle istruzioni. I metodi presenti in una classe possono essere invocati anche all’esterno delle classe di appartenenza. Ad esempio, invochiamo il metodo Deposit all’interno del main(), come mostrato nel Listato 4.4. 1 2 3 4 5 6 7 8 9 public class Main { public static void main(String[] args) { CurrentAccount CA1 = new CurrentAccount("ITK", "A",10600); float amount1=500; float amount2=12000; float tmp1=CA1.Withdraw(amount1); System.out.println("Saldo: "+tmp1); float tmp2=CA1.Withdraw(amount2); System.out.println("Saldo: "+tmp2); }} Listato 4.4. Invocazione del metodo Withdraw L’accesso al metodo Withdraw avviene tramite l’operatore “dot”. La struttura sintattica è identica all’accesso alle variabili di un oggetto. Pertanto, sia i metodi, sia gli attributi definiti public di una classe sono accessibili tramite tale operatore. Quando viene avviato il programma descritto dal Listato 4.4, la macchina virtuale esegue, inizialmente, le istruzioni del metodo main() in maniera sequenziale. Quando viene dichiarata la variabile tmp1, essa viene inizializzata tramite l’invocazione al metodo Withdraw. La macchina virtuale, alla chiamata al metodo, esegue un “salto” alla parte di codice contenuta nel metodo stesso. Essa inizializza, di conseguenza, i parametri formali ed esegue le istruzioni del sottoprogramma finchè non incontra la parola chiave return. A questo punto, essa effettua un nuovo “salto” per tornare all’esecuzione del programma e assegna al valore tmp1 il valore di ritorno del metodo Withdraw. Il risultato dell’operazione riportata nel Listato 4.4 è indicato in Figura 4.1. 54 4 Java vs Go: la gestione delle funzioni Figura 4.1. Risultato ottenuto dal Listato 4.4 4.2.2 Polimorfismo per metodi I metodi possono essere poliformi. Il polimorfismo è un concetto che è stato importato nella OOP dalla realtà di tutti i giorni. Esso può essere definito come la facoltà di assumere aspetti e forme differenti a seconda delle circostanze. Il polimorfismo per metodi consente, pertanto, di utilizzare lo stesso identificatore per funzionalità che risolvono problematiche differenti. Esso può essere realizzato, sostanzialmente, tramite due configurazioni: • Overload, ossia sovraccarico. • Override, ossia riscrittura. Overload L’overload è il polimorfismo che sfrutta la definizione dei metodi. In Java, infatti, i metodi sono univocamente determinati dalle loro “firme”, ossia dall’insieme di parametri formali e dall’identificatore. Pertanto, come è ovviamente possibile definire metodi con un diverso identificatore, ma identici parametri formali, è possibile definire, anche all’interno di una stessa classe, metodi con lo stesso identificatore, ma parametri formali ben distinti. Per una maggiore lettura del codice, è preferibile utilizzare il polimorfismo per metodi che hanno, concettualmente, identiche funzionalità, ma implementazioni differenti. Facciamo qualche esempio. Consideriamo la classe CurrentAccount, riportata nel Listato 4.5. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class CurrentAccount { private String iban; private String number; private float balance; public CurrentAccount() {} public CurrentAccount(String iban, String number, float balance) { this.iban = iban; this.number = number; this.balance = balance; } public String getIban() { return iban; } public void setIban(String iban) { this.iban = iban; } public String getNumber() { return number; } public void setNumber(String number) { this.number = number; } public float getBalance() { return balance; 4.2 Gestione delle funzioni in Java 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 55 } public void setBalance(float balance) { this.balance = balance; } public float Deposit (float amount){ System.out.println("Deposito: "+amount+" euro."); float tmp; tmp = getBalance()+amount; setBalance(tmp); return tmp;} public float Withdraw (float amount){ System.out.println("Ritiro: "+amount+" euro."); float tmp; if (getBalance()<amount){ System.out.println("Il saldo è insufficiente. Operazione annullata"); tmp=getBalance();} else{ tmp = getBalance()-amount; setBalance(tmp); return tmp;}}} Listato 4.5. CurrentAccount senza polimorfismo Supponiamo che l’operazione di ritiro, svolta in una banca differente da quella di appartenenza del conto corrente, sia automaticamente accompagnata da un costo di transazione non trascurabile. Avremo, allora, bisogno di un metodo poliforme con stessa identificazione, ma parametri formali differenti. L’esempio di polimorfismo di tipo overload è riportato nel Listato 4.6. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public float Withdraw (float amount){ System.out.println("Ritiro: "+amount+" euro."); float tmp; if (getBalance()<amount){ System.out.println("Il saldo è insufficiente. Operazione annullata"); tmp=getBalance();} else {tmp = getBalance()-amount; setBalance{tmp};} return tmp;} public float Withdraw (float amount, float cost){ System.out.println("Ritiro: "+amount+" euro."); float tmp; if (getBalance()<amount+cost){ System.out.println("Il saldo è insufficiente. Operazione annullata"); tmp=getBalance();} else {tmp = getBalance()-amount-cost; setBalance{tmp};} return tmp;} Listato 4.6. Esempio di polimorfismo di tipo overload Java presenta, nativamente, numerosi metodi che implementano un polimorfismo di tipo overload. Fra questi, degno di nota è il metodo println(). Con l’identificatore println, infatti, non viene indicato un unico metodo, ma una famiglia di metodi con stesso identificatore, appunto println(), alla quale è possibile passare stringhe, array, int, etc.; i metodi println() poliformi sono dieci e sono descritti nella Figura 4.2. 56 4 Java vs Go: la gestione delle funzioni Figura 4.2. Polimorfismo di tipo overload per i metodi println() Override L’override è ritenuto uno dei maggiori punti di forza del linguaggio di programmazione Java. Tale meccanismo, come può suggerire il nome, è la caratteristica di una sottoclasse di poter riscrivere un metodo ereditato dalla superclasse. Come è possibile intuire, una classe può utilizzare il polimorfismo di tipo override se, e solo se, estende un’altra classe; pertanto, l’override implica il concetto di ereditarietà. Cerchiamo di capire meglio il polimorfismo di tipo override; supponiamo che un conto corrente possa essere di tipo fruttifero e, pertanto, matura un certo interesse annuale. Creiamo, allora, la sottoclasse InterestBearingAccount (Listato 4.7) della superclasse CurrentAccount (Listato 4.5). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class InterestBearingAccount extends CurrentAccount { private float percentage; private year; public float getPercentage() { return percentage; } public int getYear() { return year; } public void setPercentage(float percentage) { this.percentage = percentage; } public void setYear(int year) { this.year = year; } public InterestBearingAccount() { super();} public InterestBearingAccount(String iban, String number, float balance, float percentage, int year) { super(iban, number, balance); this.percentage = percentage; this.year=year; } } Listato 4.7. Classe InterestBearginAccount Dichiariamo, pertanto, un metodo che permette di calcolare, in euro, l’interesse maturato e importiamo i metodi della superclasse (Listato 4.8). 1 2 public class InterestBearingAccount extends CurrentAccount { private float percentage; 4.2 Gestione delle funzioni in Java 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 57 private int year; public float getPercentage() { return percentage; } public int getYear() { return year; } public void setYear(int year) { this.year = year; } public void setPercentage(float percentage) { this.percentage = percentage; } public InterestBearingAccount() { super(); // TODO Auto-generated constructor stub } public InterestBearingAccount(String iban, String number, float balance, float percentage, int year) { super(iban, number, balance); this.percentage = percentage; this.year=year; } @Override public float Deposit(float amount) { // TODO Auto-generated method stub return super.Deposit(amount); } @Override public float Deposit(float amount, float transactioncost) { // TODO Auto-generated method stub return super.Deposit(amount, transactioncost); } @Override public float Withdraw(float amount) { return super.Withdraw(amount); } @Override public float Withdraw(float amount, float cost) { return super.Withdraw(amount, cost); } public float increment() { float C=(balance*percentage*year)/100; System.out.println("L’interesse maturato è: "); return (C); }} Listato 4.8. Metodi ereditati dalla superclasse I metodi Withdraw della superclasse non considerano l’interesse maturato; pertanto, utilizzeremo il polimorfismo di tipo override per meglio “adattare” il metodo alla sottoclasse InterestBearingAccount (Listato 4.9). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Override public float Withdraw(float amount) { balance+=increment(); System.out.println("Saldo maturato:"+balance); return super.Withdraw(amount); } @Override public float Withdraw(float amount, float cost) { balance+=increment(); System.out.println("Saldo maturato:"+balance); return super.Withdraw(amount, cost); } public float increment() { float C=(balance*percentage*year)/100; System.out.println("L’interesse maturato è: "+C); return (C); }} Listato 4.9. Override dei metodi Quando l’oggetto appartenente alla sottoclasse InterestBearingAccount invoca uno dei metodi Withdraw, sarà prima calcolato, in euro, il tasso di inte- 58 4 Java vs Go: la gestione delle funzioni resse e aggiornato il saldo. Successivamente si procederà con l’algoritmo del metodo Withdraw specificato nella superclasse. Il metodo main(), strutturato come riportato nel Listato 4.10, produrrà il risultato descritto in Figura 4.3. 1 2 3 4 5 6 public class Main { public static void main(String[] args) { CurrentAccount CA2= new InterestBearingAccount("KI","B",5100, 22, 8); float amount=12000; float tmp=CA2.Withdraw(amount); System.out.println("Saldo: "+tmp);}} Listato 4.10. Override: main() Figura 4.3. Risultato del metodo in override Esistono regole precise per l’implementazione del polimorfismo di tipo override; più specificatamente, esse riguarderanno: • La “firma”: il metodo override appartenente alla sottoclasse deve avere la stessa “firma” del metodo contenuto nella superclasse. • Il tipo di ritorno: i tipi di ritorno del metodo della superclasse e della sottoclasse devono coincidere. • Gli indicatori di visibilità: il metodo della sottoclasse deve avere un indicatore di visibilità pari o inferiore al metodo della superclasse. 4.3 Gestione delle funzioni in Go In Go la risoluzione dei sottoproblemi viene gestita in una forma “ibrida” fra la programmazione OOP e quella procedurale. Go presenta, infatti, le tipiche funzioni della programmazione procedurale che, però, possono essere “collegate” alle strutture, tramite i puntatori, realizzando una gestione delle funzioni notevolmente simile ai metodi delle classi in Java. 4.3.1 Le funzioni La definizione delle func è piuttusto simile a quella presente nei metodi di Java (Listato 4.11). 1 2 3 func Deposit (amount float64 , balance float64) float64{ balance+=amount return balance} 4.3 Gestione delle funzioni in Go 59 Listato 4.11. Definizione e implementazione di una func La funzione è introdotta dalla parola chiave func. Subito dopo trovano spazio l’identificatore e i parametri formali, che, come in Java, racchiudono la “firma” della funzione stessa. Nella definizione è, infine, presente il tipo di ritorno. La parentesi graffa introduce l’algoritmo per la soluzione del problema. A differenza di Java, essa deve essere scritta, obbligatoriamente, nella stessa linea della parola chiave func. Data l’assenza di ereditarietà, il linguaggio Go non permette, tipicamente, il polimorfismo, sia di tipo override che di tipo overload. Come in Java è, invece, possibile avere funzioni dove i parametri formali e/o il tipo di restituzione sono assenti. Le modalità con cui si può richiamare una funzione sono mostrate nel Listato 4.12. 1 2 3 4 5 6 7 8 func main(){ ca1:=CA {iban:"A",number:"A",balance:5} var c = ca1.balance var amount float64 amount=100 var d = Deposit(amount, c) ca1.balance=d fmt.Println("saldo: ",ca1.balance)} Listato 4.12. Modalità con cui si può richiamare una funzione in Go Come detto in precedenza, è possibile “collegare” una funzione a una struct, rendendo le definizioni di struttura e di funzione concetti molto simili alle definizioni di classi e di metodi (Listato 4.13). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package main import "fmt" type CurrentAccount struct { iban string number string balance float64 } func (CA1 *CurrentAccount) Deposit(amount float64){ fmt.Println("Deposito:", amount) CA1.balance+=amount fmt.Println("Conto corrente n.",CA1.number, ",IBAN:",CA1.iban,", Saldo:",CA1.balance ) } func main() { CA1 :=CurrentAccount{"ITkkABBBBBCCCCCXXXXXX","12345",1000} fmt.Println("Conto corrente n.",CA1.number, ",IBAN:",CA1.iban,", Saldo:",CA1.balance ) var amount float64 amount=2000 CA1.Deposit(amount) } Listato 4.13. Struct “collegata” a una func I puntatori hanno un funzionamento nettamente diverso dai valori. Nel Listato 4.14 utilizzeremo la funzione valzero, che assegna, per l’appunto, il valore 0 alla variabile e la funzione pntzero, che assegna il valore 0 al puntatore della variabile. Alla funzione valzero viene assegnato come parametro formale un dato float64, mentre alla funzione pntzero viene assegnato un puntatore. Il passaggio per riferimento è garantito dal parametro “&”. Le due funzioni non presentano un tipo di ritorno. Quando viene invocata valzero(amount), si porrà, pertanto, a zero soltanto la variabile attuale. Di conse- 60 4 Java vs Go: la gestione delle funzioni guenza i valori di amount, prima e dopo la func, coincidono e sono pari sempre a 165. Quando, invece, viene invocata la funzione pntzero(amount), le cose si fanno più interessanti; poichè sia la variabile formale che la variabile attuale sono riferite dal medesimo puntatore, quando il suo valore viene posto al valore nullo, entrambe le variabili saranno poste pari a 0. L’ultima istruzione fmt.Println(), dà accesso all’indirizzo di memoria dove riferisce il puntatore. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package main import "fmt" func valzero(amountval float64) { amountval = 0 } func pntzero(amountpnt *float64) { *amountpnt = 0 } func main() { var amount float64 amount = 165 fmt.Println("iniziale: ", amount) valzero(amount) fmt.Println("valzero: ", amount) pntzero(&amount) fmt.Println("pntzero: ", amount) fmt.Println("puntatore di amount:", &amount) } Listato 4.14. Valori e puntatori 4.3.2 Le funzioni anonime Go suppporta, nativamente, le funzioni anonime. Tali costrutti, implementati nel Java solo nell’ultima versione, promettono di avvicinare il programmatore alle caratteristiche tipiche della programmazione funzionale. Una funzione anonima (o lambda) non è una funzione standard e presenta, essenzialmente, due caratteristiche: • assenza di identificazione; • assenza di invocazione Tali peculiarità rendono, di fatto, anonima la funzione, nel vero senso della parola. La funzione pur essendo anonima, non ha esistenza autonoma. Essa, pertanto, deve essere attribuita ad una variabile, oppure devono essere implementati, e non solo definiti, i parametri formali. Nel Listato 4.15 è presente un esempio di attribuzione a una variabile withdraw di funzione anonima. 1 2 3 4 5 6 7 8 package main import "fmt" func main() { withdraw:= func (amount float64, balance float64) float64 { return (balance-amount) } fmt.Println(withdraw(341.4, 15072.0)) fmt.Println(withdraw(124.9, 4333.2)) } Listato 4.15. Variabile e funzione anonima 4.3.3 Il return multiplo Go è uno dei pochissimi linguaggi di programmazione in grado di supportare il “return multiplo” delle funzioni. 4.3 Gestione delle funzioni in Go 61 Tale funzionalità può rivelarsi uno strumento fondamentale per aumentare sia la velocità di programmazione sia quella di esecuzione. La restituzione di valori multipli può rivelsarsi un meccanismo notevolmente efficiente; per esempio, può essere utilizzata per ottenere i risultati d’esecuzione di due strutture di controllo parallele al fine di garantire in uscita sia il flusso di esecuzione corretto, sia quello errato. O, più semplicemente, può essere utilizzato per ritornare due variabili di interesse. Il Listato 4.16 presenta un tipico esempio di return multiplo. Supponiamo di voler trovare, dato un gruppo prestabilito di conto correnti, quali fra questi abbiano il saldo maggiore e minore. La risoluzione del problema può essere gestita semplicemente tramite, proprio, il return multiplo. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package main import "fmt" type CurrentAccount struct { iban string number string balance float64 } func MaxMin(currentArrey []CurrentAccount) (string, string) { max := currentArrey[0].balance var numberMAX string for _, v := range currentArrey { if v.balance > max { max = v.balance numberMAX = v.number } } min := currentArrey[0].balance var numberMIN string for _, v := range currentArrey { if v.balance < min { min = v.balance numberMIN = v.number } } return numberMAX, numberMIN } func main() { CA1 := CA2 := CA3 := CA4 := CA5 := CA6 := CA7 := CA8 := CA9 := CurrentAccount{iban: CurrentAccount{iban: CurrentAccount{iban: CurrentAccount{iban: CurrentAccount{iban: CurrentAccount{iban: CurrentAccount{iban: CurrentAccount{iban: CurrentAccount{iban: "ITKA1", number: "A", balance: 943.7} "ITDA2", number: "B", balance: 343.0} "IFTA3", number: "C", balance: 12343.2} "ITDA4", number: "D", balance: 2341.8} "IT8A5", number: "E", balance: 1000.3} "ITDKA1", number: "F", balance: 9431.7} "ITDAF2", number: "G", balance: 3400.0} "IFT1A3", number: "H", balance: 1098.2} "ITDAE4", number: "I", balance: 3041.8} var currentArrey = []CurrentAccount{CA1, CA2, CA3, CA4, CA5, CA6, CA7, CA8, CA9} var MAXBalance, MINBalance string MAXBalance, MINBalance = MaxMin(currentArrey) fmt.Println("Il conto con saldo maggiore è:", MAXBalance) fmt.Println("Il conto con saldo mainore è:", MINBalance) } Listato 4.16. Massimo e minimo in Go, return multipli In Java non è presente la funzionalità di restituzione multipla. Per potere ottenere il medesimo risultato del Listato 4.16 si potrebbe utilizzare l’algoritmo presente nel Listato 4.17. 1 2 3 4 5 6 7 8 9 10 11 import java.util.Vector; public class MaxMin { public static Vector<String> MaxMin(Vector<CurrentAccount> v){ String numberMax = null; String numberMin = null; float MAXBalance=V.get(0).getBalance(); for (int i=0;i<V.size();i++) { if (V.get(i).getBalance()>MAXBalance){ tmp=V.get(i).getBalance(); 62 4 Java vs Go: la gestione delle funzioni 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 numberMax=V.get(i).getNumber();} } float MINBalance=V.get(0).getBalance(); for (int i=0;i<V.size();i++) { if (V.get(i).getBalance()<MINBalance){ MINBalance=V.get(i).getBalance(); numberMin=V.get(i).getNumber();} } Vector<String> MaxMin = new Vector<String>(); MaxMin.addElement(numberMax); MaxMin.addElement(numberMin); return MaxMin; } Listato 4.17. Esempio di massimo e minimo in Java, return singolo 4.4 Conclusioni Nelle sezioni e sottosezioni precedenti abbiamo approfondito gli aspetti principali della gestione delle funzioni sia in Java che in Go. In Java la gestione delle funzioni si rivela molto chiara. Ogni metodo, infatti, ha la propria classe, il proprio posto e la propria funzione. Tale meccanismo migliora la leggibilità del codice e, quindi, anche la capacità di modificare ed estendere le funzionalità del sistema stesso. Inoltre, il polimorfismo per metodi garantisce, insieme alla chiarezza, anche, uno strumento in grado di assicurare un riuso del codice elevatissimo. La chiarezza e la facilità di programmazione vengono, però, pagati in termini di pesantezza del codice stesso. L’inserimento dei metodi costruttori,di metodi get e set cosı̀ come il polimorfismo stesso, se, da una parte, snelliscono alcuni meccanismi, come ad esempio l’ereditarietà, dall’altra parte, rendono il linguaggio particolarmente verboso, anche per programmi di semplice realizzazione. In Golang la gestione delle funzioni non è ordinata come in Java. Anche se è possibile implementare una programmazione che ricorda il paradigma OOP, tramite i puntatori, i “metodi” sono all’esterno delle struct e, di conseguenza, risultano di difficile lettura. Inoltre, l’implementazione di alcuni costrutti, come ad esempio, le funzioni anonime, non risultano fondamentali al fine della programmazione. Oltretutto, a partire dalla Versione SE 8, le stesse funzioni anonime sono state implementate in Java , arricchendo ancora di più il linguaggio. La gestione delle funzioni è, in Go, sicuramente sintetica. Infatti, è possibile stilare pochissime righe di codice per ottenere operazioni complesse. Tale meccanismo è in netta contrapposizione con Java; tuttavia, la verbosità di quest’ultimo non è pagata dal programmatore. I numerosi supporti software, quale, ad esempio, Eclipse, mettono a disposizione degli sviluppatori tasti di accesso rapido che permettono, con pochi click e in poco tempo, di costruire tutti i metodi necessari alla classe, come, ad esempio, i metodi get, quelli set, i metodi costruttori , etc. La presenza del return multiplo, però, rappresenta un vantaggio evidente e notevole del linguaggio di casa Google, rispetto a Java. Tuttavia, analogalmente a quanto è avvenuto per le funzioni anonime, non dovrebbe stupire se, nel prossimo futuro, anche Java aggiorni le sue specifiche per poter garantire tale peculiarità. 5 Java Vs Go: la gestione delle interfacce In questo capitolo verrà discussa l’implementazione delle interfacce nel linguaggio Java e nel linguaggio Go. Tale discussione ci porterà, anche, ad introdurre il concetto di ereditarietà multipla. 5.1 La gestione delle interfacce in Java In questa sezione verrà trattata la gestione delle interfacce nel linguaggio di programmazione Java. Per poter meglio comprendere il comportamento delle interfacce, sarà sviluppata una sottosezione dove verrà presentato il concetto di classe astratta. 5.1.1 Le interfacce Le interfacce non sono tecnicamente delle classi, ma sono più assimilabile al concetto di oggetti pubblici. Non è possibile, infatti, dichiarare, all’interno di esse, le variabili di istanza, ma solo delle costanti, ossia delle variabili del tipo public static final. I metodi dell’interfaccia sono pubblici e astratti. È possibile, pertanto, definirli, ma non è permesso implementarli. Per poter intestare un metodo come astratto bisogna utilizzare il modificatore abstract. Esso permette di modificare le funzionalità e le caratteristiche sia dei metodi che delle classi. Tuttavia, non è possibile marcare come abstract le variabili. Il metodo astratto viene definito dalla sua“firma” ed, eventualmente, dal tipo di ritorno; non prevede, pertanto, al suo interno, il blocco di codice e, di conseguenza, attraverso il modificatore abstract, sarà modificata la sintassi di base del metodo stesso. Verranno, infatti, eliminate le parentesi graffe e verrà introdotta l’interpunzione tramite punto e virgola alla fine del token. Un esempio di metodo astratto è riportato nel Listato 5.1. 1 public abstract void EsempioDiMetodoAstratto(); Listato 5.1. Esempio di metodo astratto in Java 64 5 Java Vs Go: la gestione delle interfacce Il metodo, non presentando righe di codice, non potrà essere invocato nella maniera tradizionale. Sarà, pertanto, soggetto all’override delle classi che lo implementano, sulla base di quanto indicato, nel capitolo precedente, nella sezione dedicata al polimorfismo. Java imposta, automaticamente, i marcatori relativi ad un’interfaccia; pertanto, è possibile non esplicitarli in fase di dichiarazione. Pragmaticamente, le interfacce contengono al loro interno solo le informazioni relative alle funzionalità minime che devono essere garantite dalla classe che le implementa. L’interfaccia, di conseguenza, si presenta come indicato nel Listato 5.2 e viene implementata come riportato nei Listati 5.3 e 5.4. 1 2 3 4 public interface EsempioDiInterfaccia { String P =" Privato"; String C = "Azienda"; void recognition(); } Listato 5.2. Dichiarazione di interfaccia in Java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Public class Company implements EsempioDiInterfaccia{ private String vatNum; private String businessName; public Company(String vatNumber, String businessName){ this.vatNum = vatNum; this.businessName = businessName;} public String getVatNum() { return vatNum; } public void setVatNum(String vatNum) { this.vatNum = vatNum; } public String getBusinessName() { return businessName; } public void setBusinessName(String businessName) { this.businessName = businessName; } @Override public void Recognition() { System.out.println(C); }} Listato 5.3. Implementazione nella classe Company 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Private implements EsempioDiInterfaccia{ private String Surname; private String Name; public Private (String Surname, String Name){ this.Name = Name; this.Surname = Surname;} public String Surname() { return Surname; } public void Surname(String Surname) { this.Surname = Surname; } public String Name() { return Name; } public void Name(String Name) { this.Name = Name; } @Override public void Recognition() { System.out.println(P); }} Listato 5.4. Implementazione nella classe Private Dai listati precedenti, possiamo affermare che ogni classe che implementa l’interfaccia eredita tutti i metodi della stessa. Gli stessi metodi saranno riscritti, opportu- 5.1 La gestione delle interfacce in Java 65 namente, dalla classe che li implementa usufruendo del paradigma del polimorfismo di tipo override. Per capire meglio le potenzialità delle interfacce, è necessario sviluppare altri esempi. Si vuole realizzare un programma che, ricevuto un numero predefinito di conto correnti, li ordina in modo crescente, dal saldo più a quello più alto. Per sviluppare questo programma utilizzeremo l’interfaccia riportata nel Listato 5.5. 1 2 3 public interface Sortable { public boolean balanceMinone (Object comparison); public boolean greaterBalance(Object comparison);} Listato 5.5. Interfaccia Sortable Le classe CurrentAccount che implementa l’interfaccia deve garantire, pertanto, i metodi balanceMinone e graterBalance (Listato 5.6). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public class CurrentAccount implements Sortable{ private String iban; private String number; private float balance; public CurrentAccount() {} public CurrentAccount(String iban, String number, float balance) { this.iban = iban; this.number = number; this.balance = balance; } public String getIban() { return iban; } public void setIban(String iban) { this.iban = iban; } public String getNumber() { return number; } public void setNumber(String number) { this.number = number; } public float getBalance() { return balance; } public void setBalance(float balance) { this.balance = balance; } public synchronized void ReadBalance (int i){ System.out.println(Thread.currentThread().getName()+"i="+i+"}:Conto corrente n."+number+ ". Saldo: "+getBalance()+" euro."); } public synchronized void Deposit (float amount, int i){ ReadBalance(i); System.out.println(Thread.currentThread().getName()+"i="+i+"}:Deposito: "+amount+" euro."); setBalance(getBalance()+amount); ReadBalance(i); notifyAll();} public synchronized void Withdraw (float amount, int i)throws InterruptedException{ while(getBalance()<amount){ ReadBalance(i); System.out.println(Thread.currentThread().getName()+"i="+i+"}:Ritiro: "+amount+ " euro. SALDO INSUFFICIENTE!"); wait();} ReadBalance(i); System.out.println(Thread.currentThread().getName()+"i="+i+"}:Ritiro: "+amount+" euro."); setBalance(getBalance()-amount); ReadBalance(i) } @Override public boolean balanceMinone(Object comparison){ return getBalance()< ((CurrentAccount)comparison).balance; } @Override public boolean greaterBalance(Object comparison) { return ! balanceMinone(comparison); }} Listato 5.6. Classe CurrentAccount 66 5 Java Vs Go: la gestione delle interfacce Attraverso il polimorfismo si sovrascrivono i metodi dell’interfaccia. L’algoritmo di ordinamento è gestito dalla classe SortingAlgorithm, riportata nel Listato 5.7. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class SortingAlgorithm { Sortable[] v=null; public SortingAlgorithm(Sortable[] _v) {v = _v.clone(); } int lowPos(int j) { int lowestPosition=j; int currentPosition=j++; while (currentPosition<v.length) { if(v[currentPosition].balanceMinone(v[lowestPosition]) ) lowestPosition = currentPosition; currentPosition++; } return lowestPosition; } Sortable[] selectionSortMINMAX() { for (int i=0; i<v.length - 1; i++) { int last = lowPos(i); Sortable tmp = v[i]; v[i] = v[last]; v[last] = tmp; } return v; }} Listato 5.7. Classe SortingAlgorithm Il costruttore dell’algoritmo di ordinamento riceve in ingresso il vettore di tipo Sortable, ossia un array di conto correnti “disordinati”. Il vettore viene clonato attraverso il metodo clone() della classe Object, per non “sporcare” l’array in ricezione. L’ordinamento è di tipo selection sort, ossia prevede un’algoritmo che seleziona, di volta in volta, il saldo minore nella sequenza di partenza e lo sposta in una sottosequenza ordinata. Pertanto, in qualunque istante, la sequenza di interesse, è divisa in due parti, ovvero: • sottosequenza ordinata: occupa le prime posizioni della sequenza e contiene gli elementi già ordinati dall’algoritmo; • sottosequenza disordinata: costituisce l’insieme di elementi non ancora ordinati. L’algoritmo per ordinare l’array v, di lunghezza v.lenght, fa scorrere l’indice i da 0 a v.lenght-1 tramite un ciclo for. Si ricerca il conto corrente con saldo minore nella sottosequenza disordinata, di intervallo [i, v.lenght] e, quando viene identificato, lo si scambia con l’elemento i-esimo della sequenza. Il ciclo for è ripetuto v.lenght-1 volte, poichè è chiaro che l’ultimo elemento rimasto sarà il conto corrente con il saldo maggiore. L’algoritmo viene ripetuto finchè l’array non è stato ordinato completamente. Quando l’algoritmo di ordinamento termina il proprio compito, il vettore v viene restituito in uscita. Nel main() viene, inizialmente, creato il vettore di conto correnti che si deve ordinare. Successivamente, viene creato l’oggetto della classe SortingAlgorithm, al quale si passa il vettore di array, precedentemente creato. Il vettore viene, pertanto, ordinato e il suo valore viene passato, in fase di dichiarazione, all’array currentList. Tramite un ciclo for verranno, infine, stampati i valori ordinati in base al saldo (Listato 5.8). 1 2 public class Main { public static void main(String[] args) { 5.1 La gestione delle interfacce in Java 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 CurrentAccount[] currentArrey = {new CurrentAccount("IBTSAN1","A" , 1280), new CurrentAccount("ITKIBAN2", "B", 1342), new CurrentAccount("SKIIBFATAN3", "C", 38), new CurrentAccount("PDAR21KMER", "D", 389)}; SortingAlgorithm s = new SortingAlgorithm(currentArrey); CurrentAccount[] currentList = (CurrentAccount[]) s.selectionSortMINMAX(); System.out.println("Conti correnti ordinati in ordine crescente rispetto al saldo:"); for (int i=0; i<currentList.length; i++) System.out.println("Saldo: " + currentList[i].balance+", Conto corrente n.: " + currentList[i].number+", IBAN: " + currentList[i].iban);}} 67 Listato 5.8. Il main() del programma che si occupa dell’ordinamento Eseguendo il programma, si avrà il risultato mostrato in Figura 5.1. Figura 5.1. Risultato ottenuto dall’esecuzione del programma del Listato 5.8 5.1.2 L’ereditarietà multipla Java, diversamente da altri linguaggi, come, ad esempio, C++, non supporta il concetto di ereditarietà multipla. Pertanto, una classe può estendere soltanto un’altra classe. È possibile, tuttavia, aggirare parzialmente il problema avvalendosi delle interfacce. In Java, infatti, l’implementazione delle interfacce è multipla; la classe, pertanto, può implementare più interfacce, ciascuna delle quali contiene, al proprio interno, le definizioni dei metodi che si vogliono implementare e sovrascrivere. Inoltre, Java garantisce alle stesse interfacce l’ereditarietà multipla. Un’interfaccia può, quindi, ereditare il comportamento di una o più interfacce utilizzando la keyword extends (Listato 5.9). 1 2 3 4 5 6 public interface Inheritance extends intrfc1, intrfc2 { int statfin = 3; void publastc1(i); String P(); void publastc1(); } Listato 5.9. Interfaccia che estende altre interfacce in Java Le interfacce, pertanto, hanno sia l’importante caratteristica di poter estendere altre interfacce e sia la peculiarità di poter essere implementate dalle classi. Inoltre, tali implementazioni ed estensioni possono essere utilizzate, teoricamente, all’infinito. Tali meccanismi permettono, di fatto, la simulazione dell’ereditarietà multipla. L’ereditarietà multipla simulata è, inoltre, di tipo controllato; le interfacce possono dichiarare, soltanto, delle costanti e definire dei metodi astratti; pertanto, 68 5 Java Vs Go: la gestione delle interfacce sarà garantita quella robustezza del codice che viene minata, invece, nei linguaggi provvisti di una vera e propria ereditarietà multipla. Modelliamo il case study di interesse per simulare un esempio di ereditarietà multipla; per prima cosa creiamo le interfacce che si vogliono implementare nelle varie classi (Listati 5.10-5.14). 1 2 public interface CanRead{ void ReadBalance(); } Listato 5.10. Interfaccia CanRead 1 2 public interface CanDeposit extends CanRead{ float Deposit(float amount);} Listato 5.11. Interfaccia CanDeposit 1 2 public interface CanWithdraw{ float Withdraw(float amount);} Listato 5.12. Interfaccia CanWithdraw 1 2 public interface CanTransfer{ float Transfer(CurrentAccount C, float amount);} Listato 5.13. Interfaccia CanTransfer 1 2 public interface CanInterest { float Interest();} Listato 5.14. Interfaccia CanInterest Realizziamo, ora, le classi che devono implementare i metodi astratti di tali interfacce. La prima classe da creare sarà la CurrentAccount. Essa implementa tutte le interfacce, ad eccezione della CanInterest. Di conseguenza, è necessario effettuare l’override dei metodi implementati (Listato 5.15). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class CurrentAccount implements CanRead, CanDeposit, CanWithdraw, CanTransfer { private String iban; private String number; private float balance; public CurrentAccount() {} public CurrentAccount(String iban, String number, float balance) { this.iban = iban; this.number = number; this.balance = balance; } public String getIban() { return iban; } public void setIban(String iban) { this.iban = iban; } public String getNumber() { return number; } public void setNumber(String number) { this.number = number; } public float getBalance() { return balance; 5.1 La gestione delle interfacce in Java 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 } public void setBalance(float balance) { this.balance = balance; } @Override public float Deposit(float amount) { System.out.println("Conto Corrente n."+getNumber()+", Deposito: "+amount+" euro."); setBalance(getBalance()+amount); return getBalance(); } @Override public void ReadBalance() { System.out.println("Conto corrente n."+getNumber()+", Saldo: "+getBalance());} @Override public float Transfer(CurrentAccount C, float amount) { Withdraw(amount); System.out.println("Bonifico: "+amount+" euro. Trasferimento da Conto corrente n."+getNumber()+ " al conto "+C.number); return getBalance(); } @Override public float Withdraw(float amount) { System.out.println("Conto corrente n."+getNumber()+", Ritiro: "+amount+" euro."); setBalance(getBalance()-amount); return getBalance(); }} 69 Listato 5.15. Classe CurrentAccount che implementa le interfacce Verrà realizzata, poi, la classe InterestBearingAccount. La classe estende la classe CurrentAccount ereditandone anche le interfacce. Inoltre, implementa l’interfaccia CanInterest, per poter calcolare l’interesse maturato. Di conseguenza, essa deve garantire sia i metodi del CurrentAccount sia i metodi astratti implementati dallo stesso, sia, infine, i metodi dell’interfaccia CanInterest (Listato 5.16). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public class InterestBearingAccount extends CurrentAccount implements CanInterest { private float percentage; public InterestBearingAccount() { super(); // TODO Auto-generated constructor stub } public InterestBearingAccount(String iban, String number, float balance, float percentage) { super(iban, number, balance); this.percentage=percentage; // TODO Auto-generated constructor stub } public float getPercentage() { return percentage; } public void setPercentage(float percentage) { this.percentage = percentage; } @Override public float Interest() { System.out.println("Conto corrente n. "+getNumber()+ ", Incremento: "+getPercentage()+"per cento"); setBalance((getBalance()*getPercentage())/100); return getBalance(); } @Override public float Deposit(float amount) { // TODO Auto-generated method stub return super.Deposit(amount); } @Override public void ReadBalance() { // TODO Auto-generated method stub super.ReadBalance(); } @Override public float Transfer(CurrentAccount C, float amount) { // TODO Auto-generated method stub return super.Transfer(C, amount); } 70 5 Java Vs Go: la gestione delle interfacce 50 51 52 53 54 @Override public float Withdraw(float amount) { // TODO Auto-generated method stub return super.Withdraw(amount); } } Listato 5.16. Classe InterestBearginAccount che estende CurrentAccount Infine, nel Listato 5.17, viene realizzata la classe SubordinatedBond. Essa implementerà le interfacce CanRead, CanInterest e CanWithdraw. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class SubordinatedBond implements CanRead, CanWithdraw,CanInterest{ private int id; private float percentage; private float balance; public SubordinatedBond(int id, float percentage, float balance) { super(); this.percentage = percentage; this.balance = balance; this.id=id; } public float getPercentage() { return percentage; } public void setPercentage(float percentage) { this.percentage = percentage; } public float getBalance() { return balance; } public void setBalance(float balance) { this.balance = balance; } public int getId() { return id; } public void setId(int id) { this.id = id; } @Override public float Withdraw(float amount) { ReadBalance(); System.out.println("Obbligazione n."+getId()+", Ritiro: "+amount+" euro."); setBalance(getBalance()-amount); return getBalance(); } @Override public void ReadBalance() { System.out.println("Obbligazione n."+getId()+", Capitale: "+getBalance()+" euro.");} @Override public float Interest() { System.out.println("Obbligazione n."+getId()+ ",Incremento: "+getPercentage()+"per cento"); setBalance((getBalance()*getPercentage())/100); return getBalance();}} Listato 5.17. Classe SubordinatedBond che implementa le interfacce Il main(), riportato nel Listato 5.18, produrrà il risultato indicato in Figura 5.2. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class SubordinatedBond implements CanRead, CanWithdraw, CanInterest{ private int id; private float percentage; private float balance; public SubordinatedBond(int id, float percentage, float balance) { super(); this.percentage = percentage; this.balance = balance; this.id=id; } public float getPercentage() { return percentage; } public void setPercentage(float percentage) { this.percentage = percentage; } public float getBalance() { return balance; } public void setBalance(float balance) { this.balance = balance; } public int getId() { return id; } public void setId(int id) { this.id = id; } @Override 5.1 La gestione delle interfacce in Java 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public float Withdraw(float amount) { ReadBalance(); System.out.println("Obbligazione n."+getId()+", Ritiro: "+amount+" euro."); setBalance(getBalance()-amount); return getBalance(); } @Override public void ReadBalance() { System.out.println("Obbligazione n."+getId()+", Capitale: "+getBalance()+" euro.");} @Override public float Interest() { System.out.println("Obbligazione n."+getId()+", Incemento: "+getPercentage()+"per cento"); setBalance((getBalance()*getPercentage())/100); return getBalance();}} 71 Listato 5.18. Metodo main() che implementa le interfacce Figura 5.2. Risultato ottenuto dall’esecuzione del programma del Listato 5.18 Attraverso le interfacce siamo riusciti a far ereditare i metodi implementati in una classe a diversi tipi di classi differenti. Abbiamo, pertanto, simulato un esempio di ereditarietà multipla. 5.1.3 Classi astratte ed interfacce Spesso si tende a confondere il concetto di interfaccia con quello di classe astratta. Sebbene, dal punto di vista logico, possono risultare simili, le interfacce sono un’evoluzione delle stesse classi astratte. Le classi astratte, in Java, vengono utilizzate per generalizzare le caratteristiche in comune fra determinate classi, dando luogo ad una gerarchia ben definita. Una classe astratta è, di conseguenza, una superclasse che implementa al suo interno metodi astratti. Una classe abstract, analogamente a quanto accade per le interfacce, non può essere instanziata; tuttavia, a differenza delle interfacce, può presentare sia variabili non marcate come static final, sia metodi non public, che metodi costruttori. La classe astratta, a meno dell’istanziazione degli oggetti, è, a differenza delle interfacce, una classe a tutti gli effetti. Un esempio di classe astratta è riportato nel Listato 5.19. 1 2 3 4 5 6 public abstract class EsempioDiClasseAstratta { private String attributo1; private String attributo2; private float attributo3 ; public EsempioDiClasseAstratta(String attributo1, String attributo2, float attributo3) { 72 5 Java Vs Go: la gestione delle interfacce 7 8 9 10 this.attributo1 = attributo1; this.attributo2 = attributo2; this.attributo3 = attributo3; } Listato 5.19. Esempio di classe astratta La superclasse EsempioDiClasseAstratta detta lo standard delle sottoclassi che la estendono. Una classe che eredita i metodi astratti, definiti nella superclasse astratta, deve, infatti, usufruire del polimorfismo di tipo override, oppure dichiarare i metodi ereditati come astratti. Sia le classi astratte che le interfacce si rivelano, pertanto, uno strumento fondamentale per la OOP; esse possegono, infatti, la caratteristica comune di “forzare” le classi che le estendono, o che le implementano, ad ereditare o implementare i comportamenti definiti in queste due entità. Un’evidente differenza fra le classi astratte e le interfacce è la simulazione dell’eredità multipla. Le classi abstract, essendo classi, possono estendere una sola classe per volta. Possiamo, pertanto, affermare che, mentre una classe astratta non è altro che un’astrazione troppo generica per poter essere istanziata nel contesto di riferimento, un’interfaccia può essere definita come un’astrazione di tipo procedurale. Difatti, le prime sono generalmente utilizzate per indicare entità generiche, come, ad esempio, il concetto di animale, di mezzo di trasporto, di uomo, etc.; invece, le seconde vengono utilizzate per indicare i comportamenti generici, come, ad esempio, il verso di un animale, la guida di un mezzo di trasporto, etc. Le interfacce e le classi astratte possono essere combinate. Il vantaggio sarà quello di sfruttare, pienamente, e in situazioni diverse, tutte le sfaccettature del polimorfismo, tramite l’uso di variabili eterogenee, invocazioni a metodi astratti proprie delle classi abstract e standard di programmazione dettati dall’interfacce. 5.2 La gestione delle interfacce in Go 5.2.1 Le interfacce La gestione delle interfacce rappresenta un altro fondamentale meccanismo che consente al linguaggio di programmazione Go di “collegarsi” con i linguaggi che supportano tipicamente il paradigma di programmazione OOP. La dichiarazione formale di un’interfaccia è riportata nel Listato 5.20. 1 2 3 4 5 package main import "fmt" type Tripler interface { Triple() float64 } Listato 5.20. Esempio di Interfaccia in Go Si introduce l’interfaccia con la parola chiave type. Successivamente, viene assegnato il nome alla stessa interfacca; la convezione, in Go, vuole che il nome scelto termini con il suffisso “er ” o “able”. Subito dopo viene introdotta, come in Java, la 5.2 La gestione delle interfacce in Go 73 keyword interface e il token termina con la parentesi graffa, rigorosamente inserita sulla stessa linea. Infine, viene inserita la collezione di “firme” delle func, ossia un insieme identificatori e parametri formali delle funzioni. Due tipi di interfaccia saranno, pertanto, ritenute identiche dal compilatore se presentano lo stesso insieme di funzioni, ossia func aventi stessa identificazione, stessa definizione dei parametri formali e stesso tipo di ritorno. L’ordine in cui sono riportate le “firme” risulta irrilevante; tuttavia, grazie al fatto che Go è case sensitive, due interfacce saranno trattate in maniera diversa se le func presentano identificatori che si scostano anche di solo una lettera maiuscola o minuscola. Per implementare un’interfaccia si procede come riportato nel Listato 5.21. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package main import "fmt" type Tripler interface { Triple() float64 } type CurrentAccount struct { balance float64 number string iban string } func (ca CurrentAccount) Triple() float64 { return ca.balance*3 } func main() { CA := CurrentAccount{balance:56, number:"A", iban:"IB"} c := Tripler(CA) fmt.Println("Il saldo triplicato è :", c.Triple()) } Listato 5.21. Implementazione di una interfaccia in Go Il body dell’interfaccia racchiude la “firma” delle funzioni; nel caso considerato sarà presente un’unica func Triple(), che prevede un tipo di ritorno float64. Successivamente, viene introdotta la struct CurrentAccount. Nel main(), vengono inizializzate la struct e una variabile c di tipo Tripler, legata alla struct instanziata CA. L’istruzione riportata nel Listato 5.22 è assimilabile all’istruzione Java riportata nel Listato 5.23. 1 c := Tripler(CA) Listato 5.22. Istruzione equivalente in Go 1 public class CurrentAccount implements Tripler Listato 5.23. Istruzione equivalente in Java Come è possibile notare dal listato, a differenza di Java, le funzioni indicate nelle interfacce Go non sono riportate all’interno del tipo che le implementa. Tuttavia, come in Java, le func dell’interfaccia definiscono il comportamento standard per i type che le implementano. In Go è presente, inoltre, un tipo interfaccia che non specifica al proprio interno alcuna “firma”; tale interfaccia viene definita interfaccia vuota, riportata nel Listato 5.24. 74 5 Java Vs Go: la gestione delle interfacce 1 interface{} Listato 5.24. Interfaccia vuota Le interfacce vuote vengono utilizzate, essenzialmente, per gestire valori di type indefinito. Poichè ogni type può anche non riferirsi ad alcuna funzione, ne deriva che interfacce vuote possono essere implementate in qualsiasi tipo. L’istruzione fmt.Print è particolarmente indicata per l’utilizzo delle interfacce vuote, poichè, tipicamente, la sua funzione è proprio quella di stampare entità indefinite. Un’implementazione di interfaccia vuota sarà analizzata nella prossima sottosezione. 5.2.2 Strutture, puntatori e interfacce Nel Listato 5.25 viene esplicitato un tipico esempio di interfaccia vuota. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package main import "fmt" type Private struct{ idCustomerP int name string surname string taxCode string documentNum string address string postalCode int city string email string landline int cellphone int } func PrintAll(vals []interface{}) { for _, val := range vals { fmt.Println(val) } } func main() P1:= P2:= P3:= { Private{idCustomerP:1, name:"John", surname:"Snow"} Private{idCustomerP:2, name:"Ramsey",surname:"Bolton"} Private{idCustomerP:3, name:"Frank", surname:"Castle"} surnames := []string{P1.surname, P2.surname, P3.surname} vals := make([]interface{}, len(surnames)) for i, v := range surnames { vals[i] = v } PrintAll(vals) } Listato 5.25. Interfaccia vuota e puntatori Nel metodo main() vengono instanziati tre client relativi alla struct Private. In fase di dichiarazione dei client, verranno specificati solo tre attributi del campo; gli altri saranno inizializzati di default. Successivamente, saranno inseriti i cognomi dei client in nell’array surnames. A questo punto viene creata una variabile vals di tipo slice, attraverso l’istruzione make. Le slice sono legate, concettualmente, con gli array; esse sono costituite da un riferimento (un puntatore) e dalla loro lunghezza. Il riferimento, generalmente, è legato ad una porzione dell’array sottostante. Tale segmento può anche coincidere con tutta la lunghezza dell’array cui riferisce. A differenza degli array, la lunghezza di una slice è variabile e può, pertanto, essere modificata dinamicamente. Le slice possono, anche, non occupare, effettivamente, 5.2 La gestione delle interfacce in Go 75 zone di memoria. Il contenuto di una slice è, essenzialmente, un puntatore che riferisce alla sottosezione di memoria occupata dall’array sottostante. Nell’esempio trattato, la slice si riferisce, inizialmente, ad una interfaccia vuota e la sua lunghezza viene posta pari alla lunghezza dell’array sottostante surnames. Successivamente, tramite un ciclo for, si scorrono tutti gli elementi appartenenti all’array sottostante che vengono riferiti. Di volta, in volta, gli elementi riferiti vengono passati all’interfaccia vuota. Concluso il ciclo, viene passata l’interfaccia vuota e la dimensione dell’array sottostante, vengono passate, tramite la slice, alla func PrintAll, come parametro formale. Il body della funzione consiste in un ciclo for che permette di scorrere i valori dell’interfaccia vuota per poi stamparli. La gestione delle interfacce, nel linguaggio Go, presenta la caratteristica che il type che implementa un’interfaccia non deve necessariamente esplicitare la sua implementazione. L’istruzione, appartenente al flusso d’esecuzione del Listato 5.21, vista nella sottosezione precedente, e riportata nel Listato 5.26 per una maggiore chiarezza, rappresenta, sostanzialmente, un puntatore che dal Triple() si riferisce al CurrentAccount. 1 2 func (ca CurrentAccount) Triple() float64 { return ca.balance*3} Listato 5.26. Implementazione di una interfaccia in Go Le funzioni dell’interfaccia, per poter essere riutilizzate da altri type, devono, di conseguenza, puntare, volta per volta, al type che le vuole implementare, altrimenti saranno sempre riferite al type che le ha implementate per ultimo. Affinchè un type sia accettato dall’interfaccia, deve essere puntato dalla func Triple(). Ad esempio, introducendo, la struct SubordinatedBond, se non si effettua tale operazione, la func dell’interfaccia punterà sempre alla struct CurrentAccount e, in fase di esecuzione, il compilatore riporterà l’errore “SubordinatedBond does not implement Tripler (missing Triple method)” Bisognerebbe, pertanto, modellare il codice del Listato 5.21 come riportato nel Listato 5.27. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package main import "fmt" type Tripler interface { Triple() float64 } type CurrentAccount struct { balance float64 number string iban string } type SubordinatedBond struct{ id int balance float64 percentage float64 } func (ca CurrentAccount) Triple() float64 { return ca.balance*3} func (sb SubordinatedBond ) Triple() float64 { return (sb.balance+(sb.balance*sb.percentage)*3/100)} func main() { CA := CurrentAccount{balance:56, number:"A",iban:"IB"} 76 5 Java Vs Go: la gestione delle interfacce 26 27 28 29 30 31 SB := SubordinatedBond{id:32, balance:1430, percentage:5} c := Tripler(CA) fmt.Println("Il saldo triplicato è:", c.Triple()) s:= Tripler(SB) fmt.Println("Il capitale triplicato è:", s.Triple()) } Listato 5.27. type e interfacce In Go, una struct presenta func “reali” e un campo di attributi, mentre un’interfaccia presenta func “virtuali” e nessun campo di attributi. I due elementi, tramite i puntatori, si possono legare perfettamente; la possibilità, tramite le interfacce, di “obbligare” le struct a implementare le func rende Go un linguaggio capace di godere, seppur in maniera atipica, del concetto di polimorfismo. Le interfacce, inoltre, permettono come in Java, la simulazione dell’ereditarietà multipla. 5.3 Conclusioni Le interfacce possono essere viste come l’implementazione attesa di un programma e la sua effettiva realizzazione. L’interfaccia è, infatti, l’entità che vincola le classi o le struct che la implementano a supportare le funzioni in essa dichiarate. Esse sono un elemento necessario, soprattutto dal punto di vista progettuale, poichè permettono di realizzare un controllo veloce ed efficace sul codice del programma. Oltre a consentire un’immediata verifica del codice, le interfacce rappresentano un componente indispensabile per garantire sia estendibilità che riutilizzo del codice. Esse permettono, di conseguenza, di migliorare la produttività della programmazione. In Java, grazie al paradigma OOP, le porzioni di codice riutilizzabili sono ben delineate. La presenza di file con estensione .java per ogni interfaccia, e in generale per ogni classe, generato nel Workspace, rende il riutilizzo di questi componenti semplice e immediato. Quando si utilizza un’interfaccia, sarà, pertanto, garantita sia la leggibilità che la robustezza del codice. Nel linguaggio di programmazione Go, le interfacce non trovano uno spazio isolato e ben definito all’interno del programma. Esse tendono a confondersi con il codice stesso, rendendo, pertanto, la loro leggibilità poco chiara. Inoltre, l’utilizzo dei puntatori e la presenza dei metodi da implementare discostati completamente sia dal blocco che codifica l’interfaccia sia da quello che codifica la struct, rendono le lettura del codice ancora più “astrusa”. Go, nella gestione delle interfacce, ha, di certo, snellito il codice verboso del programmi tipicamente OOP. La sintesi prodotta, tuttavia, viene pagata in termini di chiarezza. Se tale aspetto risulta trascurabile per un singolo programmatore (ognuno può riconoscere il proprio metodo di programmazione), risulta un incoveniente di notevole proporzione se le interfacce devono essere riutilizzate da programmatori diversi, anche appartenenti allo stesso team di sviluppo. La scelta di riutilizzare il codice per velocizzare le operazioni di programmazione potrebbe portare, paradossalmente, all’effetto opposto; l’interpretazione del codice stesso potrebbe richiedere, infatti, un tempo di lavoro maggiore rispetto a quello di sviluppo dello stesso programma. Inoltre, la scarsa leggibilità delle interfacce potrebbe portare in errore gli stessi programmatori, aumentando la probabilità di errata implementazione del codice stesso. 5.3 Conclusioni 77 Go, inoltre, non presentando un meccanismo reale di ereditarietà, non renderà possibile l’esistenza contemporanea sia di estensioni che di implementazioni multiple delle interfacce. Java, anche da questo punto di vista, si dimostra migliore. La piena ereditarietà singola, la presenza delle classi astratte e, in generale, delle superclassi, la multi-eriditarietà reale fra le interfacce e l’ereditarietà multipla simulata, tramite le stesse combinate in modo chiaro, controllato e preciso, permettono di stilare quelle generalizzazioni necessarie ad una buona programmazione. 6 Java vs Go: Riuso del software In questo capitolo verrà presentata, inizialmente, un’introduzione generale per esplicare il concetto di riuso del software. Successivamente, si illustrerà la possibilità di riuso nei linguaggi di programmazione Java e Go. In particolare, si approfondiranno i concetti di ereditarietà ed embedding. 6.1 Il concetto di riuso di software Un sistema software nasce, nella maggior parte dei casi, dal riuso di componenti preesistenti. Occorre, pertanto, adattare le fasi di progettazione su un riutilizzo sistematico del codice. I vantaggi del riuso sono molteplici; la realizzazione di un software ex-novo causa, infatti, costi notevoli sia in termini di tempo che in termini di denaro relativi, soprattutto, allo sviluppo, al testing e alla manutenzione dello stesso. Il riutilizzo del codice, inoltre, ci permetterebbe di garantire l’affidabilità, già consolidata, dal sistema di appartenenza. Tuttavia, bisogna stare ben attenti nella scelta dei software che si vogliono riutilizzare e nelle modalità di implementazione degli stessi. In genere, è buona norma affidarsi a sistemi software che hanno già garantito alta qualità nelle prestazioni e dei quali è esplicitato il codice sorgente; infatti, se si riutilizzasse un’applicazione dove il codice non è dichiarato in maniera esplicita, potremmo avere sia notevoli problemi di adattamento alle specifiche del software che si vuole realizzare e sia impossibilità di aggiornamento del codice stesso. Il riuso, di conseguenza, non può essere scriteriato; è necessaria, infatti, un’attenta pianificazione e valutazione per quanto concerne le componenti software che si vogliono utilizzare. La pianificazione deve, innanzitutto, tenere conto delle criticità del sistema, dell’integrazione delle componenti, delle tempistiche di utilizzo, delle piattaforme su cui il software vuole garantire il servizio, e degli stessi servizi che si vogliono fornire. Esplicate tali premesse, analizziamo, ora, le possibilità di riuso del software garantite dai linguaggi di programmazione Java e Go. 80 6 Java vs Go: Riuso del software 6.2 Riuso del software in Java Durante lo sviluppo del software, nel linguaggio di programmazione Java, ci si accorge, frequentemente, che alcune porzioni del codice realizzato sono molto simili fra loro. Risulta, pertanto, molto vantaggioso adattare e manipolare codice già esistente, per poter rispondere più efficaciemente alle problematiche riscontrate. Java garantisce il riutilizzo del codice, soprattutto grazie alla caratteristica di supportare il paradigma OOP, che prevede, fra le altre cose, la proprietà fondamentale dell’ereditarietà. 6.2.1 Ereditarietà Gerarchie Come detto nella prima sezione, il riuso del software deve seguire un certo criterio. Classi con attributi comuni, ma con un comportamento completamente diverso porterebbero a un riuso del codice inefficiente e potenzialmente dannoso al punto tale da rendere preferibile la cancellazione del codice finora stilato, piuttosto che una sua eventuale rienterpretazione. L’ereditarietà è, sicuramente, uno strumento importante e utile, tuttavia la sua implementazione è affidata in prima istanza al programmatore. Pertanto, per poter implementare tale meccanismo al meglio, è necessario, innanzitutto, elaborare una pianificazione saggia e funzionale. Nasce quindi l’esigenza di sviluppare una gerarchizzazione delle classi che risponda ai criteri dell’ereditarietà. Nel caso di studio che stiamo sviluppando nella presente tesi si è asserito che sia privati cittadini che aziende potevano usufruire delle prestazioni della banca. Essi hanno numerosi attributi in comune, ossia: • identificatore: sia i privati che le aziende possono essere univocamente identificati da un numero progressivo; • citta, indirizzo, C.A.P : sia i privati cittadini che le aziende risiedono in una determinata città, in un determinato indirizzo, identificato dal Codice di Avviamento Postale; • telefono fisso, telefono mobile: sia il cittadino che l’azienda devono fornire i rispettivi recapiti telefonici; • e-mail : sia il cittadino che l’azienda devono, inoltre, fornire un indirizzo di posta elettronica per eventuali comunicazioni via e-mail. Supponendo che i dettagli anagrafici del privato non siano interessanti al fine della progettazione, consideriamo, oltre agli attributi in comune, precedentemente elencati, soltanto i campi businessName e vatNum della classe Company. Un approccio privo di un’attenta pianificazione ci porterebbe a far ereditare alla classe Private gli attributi della classe Company; tale modo di procedere, però, produrrebbe una gerarchizzazione sostanzialmente errata (Figura 6.1). 6.2 Riuso del software in Java 81 Figura 6.1. Esempio di gerarchizzazione errata Per evitare tali errori, è possibile, in prima analisi, utilizzare un test tanto semplice quanto efficace, ossia la relazione “is a”. La prima cosa da fare per stilare, opportunamente, una gerarchia è quella di chiedersi se un determinato elemento fa parte della famiglia di un’altro elemento. Utilizzando tale relazione è immediata la comprensione che Private “isn’t a” Company; si cerca, allora, di introdurre una classe che possa “astrarre” i concetti comuni in modo da poter risolvere positivamente la relazione. La classe che può effettuare tale “astrazione” è la classe Customer; infatti Private “is a” Customer e Company “is a” Customer. La gerarchizzazione, riportata in Figura 6.2, sarà, pertanto, quella corretta. Figura 6.2. Esempio di gerarchizzazione corretta Nel caso considerato, Customer sarà la superclasse delle sottoclassi Company e Private. La definizione delle superclassi e delle sottoclassi sarà espressa, nel dettaglio, nella sottosezione successiva. Superclassi e sottoclassi Definita la gerarchia, è possibile determinare le superclassi e le sottoclassi di interesse. Quando una classe eredita il comportamento da un’altra, allora eredita i suoi metodi, con tutte le caratteristiche che abbiamo trattato precedentemente, nella sezione dedicata alla gestione delle funzioni. 82 6 Java vs Go: Riuso del software È possibile, inoltre, introdurre specifici metodi necessari a determinare il comportamento della sottoclasse. Creiamo, allora, la classe Customer, ottenuta in fase di gerarchizzazione (Listato 6.1). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class Customer { private int idCustomer; private int postalCode; private int landline; private int cellphone; private String address; private String city; private String email; public Customer(){} public Customer(int idCustomer, int postalCode, int landline, int cellphone, String address, String city, String email){ this.idCustomer = idCustomer; this.postalCode = postalCode; this.landline = landline; this.cellphone = cellphone; this.address = address; this.city = city; this.email = email;} public int getIdCustomer() { return idCustomer;} public void setIdCustomer(int idCustomer) { this.idCustomer = idCustomer;} public String getAddress() { return address;} public void setAddress(String address) { this.address = address;} public String getCity() { return city; } public void setCity(String city) { this.city = city; } public int getPostalCode() { return postalCode; } public void setPostalCode(int postalCode) { this.postalCode = postalCode; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public int getLandline() { return landline; } public void setLandline(int landline) { this.landline = landline; } public int getCellphone() { return cellphone; } public void setCellphone(int cellphone) { this.cellphone = cellphone; } } Listato 6.1. Superclasse Customer in Java Costruiamo, ora, le sottoclassi Private e Company, riportate, rispettivamente, nei Listati 6.2 e 6.3. Per poter implementare una sottoclasse è necessario postporre la parola chiave extends alla fine della dichiarazione della stessa classe, e subito prima della parentesi graffa che chiude il token. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Private extends Customer{ private String name; private String surname; private String taxCode; private String documentNum; public Private() {} public Private(int idCustomer, int postalCode, int landline, intcellphone, String address, String city, String email, String name, String surname, String taxCode, String documentNum) { super(idCustomer, postalCode, landline, cellphone, address, city, email); this.name = name; this.surname = surname; this.taxCode = taxCode; this.documentNum = documentNum; } public String getName() { return name; } public void setName(String name) { this.name = name; } 6.2 Riuso del software in Java 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 83 public String getSurname() { return surname; } public void setSurname(String surname) { this.surname = surname; } public String getTaxCode() { return taxCode; } public void setTaxCode(String taxCode) { this.taxCode = taxCode; } public String getDocumentNum() { return documentNum; } public void setDocumentNum(String documentNum) { this.documentNum = documentNum; } } Listato 6.2. Sottoclasse Private 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Company extends Customer{ private String vatNum; private String businessName; public Company() { } public Company(int idCustomer, int postalCode, int landline,int cellphone, String address, String city, String email, String vatNum, String businessName) { super(idCustomer, postalCode, landline, cellphone, address, city, email); this.vatNum = vatNum; this.businessName = businessName; } public String getVatNum() { return vatNum; } public void setVatNum(String vatNum) { this.vatNum = vatNum; } public String getBusinessName() { return businessName; } public void setBusinessName(String businessName) { this.businessName = businessName; }} Listato 6.3. Sottoclasse Company Se la superclasse prevede incapsulamento, le sottoclassi non potranno ereditare i termini del costruttore, poichè essi saranno marcati come private. Per poter risolvere questo problema, nel costruttore della sottoclasse sarà invocato il metodo costruttore della superclasse. Tale operazione è resa possibile poichè i metodi d’accesso alle variabili, a differenza delle variabili stesse, sono marcati come public. Per effettuare tale operazione, si introduce la parola chiave super, inserendo come parametri formali gli attributi in comune fra la superclasse e la sottoclasse. Pertanto, anche se le sottoclassi Private e Company non possiedo, effettivamente, le variabili della superclasse, possono comunque implementarle, virtualmente, attraverso l’incapsulamento. La sottoclasse sarà sempre obbligata a inserire il costrutto super all’inizio del proprio costruttore, anche in caso di metodo costruttore vuoto. La parola chiave super obbliga, pertanto, il programmatore a seguire un paradigma di programmazione tipicamente OOP. Subito dopo aver invocato il costruttore della superclasse, verranno inseriti i parametri appartenenti alle stesse sottoclassi. 84 6 Java vs Go: Riuso del software La superclasse Object L’idea principale della programmazione OOP riflette, essenzialmente, la quotidianità riscontrabile nel mondo reale. Mentre è semplice descrivere le caratteristiche e le funzionaltà che appartengono, ad esempio, ad un’aereo, risulta estremamente complesso spiegare, nel dettaglio, gli intricati meccanismi che permettono a quell’aereo di avere quel tipo di caratteristiche e quel tipo di comportamento. Analogalmente, il paradigma OOP tenta di descrivere, nella maniera più semplice possibile, gli attributi e i metodi che uno specifico software implementa. La documentazione API di Java è stata organizzata in modo tale da soddisfare ogni sfaccettatura delle OOP. Le librerie, infatti, mettono a disposizione del programmatore una preesistente classe denominata Object che “astrae”il concetto di oggetto stesso. Essa appartiene al package java.lang e può essere, tranquillamente, definita come la superclasse di tutte le classi. La classe Object occupa, pertanto, la cima di ogni piramide che si viene a creare tramite l’utilizzo della gerarchizzazione. Possiamo, pertanto, affermare che, quando viene realizzata una classe, implicitamente essa estende la classe Object. Quando realizziamo una classe qualsiasi, essa implementerà, automaticamente, tutti i metodi messi a disposizione da Object. I metodi di questa classe possono essere, sostanzialmente, raggruppati in due tipologie: • Metodi in override: Sono i metodi che possono essere sovrascritti dalle sottoclassi di Object (Listato 6.4). Essi sono: – clone(): permette di clonare un oggetto preesistente. Il metodo clone è stato utilizzato nel Listato 5.7. – equals(): è un metodo boolean che restituire true se i due oggetti ricevuti in ingresso sono identici. – finalize(): permette di garantire un uso corretto delle risorse esterne, non gestibili correttamente dalla garbage collection. – toString(): restituisce una stringa contenente le informazioni dell’oggetto. • Metodi non in override. Essi sono: – getClass(): restituisce la gerarchia e le interfacce collegate ad una particolare classe. – notify(), notify(), wait(): le loro caratteristiche sono state esplicate nella descrizione del Listato 3.10. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Classe { @Override protected Object clone() throws CloneNotSupportedException{ return super.clone(); } @Override public boolean equals(Object arg0) { return super.equals(arg0); } @Override protected void finalize() throws Throwable { super.finalize(); } @Override public int hashCode() { return super.hashCode(); } @Override public String toString() { return super.toString(); } } 6.2 Riuso del software in Java 85 Listato 6.4. Metodi della classe Object in override 6.2.2 Valutazioni sul riuso del software Il linguaggio di programmazione Java è stato concepito, fin dal dall’origine, come predisposto al riuso del software. L’obiettivo dichiarato dal “Green Team” era, infatti, quello di realizzare un sistema software basato su una macchina virtuale che potesse eseguire il bytecode generato, indipendentemente dalla piattaforma. L’indipendenza dell’architettura permette un riuso del codice che non ha pari in altri linguaggi di programmazione; garantire le funzionalità di un programma, a prescindere dalla piattaforma sui cui il software è implementato, evita sia la duplicazione che la reinterpretazione di un codice che deve essere adattato, volta per volta, per le conversioni ad altre piattaforme. Tale meccanismo, pertanto, risponde, perfettamente, al concetto di riutilizzo del software. La presenza di una libreria vastissima e in costante aggiornamento evita, inoltre, al programmatore, la stesura di codice per la risoluzione di problemi già risolti dalle librerie. Non sono soltanto la JVM e la documentazione API a garantire il riuso del codice. Java, infatti, è stato pensato per essere un linguaggio OOP a tipizzazione statica in modo da favorire la modularità e, soprattutto, il riuso. Utilizzando i concetti tipici dei linguaggi OOP, Java suddivide i problemi che un programma deve risolvere in classi di problemi; i programmi, pertanto, non saranno costituiti da un blocco di codice unico, ma da classi che definiscono l’“astrazione” degli oggetti che devono instanziare. Tramite il meccanismo dell’incapsulamento, inoltre, si pone la netta suddivisione fra le “astrazioni” degli oggetti, assicurando sia la robustezza che la modularità. L’“astrazione” data dalle classi implica sia quella dei dati che quella dei metodi. Quest’ultima rende possibile la risoluzione di problemi simili o identici, delegandoli ad un unico sottoprogramma, senza dover, pertanto, scrivere porzioni duplicate di codice. I metodi, inoltre, essendo suddivisi per classi, sono di facile lettura. Il programmatore, pertanto, sarà incoraggiato ad usufruire di quelli, già definiti, favorendo, ancora una volta, il riutilizzo del sofrware. Per di più, l’implementazione dei metodi per le classi può essere garantita, in fase di progettazione, tramite un oculato uso delle interfacce. L’ereditarietà, inoltre, consente di estendere e specializzare il codice già scritto. Tale meccanismo, come è stato possibile verificare nella sezione precedente, permette di ereditare il campo di una classe, per poi estenderlo, aggiungendo, se è necessario, altri attributi. L’ereditarietà riguarda, anche, i metodi; possiamo, pertanto, utilizzare i metodi dichiarati dalle superclassi e, in caso, sovrascriverli, tramite un polimorfismo di tipo override, per adattarli e renderli compatibili con le specifiche della sottoclasse. La strutturazione gerarchica, tipica dell’ereditarietà, assicura, ancora di più, la leggibilità dei programmi e garantisce, pertanto, un’ulteriore riduzione dei tempi di sviluppo grazie al riutilizzo del codice. 86 6 Java vs Go: Riuso del software Non è, inoltre, da sottovalutare la presenza della classe Object. Essa, occupando la cima della piramide, garantisce, automaticamente, il riutilizzo del software; ogni classe, infatti, può sovrascrivere o usuifruire dei suoi utilissimi metodi, senza doversi preoccupare di redigire un codice dedicato. A permettere il riuso del software è, anche, la possiblità di spostare classi e interfacce da un progetto all’altro, tramite un semplice e banale “copia e incolla” dei file con estensione .java. La JVM, la documentazione API, i file .java, l’implementazione delle interfacce, le classi, l’ereditarieta, l’incapsulamento e il polimorfismo, combinati insieme e implementati con una sufficiente “astrazione”, garantiscono, senza dubbio, il riuso ottimo del software nel linguaggio di programmazione Java. 6.3 Riuso del software in Go 6.3.1 Embedding Nella scrittura di un codice di un programma Go è possibile riscontrare, cosı̀ come in Java, la presenza di numerose struct caratterizzate dall’avere campi, fondamentalmente, simili o identici. Pertanto, è necessario un meccanismo che permetta di snellire la stesura del codice e riutilizzare le componenti software, similmente a come si è visto con il concetto dell’ereditarietà. Tuttavia, Go non è tipicamente OOP e non presenta, di conseguenza, il concetto di oggetto e classe in maniera definita. L’“astrazione”, pertanto, non è applicabile. Infatti, un approccio basato sulle gerarchie dell’ereditarietà si rivela, in questo caso, un approccio sostanzialmente errato. L’assenza di classi comporta, naturalmente, l’assenza delle sottoclassi che le estendono e delle superclassi che le includono. Bisogna, pertanto, trovare una soluzione simile, ma alternativa, all’ereditarietà. Lo stesso Rob Pike dà una possibile soluzione: “if C++ and Java are about type hierarchies and the taxonomy of types, Go is about composition.”1 Le relazioni che intercorrono fra le struct non si costruiscono per estensione, ossia A“is a” B, bensı̀ per composizione, ossia A “has a” B. Bisogna, pertanto, non far ereditare a una struct gli attributi di un’altra, ma far si che essa la incorpori. Il meccanismo con cui far incorporare ad una struct le procedure e i campi di un’altra è chiamato, nel linguaggio di programmazione Go, “embedding”. Per quanto riguarda il case study di interesse, il Listato 6.5 è equivalente ai Listati 6.1, 6.2 e 6.3. 1 2 3 4 5 6 1 package main import "fmt" type Customer struct { idCustomer int postalCode int https://www.youtube.com/watch?v=5kj5ApnhPAE 6.3 Riuso del software in Go 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 87 landline int cellphone int address string city string email string } type Company struct { Customer vatNum string businessName string } type Private struct { Customer name string surname string taxCode string documentNum string} Listato 6.5. Esempio di embedding Come è possibile notare, implementare l’embedding, dal punto di vista sintattico, è molto semplice. Basta inserire all’interno del campo di una struct, come primo elemento, il nome della struct da incorporare. Per poter instanziare una struct, si segue la procedura illustrata nel Listato 6.6. 1 2 3 4 func main() { Cmp1 := Company{Customer{1,89100,0,328,"Via A n.1","Rho","[email protected]"}, "1212311","Golang Inc"} Prv1 := Private{Customer{2,20141,0,333,11212,"Via B n.7","Roma","[email protected]"}, "Rob","Pike","RBKT","j542"} } Listato 6.6. Instaziazione di struct embedding Per poter stampare le due istanze si utilizzerà il programma riportato nel Listato 6.7, il quale produrrà il risultato riportato in Figura 6.3. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package main import "fmt" type Customer struct { idCustomer int postalCode int landline int cellphone int address string city string email string } type Company struct { Customer vatNum string businessName string } type Private struct { Customer name string surname string taxCode string documentNum string } func (c *Company) DatiPrincipali() { fmt.Println("Cliente n.:", c.idCustomer, ", P.Iva:", c.vatNum, ", BusinessName:", c.businessName, ", telefono:", c.cellphone, "\n") } func (p *Private) DatiPrincipali() { fmt.Println("Cliente n.:", p.idCustomer, ", Nome:", p.name, ",Cognome:", p.surname, ",Codice fiscale:", p.taxCode) } func main() { Cmp1 := Company{Customer{1, 2, 3, 4, "Samarren.3", "FDS", "[email protected]"}, "X5", "Go Inc"} Prv1 := Private{Customer{2, 20, 0, 333, "Via B n.7", "Roma", "[email protected]"}, "Rob", "Pike", "RBKT", "j542"} Cmp1.DatiPrincipali() Prv1.DatiPrincipali() } 88 6 Java vs Go: Riuso del software Listato 6.7. Dati principali Figura 6.3. Risultato del programma riportato nel Listato 6.7 Naturalmente, è possibile utilizzare struct in embedding in sinergia con le interfacce. Nel Listato 6.8 viene descritto un esempio di come si possa realizzare tale meccanismo. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package main import "fmt" type Transactionable interface { Withdraw(float64) float64 Deposit(float64) float64 } type CurrentAccount struct { balance float64 number string iban string } type InterestBearingAccount struct { CurrentAccount percentage float64 } func (ca CurrentAccount) Withdraw(amount float64) float64 { fmt.Println("Conto n.", ca.number, "Saldo:", ca.balance, ",Ritiro:", amount) return (ca.balance - amount) } func (iba InterestBearingAccount) Withdraw(amount float64) float64 { var newBalanceInts float64 newBalanceInts = iba.balance +(iba.balance*iba.percentage)/100 fmt.Println("Conto n.", iba.number, ",Saldo+interessi:",newBalanceInts, ",Ritiro:", amount) return (newBalanceInts - amount) } func (ca CurrentAccount) Deposit(amount float64) float64 { fmt.Println("Conto n.", ca.number, "Saldo:", ca.balance,",Deposito:", amount) return (ca.balance + amount) } func (iba InterestBearingAccount) Deposit(amount float64) float64 { var newBalanceInts float64 newBalanceInts = iba.balance +(iba.balance*iba.percentage)/100 fmt.Println("Conto n.", iba.number, ",Saldo+interessi:",newBalanceInts, ", Deposito:", amount) return (newBalanceInts + amount) } func main() { CA := CurrentAccount{balance: 560, number: "A", iban: "IB"} c := Transactionable(CA) IBA := InterestBearingAccount{CurrentAccount{1100, "B","ITK"}, 5} d := Transactionable(IBA) var amount float64 amount = 100 fmt.Println("Nuovo fmt.Println("Nuovo fmt.Println("Nuovo fmt.Println("Nuovo Saldo:", Saldo:", Saldo:", Saldo:", c.Withdraw(amount)) d.Withdraw(amount)) c.Deposit(amount)) d.Deposit(amount)) } Listato 6.8. Embedding e interfacce Il programma presenta due struct, ossia CurrentAccount e InterestBearingAccount; La seconda struct incorpora, tramite il processo di embedding, i campi della struct CurrentAccount. Nel main() viene instanziato, inizialmente, un conto corrente. Successivamente, viene implementata l’interfaccia (similmente a come era stato fatto nel Listato 5.27), Transactionable, composta dalle “firme” delle func Withdraw e Deposit. 6.3 Riuso del software in Go 89 La struct deve, pertanto, essere riferita ai metodi dell’interfaccia; vengono, pertanto, implementate le func Withdraw e Deposit, legate al CurrentAccount, tramite un puntatore. Successivamente, viene instanziata la struct InterestBearingAccount. Anch’essa implementa l’interfaccia, similmente a quanto visto da CurrentAccount, tuttavia le func sono state adattate per calcolare, al momento del deposito e del ritiro, l’interesse accumulato. In seguito, viene inizializzata la variabile amount e invocate le funzioni per le due struct alle quali sarà passata, come parametro formale, la stessa variabile amount. Il programma produrrà il risultato indicato in Figura 6.4. Figura 6.4. Risultato del programma riportato nel Listato 6.8 6.3.2 Valutazioni sul riuso del software La composizione realizzata tramite embedding supporta, perfettamente, eventuali aggiornamenti dei campi delle struct Incorporare dei componenti da una struct, anzichè ereditarli, garantisce, infatti, che, nel caso di aggiornamento delle specifiche di una struct componente, non si sia necessario un eccessivo notevole aggiornamento dei campi relativi struct composte. Attraverso la composizione, infatti, basterebbe non incorporare o incorporare in parte, i campi della struct componente aggiornata. Dal punto di vista teorico, l’embedding può accogliere facilmente futuri cambiamenti delle specifiche, rispetto al meccanismo dell’ereditarietà proprio del linguaggio di programmazione Java. L’embedding, infatti, prevede un approccio dinamico e assicura un riuso ottimale del codice. L’ereditarietà, invece, utilizza un approccio statico; in caso di aggiornamento, per esempio, risulta difficile cambiare, implementare o eliminare l’interfaccia di una superclasse, poichè tale modifica del codice si ripercuoterà, senza dubbio, su tutte le sottoclassi, che andrebbero, pertanto, riviste e corrette. Un riuso maggiore del codice avviene combinando l’embedding con le interfacce. Come è stato possibile appurare, struct composte possono incorporare sia i campi che le interfacce delle struct componenti, assicurando uno standard di implementazione simile a quello che veniva garantito dalla combinazione di interfacce ed ereditarietà; inoltre, attraverso l’embedding è possibile simulare sia la stessa ereditarietà, come si è voluto fare nostro caso di studio e sia l’ereditarietà multilpla. Tuttavia, l’embedding, non prevedendo una gerarchizzazione statica dei componenti è soggetto ha una difficile interpretazione e una difficile lettura del codice che cozza con il concetto di riuso del software. 90 6 Java vs Go: Riuso del software Inoltre, Go, a differenza di Java, è un linguaggio compilato. Non essendo provvisto di una macchina virtuale, non viene assicurata l’indipendenza dalla piattaforma sottostante. Tale assenza comporta una riscrittura del codice in modo che quest’ultimo venga adattato alle specifiche della piattaforma su cui si vuole implementare il servizio. Pertanto, anche da questo punto di vista, Go va contro alle buone norme di riuso del software. 6.4 Conclusioni Il riuso del software nel linguaggio di programmazione Java è il punto forte sul quale hanno puntato gli sviluppatori. Come abbiamo potuto vedere nel dettaglio in questo capito e in quelli precendenti, Java preferisce avere un linguaggio di programmazione corposo ma chiaro, leggibile e facilmente riutilizzabile. Tale prolissità del linguaggio non insidia la facilità di programmazione. I supporti software e le librerie messi a disposizione del programmatore veicolano la corretta implementazione delle classi e delle interfacce, che per la loro natura tipicamente OOP, garantiscono, per definizione, un riuso del software ottimale. Go, invece, ha compiuto una scelta differente; gli sviluppatori di casa Google hanno improntato il linguaggio in modo tale che esso possa snellire la complessità di programmazione, soprattutto dal punto di vista sintattico, permettendo di scrivere, in poche righe, un programma equivalente a decine e decine di linee di codice in linguaggio Java. Entrambi gli aspetti sembrano essere condivisibili; tuttavia, dal punto di vista, puramente, progettuale, e quindi di riuso del software, è doveroso evidenziare che sacrificare la leggibilità per la sintesi, in progetti software corposi e improntati sul lavoro di squadra, risulta essere controproducente. Il lavoro di squadra, infatti, implica l’adottare un livello di “astrazione” condivisibile, sufficiente a stilare una documentazione chiara e precisa. Java, attraverso i meccanismi dell’ereditarietà e grazie alla presenza delle classi astratte, si presta particolarmente allo sviluppo di progetti importanti, in quanto le stesse gerarchizzazioni statiche permettono una lettura comprensibile e fissano uno standard di programmazione elevato per tutti gli sviluppatori del progetto. Go utilizza, invece, un approccio dinamico; la presenza di elementi quali puntatori, interfacce vuote e sintesi estrema del codice rendono di difficile attuazione un approccio standard; pertanto, il codice appare caratterizzato da una scarsa leggibilità e da una difficile comprensione, soprattutto per ciò che riguarda il riuso di software preesistente. Nella sezione precedente, si è affermato che l’embedding può risultare l’approccio più conveniente da adottare, soprattutto in presenza di massicci aggiornamenti. Tuttavia, anche la programmazione OOP supporta, perfettamente, il meccanismo di composizione. Una porzione di codice identica a quella scritta precedentemente nel Listato 6.5 viene riportata nel Listato 6.9. 1 2 3 4 5 public class Private{ private String private String private String private String name; surname; taxCode; documentNum; 6.4 Conclusioni 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 private Customer customer; public Private() {} public Private(String name, String surname, String taxCode, String documentNum, Customer customer) { super(); this.name = name; this.surname = surname; this.taxCode = taxCode; this.documentNum = documentNum; this.customer = customer; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSurname() { return surname; } public void setSurname(String surname) { this.surname = surname; } public String getTaxCode() { return taxCode; } public void setTaxCode(String taxCode) { this.taxCode = taxCode; } public String getDocumentNum() { return documentNum; } public void setDocumentNum(String documentNum) { this.documentNum = documentNum; } public Customer getCustomer() { return customer; } public void setCustomer(Customer customer) { this.customer = customer; } } Listato 6.9. Composizione in Java 91 7 Conclusioni e uno sguardo sul futuro Il percorso intrapreso nella stesura della tesi è maturato da un attento lavoro di ricerca per poter elaborare al meglio un confronto fra le peculiariarità dei linguaggi di programmazione Java e Go. Inizialmente, sono state descritte la storia e le caratteristiche principali dei due linguaggi; si è potuto, pertanto, effettuare un primo raffronto evidenziando il paradigma OOP e l’interdipendenza dell’architettura tipiche di Java nonchè la sintassi semplice e sintetica di Go. Successivamente, si è introdotto un caso di studio basato sul modello delle transazioni bancarie, per poter mettere in evidenza la diversa gestione della programmazione concorrente. Durante le fasi di lavoro è stato constatata la maggiore efficienza in merito alle tempistiche di esecuzione dei thread di Java rispetto alle goroutine. Un ulteriore confronto è stato svolto rispetto alla gestione delle funzioni e delle interfacce; in queste fasi della tesi si è potuta notare una capacità di risposta ai problemi posti sostanzialmente simile per i due linguaggi, ma una leggibilità maggiore per Java, grazie alla maggiore “astrazione” che esso riesce ad ottonere con il supporto del paradigma OOP. Tale confronto ci ha indotti, alla fine del nostro viaggio, ad espandere il concetto di riuso del codice e la sua implementazione nei due linguaggi. Java, in questo caso, si dimostra superiore, anche se i meccanismi dell’embedding, tipici di Go, risultano più “performanti” in struct in continuo aggiornamento rispetto all’ereditarietà. Tuttavia, Java può implementare entrambi i meccanismi, mentre Go ne è impossibilitato. In conclusione, possiamo affermare che non esiste un linguaggio perfetto. L’aumento delle specifiche dei calcolatori porta, inesorabilmente, ad un aumento del codice da scrivere; pertanto, serve una squadra numerosa in grado di poter riutilizzare codice con una sufficiente “astrazione”, senza dare adito a potenziali incomprensioni. Tale chiarezza, negli ultimi anni, risulta garantita solo da Java. Ma esso è, notoriamente, piuttosto verboso; pertanto, il codice da scrivere aumenta e il ciclo ricomincia. Go tenta di spezzare tale ciclo attraverso un linguaggio caratterizzato dalla sintassi leggera e dall’utilizzo dei puntatori. Ma proprio la sua sintassi, non permettendo una buona “astrazione”, causa scarsa lettura del codice che, pertanto, scoraggerà lo sviluppo in team e, quindi, il riuso. 94 7 Conclusioni e uno sguardo sul futuro Go, pertanto, fallisce, secondo il nostro punto di vista, nell’obiettivo di “rottamare” Java. C’è ancora molto altro da dire per quanto riguarda i due linguaggi. Essi sono, infatti, in continua evoluzione; recentemente Java sembra voler affiancare alla strada del rispetto al paradigma OOP, una programmazione tipicamente funzionale (ne è un esempio l’introduzione delle funzioni anonime), mentre Go tenta di migliorare la strada della concorrenza e del parallelismo. Pertanto, sarà interessante vedere, nel prossimo futuro, come si evolverà il rapporto fra questi due linguaggi. Ringraziamenti Desidero ringraziare tutti coloro che mi hanno aiutato nella stesura di questa tesi di laurea: ringrazio, innanzitutto, il Professor Domenico Ursino, mio Relatore, che mi ha guidato in questo percorso con la sua disponibilità e la sua professionalità. Proseguo ringraziando tutti i colleghi e gli amici che mi hanno incoraggiato in questi anni di università. Vorrei, infine, ringraziare le persone a me più care: mio padre Giovanni, mia madre Graziella e mia sorella Silvia; il loro sostegno e il loro affetto mi ha permesso di raggiungere questo traguardo. A voi va tutto il mio affetto e tutta la mia gratitudine. Riferimenti bibliografici 1. So why did they decide to call it Java? http://www.javaworld.com/article/ 2077265/core-java/so-why-did-they-decide-to-call-it-java-.html, 1996. 2. Write once, run anywhere? http://www.computerweekly.com/feature/Write-oncerun-anywhere, 2002. 3. OSCON 2010: Rob Pike, “Public Static Voi”. https://www.youtube.com/watch?v= 5kj5ApnhPAE, 2010. 4. Rob Pike - “Concurrency Is Not Parallelism”. https://vimeo.com/49718712, 2012. 5. Overview of the four main programming paradigms. http://people.cs.aau. dk/~normark/prog3-03/html/notes/paradigms_themes-paradigm-overviewsection.html, 2013. 6. Oracle and IBM Collaborate to Accelerate Java Innovation Through OpenJDK. http: //www.oracle.com/us/corporate/press/176988, 2015. 7. Programmazione a oggetti, cos’è e come funziona. http://www.fastweb.it/ internet/programmazione-a-oggetti-cos-e-e-come-funziona/, 2015. 8. Informazioni sulla tecnologia Java. https://www.java.com/it/about/, 2016. 9. Java Platform, Standard Edition 7 API Specification. https://docs.oracle.com/ javase/8/docs/api/, 2016. 10. Java Platform, Standard Edition 8 API Specification. https://docs.oracle.com/ javase/8/docs/api/, 2016. 11. The Go Programming Language, Documentation. https://golang.org/doc/, 2016. 12. The Go Programming Language, Packages. https://golang.org/pkg/, 2016. 13. The Go Programming Language, The Go Project. https://golang.org/project/, 2016. 14. The Golang Programming Language, Frequently Asked Questions (FAQ). https: //golang.org/doc/faq, 2016. 15. The Java Programming Language. http://www.tiobe.com/tiobe_index?page=Java, 2016. 16. J. Arlow and I. Neustadt. UML 2 e Unified Process - Analisi e progettazione ObjectOriented. McGraw-Hill, Milano, 2006. 17. P. Atzeni, S. Ceri, S. Paraboschi, and R. Torlone. Basi di Dati - Modelli e linguaggi di interrogazione. McGraw Hill, Milano, 2009. 18. I. Balbaert. The Way to Go: A Thorough Introduction to the Go Programming Language. iUniverse Inc, Bloomington, Indiana, U.S.A., 2012. 19. J. Byous. Java Technology: An Early History. https://www.santarosa. edu/~dpearson/mirrored_pages/java.sun.com/Java_Technology_-_An_early_ history.pdf, 2005. 98 Riferimenti bibliografici 20. C. De Sio Cesari. Manuale di Java 7: Programmazione orientata agli oggetti con Java Standard Edition 7. Hoepli, Milano, 2012. 21. O.J. Dahl. The Birth of Object Orientation: the Simula Languages. http://www. olejohandahl.info/old/birth-of-oo.pdf, 2001. 22. H.M. Deitel, P.J. Deitel, R. Ranon, and A. Baruzz. Java. Fondamenti di programmazione. Apogeo S.R.L., Milano, 2006. 23. N. Deshpande, E. Sponsler, and N. Weiss. Analysis of the Go runtime scheduler. http://www.cs.columbia.edu/~aho/cs6998/reports/12-12-11_ DeshpandeSponslerWeiss_GO.pdf, 2015. 24. A. Donovan and B.W. Kernighan. The Go Programming Language. Financial Times/Prentice Hall, 2015. 25. J. Gosling, B. Joy, G. Steele, G. Bracha, and A. Buckley. The Java Language Specification, Java SE 8 Edition (Java Series). Addison-Wesley Professional, Boston, Massachusetts, U.S.A., 2014. 26. M. Hicks. Interview with Go’s Russ Cox and Sameer Ajmani. http://www.plenthusiast.net/2015/03/25/interview-with-gos-russ-cox-and-sameerajmani/, 2015. 27. C. Horstmann. Concetti di Informatica e fondamenti di Java 2 Tecniche avanzate(II Edizione) Edizione italiana a cura di Marcello Delpasso. Apogeo S.R.L., Milano, 2002. 28. C. Horstmann. Concetti di Informatica e fondamenti di Java (IV Edizione) Edizione italiana a cura di Marcello Delpasso. Apogeo S.R.L., Milano, 2007. 29. W. Kennedy, B. Ketelsen, and E.S. Martin. Go in Action. Manning Publications, Greenwich, Connecticut, U.S.A., 2015. 30. T. Lindholm, F. Yellin, G. Bracha, and A. Buckley. The Java Virtual Machine Specification: Java SE 8 Edition. Financial Times/Prentice Hall, 2014. 31. P. Niemeyer and D.Leuck. Learning Java: Edition 4. O’Reilly Media Inc, Sebastopol, California, U.S.A., 2013. 32. R. Ramakrishnan and J. Gehrke. Sistemi di Basi di Dati. McGraw Hill, Milano, 2004. 33. P. Van Roy. Programming Paradigms for Dummies: What Every Programmer Should Know. https://www.info.ucl.ac.be/~pvr/VanRoyChapter.pdf, 2016.