In questo post si vuole indagare il supporto alla programmazione a oggetti fornito da Scala, prendo come guida esplicitamente il libro di Odersky che si può acquistare qui.
La metafora più abusata per descrivere per la prima volta la relazione tra classi e oggetti è certamente quella dello stampo per biscotti e i biscotti. Cioè come lo stampo una volta creato ci permette di formare il numero di biscotti che desideriamo, così la classe una volta definita ci permette di creare un oggetto ogni qual volta ne abbiamo necessità. Sperando di non attirarci le ire di nessuno muoviamo una piccola critica alla storica metafora. Lo stampo per biscotti riesce a renderci tutti i biscotti che vogliamo nella forma desiderata, ma non ci garantisce nulla sul loro gusto, sul loro grado di friabilità, su quanto velocemente assorbe il latte una volta inzuppato, in poche parole lo stampo non ci garantisce nulla sul comportamento del futuro biscotto e ne definisce solo la sua forma, la sua struttura.
Ora, uno dei principi fondanti della programmazione a oggetti è l’incapsulamento e cioè di incorporare negli oggetti i dati (struttura) e le funzionalità sotto forma di metodi di accesso ai dati (comportamento) per evitare un utilizzo improprio dei dati.
Senza stravolgere la millenaria prassi, la nostra metafora manterrà lo stampo per biscotti, ma allo stesso allega una ricetta per preparare l’impasto e fornire una cottura adeguata, in questo modo saremo certi che i nostri biscotti saranno proprio quelli che desideriamo.
Torniamo a Scala, come in Java, la parola chiave class è designata alla dichiarazione delle nostre classi, definiremo così i nostri campi e i nostri metodi con def.
class Persona(){
private var: String nome;
var: Int eta;
val: Date dataDiNascita;
def ciao():Unit = println(“hello world”);
}
e istanzieremo la classe Persona con la parola chiave new
val ugo = new Persona;
ugo.nome= “Ugo”
ugo.eta= 33
osserviamo la sintassi molto simile a Java, ma ci sono particolari significativi che differiscono nei due linguaggi, il modificatore di accesso di default per scala è public, quindi possiamo ometterlo per campi e metodi pubblici, e indicare private quando vogliamo rendere privato un campo o un metodo, infatti l’espressione ugo.nome= “Ugo” causerà un errore in quanto si tenta di accedere a un campo privato. L’aver dichiarato come val l’oggetto ugo, non ci impedisce di modificarne il campo età, in questo caso l’oggetto è immutabile nel senso che la variabile ugo non potrà fare riferimento a nessun altro oggetto di tipo Persona, ma essendo eta un campo var, si puo’ riassegnare il suo valore.
Il tipo di ritorno del metodo ciao() è Unit, in Scala questo tipo corrisponde all’ incirca al tipo void per Java, e ci dice che la funzione non restituisce nessun valore, ma evidentemente ha degli effetti collaterali, in questo caso scrive sullo standard output, da notare che omettendo il tipo di ritorno Unit, il compilatore l’avrebbe inferito e quindi la definizione di ciao() sarebbe stata equivalente.
Costruttori di classe
In Scala tutto il codice all’interno della graffe, fa parte del costruttore primario e inoltre possiamo passare i parametri per il costruttore primario direttamente nelle tonde subito dopo la definizione del nome della classe.
class Punto( val x: Float, val y: Float)
class Cerchio( raggio: Float, centro: Punto){
require(raggio != 0) // stabiliamo una precondizione
private val r=raggio;
private val c: Punto = centro;
def area() : Float = scala.math.Pi *r*r;
def draw() = paint(this);
}
mioCerchio = New Cerchio(2.4, new Punto(-1,-1))
la possibilità di definire un costruttore ausiliare in scala esiste, ma viene gestita in maniera differente da come siamo abituati in Java, possiamo costruire tutti i costruttori ausiliari che vogliamo, ma devono sempre richiamare direttamente o mediante una catena di costruttori ausiliari il costruttore primario. Se volessimo, ad esempio, aggiungere alla classe Cerchio un costruttore ausiliare che definisce cerchi centrati nell’ origine degli assi prendendo il solo raggio come parametro
def this(raggio: Float) = this(raggio, new Punto(0,0))
Scala supporta la programmazione a oggetti con due altri concetti fondamentali, gli oggetti singleton e I tratti, definibili con le parole chiave object e trait. Vedremo come agiscono e come rendano differente l'approccio rispetto a Java.
In Scala una classe non può avere campi o membri statici, a questo (ma non solo) ovviano gli oggetti singleton, la sintassi è simile a quella per le classi
object Cerchio {
var : String stileCerchi=”tratteggiato”;
def setStileCerchi(stile: String) stileCerchi=stile;
def getStileCerchi(): String = this.stileCerchi
}
quando un oggetto singleton è definito nello stesso file della classe e ne condivide il nome, diciamo che l'oggetto è l'oggetto companion della classe e viceversa e i campi e metodi si comportano come campi e membri statici, possono essere acceduti con la sintassi;
Cerchio.setStileCerchi(“ombreggiato”)
oggetti e classi possono accedere a campi e metodi dei propri companion.
Gli oggetti singleton non sono tenuti ad avere una classe companion e in questo caso vengono detti standalone e possono essere utilizzati in svariati modi, come raccogliere dei metodi di utilità , o essere l'entry-point per una applicazione:
object PrimaAppScala{
def main(args: Array[String]) {
println “ecco i parametri di questa applicazione”
for (arg ← args)
println(arg)
}
}
questo è il modo con cui si può scrivere una applicazione in scala, il metodo main (args[]) come in Java, ma associato a un oggetto singleton.
Possiamo scrivere questo codice in un file
esempio.scala
non è obbligatorio nominare il file come la classe o l'oggetto che contiene e si possono raccogliere piu' definizioni di classiesempio.scala e oggetti all' interno dello stesso fille. Compilarlo da linea di comando:
# scalac esempio.scala
ed eseguirlo:
#scala esempio primo secondo terzo
avremo in output:
ecco i parametri di questa applicazione
primo
secondo
terzo
#
Traits (tratti) possiamo pensare ai tratti come a delle interfacce java, ma che possono avere sia campi che metodi con la relativa implementazione, tanto da far pensare a una sorta di ereditarietà multipla.
Differenti tratti possono essere pluggati in una classe rendendola capace di gestire qualche particolare aspetto. Vediamo un esempio:
class Persona(var nome:Strint, var eta:Int){
private var: String nome;
var: Int eta;
def saluta():Unit = println(“hello world”);
}
trait Elettore {
private numeroTesseraElettorale: BigInt
private List partecipazioneElezioni;
def getTessera(): BigInt = numeroTesseraElettorale
def setTessera(codice: BigInt) numeroTesseraElettorale=codice
def vota() = println “fatto”
}
class Votante() extend Persona with Elettore {
require(eta >= 18)
override def saluta() = println “hello political world”
}
val mRossi = new Votante (“Mario Rossi”, 35)
i tratti definiscono anche un tipo, infatti possiamo creare una variabile di tipo Elettore e assegnargli qualsiasi oggetto appartenente a una classe che utilizza il tratto.
val mr: Elettore = mRossi
i tratti insieme alle classi astratte ci forniscono un incredibile strumento per il riutilizzo e la scrittura di codice coinciso che rende pluggabili nuovi comportamenti alle nostre classi , vediamo un esempio.
abstract class coda {
def push( x: Int)
def get(): Int
}
e realizziamo una classe concreta CodaSemplice che la realizza
import scala.collection.mutable.ArrayBuffer
class CodaSemplice extends Coda{
private val buf = new ArrayBuffer[Int]
def get() = buf.remove(0)
def put(x: Int) { buf += x }
}
trait FiltraNegativi extends Coda{
abstract override def put(x: Int) {
if (x >= 0) super.put(x)
}
}
vogliamo definire un tratto che inserisce l'elemento nella coda solo se è non negativo , analizziamo la definizione del tratto FiltraNegativi, notiamo subito che il tratto estende la classe astratta coda, questo significa che il tratto potrà essere utilizzato solo e soltanto da quelle classi che realizzano la classe astratta coda.
Il secondo aspetto importante di queste due righe di codice è che nella definizione del tratto troviamo una chiamata super.put(x) riferita a una classe astratta, questo a prima vista sembra almeno un po' strano e non sarebbe permesso nella definizione di una classe, ma nei tratti è permesso associando le parole chiave abstract override e questa chiamata verrà legata dinamicamente all' implementazione del metodo da parte della classe concreta che utilizzerà il tratto. Semplicemente dichiarando una variabile in questo modo
val codaPositivi= (new CodaSemplice Incrementing with FiltraNegativi )
otterremo una coda con il comportamento desiderato senza nessuna modifica al codice della classe concreta.