La programmazione ad oggetti

Con l'evoluzione dei linguaggi di programmazione verso l'alto livello di comprensibilità umana e la crescente gestione delle informazioni in maniera aggregata nei database, anche i tipi di valori che un programma può elaborare si sono evoluti. Nuovi tipi, chiamati tipi composti, sono costruiti aggregando valori, chiamati campi, per descrivere non un singolo aspetto della realtà ma un elemento della realtà. I valori di questo tipo composto sono chiamati oggetti (classi in alcuni linguaggi) e la programmazione che ne fa uso è chiamata programmazione orientata agli oggetti (in inglese OOP, Object Oriented Programming).

Per fare un esempio, se pensi ad un'auto come oggetto, puoi creare un tipo di valore chiamato Auto che aggrega un valore numerico per il numero delle porte, un valore booleano per indicare se ha il cambio automatico, un valore testuale per il nome del modello e così via. Gli oggetti possono avere funzioni che eseguono operazioni in cui sono coinvolti, ad esempio un'auto può sterzare. Queste funzioni sono chiamate metodi. L'oggetto può compiere queste operazioni ma sono dal programmatore, come il guidatore che nell'auto gira il volante per sterzare.

Progettazione di un oggetto

Definire un oggetto o creare un tipo di valore significa scrivere il codice che descrive le proprietà di un oggetto, chiamate anche campi o attributi, e i suoi metodi. Un oggetto può essere definito solo con proprietà, solo con metodi o con entrambi. In linea generale, gli oggetti sono definiti con l'ottica della riusabilità.

Questo significa che si tende a definire tipi generici e specializzarli in base alle necessità. Per riprendere l'esempio di poco fa, se volessi definire un tipo di valore che rappresenta un Fuoristrada, potresti definire un tipo Auto con le proprietà generiche e da questo poi definire il tipo Fuoristrada aggiungendo le proprietà caratteristiche. In termini informatici, si parla di ereditarietà quando un tipo è definito sulla base di un altro dal quale eredita appunto tutte le caratteristiche e diviene un discendente, ovvero un sotto-tipo del capostipite con il quale condivide un'identità come il cognome o il dna nelle famiglie in carne e ossa.

Classi e Istanze

Le classi sono un elemento fondamentale per la programmazione ad oggetti, la loro dichiarazione è fondamentale. Per poter dichiarare una classe in Kotlin dobbiamo seguire la seguente sintassi:

<modificatore/i> class <nome_classe> (constructor) (eventuali parametri del costruttore primario) {

// eventuale corpo della classe

}

Una classe è un modello di oggetto. È importante capire che quando dichiaro una classe, non ho fatto (ancora) niente. Ho solo specificato cosa voglio, ma senza aver creato nulla che fa quello che voglio. Ho creato una forma, uno stampino nel quale mettendo il gesso o la cera per creare effettivamente l'oggetto che potrò poi usare davvero. Definire una classe è come fare un progetto: progettare le biciclette non significa costruirle effettivamente. Solo quando il progetto viene effettivamente realizzato avremo una delle biciclette che abbiamo progettato. Proprio per questo le classi devono essere istanziate per poter realizzare un oggetto "vievente". Visto che quando dichiarate una classe, state solamente definendo il tipo dell'oggetto. Istanziare un tipo significa fare qualcosa, un'istanza, di quel tipo. Istanziare un oggetto significa creare un oggetto che occupa memoria e che ha (almeno) lo spazio per memorizzare i campi.

Immagine 2.5 - Esempio di classe cane ed istanza di una classe cane. Noi possiamo l'azione di abbiare all'interno della classe, ma senza avere un cane fisico non possiamo abbaiare.

Per poter realizzare un'istanza di classe in Kotlin dobbiamo seguire la seguente sintassi:

(eventuale variabile =) NomeClasse(parametri del costruttore)

Programmazione ad oggetti in Kotlin

Per prima cosa realizziamo la classe più semplice possibile: una classe senza corpo e senza niente e la istanziamo:

class MinimalClass

fun main(args: Array<String>) {

val minimalClass = MinimalClass()

println(minimalClass)

}

L'output sarà simile al seguente: MinimalClass@5e481248

In maniera simile al Java Kotlin implementa una generica conversione istanza - stringa: fatta nel seguente modo NomeClasse + "@" + hashCode(). Dove hashCode() di default torna l'indirizzo di memoria della suddetta istanza.

Natualmente le classi in Kotlin supportano gli attributi (variabili e costanti), le proprietà, le funzioni e i costruttori.

Attributi

Per dichiarare gli attributi di una classe utilizziamo le keyword val e var. Come già visto prima la keyword val serve per le variabili in sola lettura o costanti, mentre la keyword var serve per le variabili read/write:

class Order {

val item = item

var quantity = quantity

var price = price

}

Costruttori

I costruttori sono dei metodi speciali che consente di allocare, inizializzare e istanziare un nuovo oggetto, in Kotlin vengono fatte delle distinzioni tra costruttore primario e secondario:

  • Il costruttore primario determina anche i parametri interni alla classe;
  • I costruttori secondari permettono di inizializzare la classe altri modi.

Inoltre Kotlin prevede anche le property initializers e un blocco di inizializzazione.

Costruttore primario

In Kotlin c'è un forte legame tra classe e costruttore primario, si usa la keyword constructor per realizzare che accetta dei parametri:

class Person constructor(firstName: String) {

// ...

}

La keyword constructor può essere omessa se il costruttore primario non ha modificatori di visibilità:

class Person (firstName: String) {

// ...

}

Si, ora abbiamo dichiarato un costruttore, ma cosa c'è ne facciamo della variabile firstName?

Property initializers e blocco di inizializzazione

La assegnamo ad un'attributo all'interno della classe (tramite le property initializers):

class Person (firstName: String) {

var firstName = firstName

// ...

}

Kotlin ci permette anche di eseguire operazioni semplici sulla suddetta variabile:

class Person(firstName: String) {

var customerKey = firstName.toUpperCase()

val firstName = firstName

}

Ma se dobbiamo fare operazioni più complicate possiamo ricorrere al blocco di inizializzazione, preceduto dalla parola chiave init:

class Person(firstName: String) {

init {

logger.info("Customer initialized with value ${firstName}")

}

}

Possiamo utilizzare la suddetta notazione anche per costruttori che hanno più parametri:

class Order(item: String, quantity: Int, price: Double) {

val item = item

var quantity = quantity

var price = price

}

Variabili di classe dichiarate nel costruttore primario

Ma Kotlin ci permette di semplificare dichiarando gli attributi direttamente nel costruttore tramite var o val:

class Order(val item: String, var quantity: Int, var price: Double)

Il risultato tra lo snippet 2.26 e 2.27 è identico. Ricordo che se nel costruttore primario i parametri non sono contrassegnati con var o val le suddette variabili non sono visibile all'interno della classe, eccetto che nel blocco di inizializzazione e nelle assegnazioni agli attributi interni (property initializers):

class Customer(name: String) {

var keyName = name.toUpperCase() // ok

init {

println("Customer initialized with value ${name}") // ok

}

fun printName() {

println(name) // errore impossibile trovare name

}

}

Costruttore secondario

Il costruttore secondario richiede la keyword constructor. Esso deve richiamare il costruttore primario tramite keyword this. Inoltre è possibile usare la keyword this anche per accedere a campi della classe nascosti (nel senso che ci sono due campi con lo stesso nome) dai campi del costruttore:

class Person(val name: String) {

var surname : String = ""

constructor(name: String, surname: String) : this(name) {

this.surname = surname

}

}

È possibile omettere le parentesi graffe {} se il corpo del costruttore è vuoto:

class Person(val name: String, val surname: String) {

constructor(name: String) : this(name, "")

}

Ricordiamo che i vari costruttori devono differenziarsi per il numero e/o il tipo dei parametri. Analizzeremo l'importanza della differenza dei parametri successivamente. Il costruttore secondario non può avere parametri dichiarati con tramite var o val. Le proprietà inizializzate nel costruttore primario possono essere aggiunte implicitamente alla classe, mentre non è così nel caso del costruttore secondario, in questo caso deve essere fatta la copia del riferimento tramite l'operatore = o tramite la chiamata al costruttore primario (this()), se esso ha più parametri. Nessuno comunque ci vieta di invertitre i parametri tra il costruttore primario e secondario:

class Order(val item: String, var quantity: Int, var price: Double) {

constructor(item: String, price: Double, quantity: Int) : this(item, quantity , price)

}

Anche se questo codice non è elegante. Inoltre se il costruttore primario non prevede paraemtri è possibile omettere le parentesi tonde (). Le seguenti tre classi sono identiche:

class Prova constructor() {

}

class Prova () {

}

class Prova {

}

Istanze

Realizzare istanze di un oggetto, cioè realizzare un nuovo oggetto in esecuzione, è semplice basta usare il nome della classe seguito dai parametri:

val invoice = Invoice()

val customer = Customer("Joe Smith")

Ma se per esempio vogliamo realizzare una classe non istanziabile, basta che mettiamo la keyword private prima della parola constructor, che in questo caso è obbligatoria per evitare ambiguità con classi private. Analizzeremo questa keyword in un secondo momento.

class DontCreateMe private constructor () {

}

Metodi

Come già visto il Kotlin prevede le funzioni top level (o out of the class): ossia quelle esterne alle classi. I metodi sono concettualmente uguali alle funzioni top-level, ma, a differenza di esse, sono applicate ad una istanza di una classe e non eseguite stand-alone. Quindi l'operato della suddetta funzione dipende sia dai parametri passati sia dall'istanza.

Kotlin supporta tramite un formalismo particolare metodi o attributi statici (legati alla classe e non all'istanza) in quanto, come già detto permette di avere le cosiddette funzioni top level e sarebbe solo una, inutile, ripetizione. I metodi descrivono le operazioni che possono essere usate per manipolare gli oggetti, l’implementazione e la firma dei metodi è descritta nella classe. Il metodo agisce esclusivamente sui campi dell’oggetto che lo invoca tramite la dot notazion (l'operatore .):

oggetto.metodo(parametri)

Per eseguire un metodo abbiamo bisogno dell'istanza di un oggetto, se l'oggetto viene usato in più posti è utile asegnarlo ad una variabile, altrimenti possiamo richiamre il costruttore seguito dal metodo:

var obj = Sample() // create instance of class Sampl

obj.foo() // call foo()

Sample().foo() // create instance of class Sample and call foo()

Caratteristiche dei metodi

Per la realizzazione di un metodo valgono le stesse carattersitche delle funzioni:

  • Può avere dei parametri o meno:

class Person (val name: String, val email: String, var age: Int){

fun maggiorenne(): Boolean {

return age >= 18

}


fun changeAge(newAge : Int) {

age = newAge

}

}

  • Può ritornare o meno un valore:

class TreNumeri (var a: Int, var b: Int, var c: Int) {

fun print() {

println("a " + a + " b " + b + " c " + c)

}

fun treNumeri() : Int {

var somma: Int = 0

somma = a + b + c

return somma

}

}

  • Possono vedere le variabili locali alla funzione, locali alla classe e quelle globali, ma non quelle di altre funzioni e/o costruttori:

class Classe (var variabile1 : Int, variabile2 : Int){

var variabile3 = 30

fun altra() {

var variabile4 = 40

}

fun stampa() {

var variabile5 = 40

println(variabile0)

println(variabile1)

println(variabile2) // errore

println(variabile3)

println(variabile4) // errore

println(variabile5)

}

}

  • Le funzioni che modificano il valore di uno o più parametri passati alla funzione si dicono con “side effects” o “mutators”:

class Classe {

var global = 20

fun funzione() {

global = 30 // realizza un side effect

}

}

  • Se il corpo produce un risultato, ogni possibile ramo del flusso del codice deve restituire qualcosa o lanciare un’eccezione:

class Classe (var x: Int){

fun funzione() : Int {

if(x == 30) return 40

if(x == 40) return 30

// errore questo ramo non ha un return

}

fun funzione() : Int {

if(x == 30) return 40

if(x == 40) return 30

return 0

}

fun funzione() : Int {

if(x == 30) return 40

if(x == 40) return 30

throw InvalidArgumentException(arrayOf(x.toString(),"diverso da 30 o 40"))

}

}

  • I parametri, se più di uno, devono essere separati tramite la virgola e ogni parametro deve sempre essere tipizzato.

Inizializzazioni di attributi

Possiamo utilizzare la keyword lateinit per evitare di ripetere inizializzazioni, inutili, che realizzano solo un overhead del codice. lateinit non è disponibile per i tipi primitivi né per i tipi nullabili.

lateinit var stringProperty: String // ok

lateinit var intProperty: Int // errore

lateinit var intNullableProperty: Int? // errore

Natualmente tutte le proprietà devono essere sempre inizializzate, anche quelle con la keyword lateinit. Infatti se si accede ad una proprietà lateinit prima che essa venga inizializzata viene generata una eccezione. Il tipo della prorpietà è opzionale solo se si può dedurre dall'inizializzatore:

var allByDefault: Int? // error: explicit initializer required

var initialized = 1 // has type Int

val simple: Int? // has type Int, must be initialized in constructor

val inferredType = 1 // has type Int

Proprietà calcolate (get() e set()) e campi di backup

I metodi get() e set() (chiamati anche getter e setter) sono veramente utili quando sviluppiamo e verranno utilizzato molto spesso nelle progettazioni. Utilizzando questa metodologia rendiamo i nostri dati da memorizzare Persistenti, controlliamo il loro corretto inserimento e "proteggiamo" il loro contenuto (ovvero li incapsuliamo).

In Kotlin tutte le variabili all'interno delle classi sono di default delle proprietà con il getter e setter implicito, in quanto nel 90% dei casi non serve eseguire ulteriori controlli al get() o al set() . Ma c'è sempre quel 10% dei casi che richiedere dei controlli al gettero e/o al setter. Kotlin tramite tramite le propriertà calcolate ci permette di realizzare il get() e il set() per questi casi:

var <propertyName>[: <PropertyType>] [= <property_initializer>]

[<getter>]

[<setter>]

Possiamo usare la keyword field per dichiarare un campo di bakcup visibile solo all'interno della proprietà:

var someProperty: String = "defaultValue"

get() { return field }

set(value) { field = value }

Passiamo ad un esempio di utilizzo, molto semplice:

class Orario(ore : Int, minuti : Int, secondi : Int) {

var ore : Int = ore

get() {

return field

}

set(value) {

if(value in 0 until 24)

field = value

}

var minuti : Int = minuti

get() {

return field

}

set(value) {

if(value in 0 until 60)

field = value

}

var secondi : Int = secondi

get() {

return field

}

set(value) {

if(value in 0 until 60)

field = value

}

}

Backing property

In questo caso viene effettuata la modifica al field solo e solo se l'ora assegnata è giusta. Ma spesso ci sono casi in cui la visibilità di field è troppo limitativa, come in questo caso dove dovremmo introdurre i campi di backup (backing property) e la keyword private, per limitare l'accesso alla variabile:

private var _table: Map<String, Int>? = null

var table: Map<String, Int>

get() {

if (_table == null) {

_table = HashMap() // Type parameters are inferred

}

return _table ?: throw AssertionError("Set to null by another thread")

}

set(value) {

table = value

}

Proprietà calcolate in sola lettura

Inoltre è possibile realizzare proprietà in sola lettura, tramite la parola chiave val:

val isEmpty: Boolean

get() { return this.size == 0 }

Proprietà o funzioni

Ma ci sono casi in cui è possibile usare le proprietà che le funzioni. Scegliamo le proprietà se il suddetto codice:

  • Non genera eccezioni, se non alcuni contesti (valori nel set non validi, valori null nel get...)
  • Ha una complessità O(1)
  • È facile da calcolare (o, altrimenti, genera una cache al primo utilizzo)
  • Restituisce lo stesso risultato su più invocazioni

class Data {

private var _giorno = 1

var giorno: Int

get() {

return _giorno

}

set(value) {

if(dataValide(value, mese, anno))

_giorno = value

}

private var _mese = 1

var mese: Int

get() {

return _mese

}

set(value) {

if(dataValide(giorno, value, anno))

_mese = value

}

private var _anno = 1900

var anno: Int

get() {

return _anno

}

set(value) {

if(dataValide(giorno, mese, value))

_anno = value

}

constructor()

constructor(giorno: Int, mese: Int, anno: Int) {

if(dataValide(giorno, mese, anno)) {

_giorno = giorno

_mese = mese

_anno = anno

}

}

constructor(giorni : Long) {

this.giorni =giorni

}

var giorni : Long

get() {

var giorni: Long = 0

val annopart = 1900

val mesi = arrayOf(31, if(annoBisestile(_anno)) 29 else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 )

for(i in annopart until _anno)

giorni += if(annoBisestile(i)) 366 else 365

for(i in 0..(_mese - 2))

giorni += mesi[i]

giorni += _giorno - 1

return giorni

}

set(value) {

val annopart = 1900

var anno = annopart

var mese = 0

var giorni = value

while (giorni >= if (annoBisestile(anno)) 366 else 365) {

giorni -= if (annoBisestile(anno)) 366 else 365

anno++

}

val mesi = arrayOf(31, if(annoBisestile(anno)) 29 else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 )

while (giorni >= mesi[mese])

{

giorni -= mesi[mese]

mese++

}

_giorno = giorni.toInt() + 1

_mese = mese + 1

_anno = anno

}


fun getGiorni() : Long { // errore non può coesistere con la proprietà con lo stesso nome

var giorni: Long = 0

val annopart = 1900

val mesi = arrayOf(31, if(annoBisestile(_anno)) 29 else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 )

for(i in annopart until _anno)

giorni += if(annoBisestile(i)) 366 else 365

for(i in 0..(_mese - 2))

giorni += mesi[i]

giorni += _giorno - 1

return giorni

}

fun setGiorni(value : Long) { // errore non può coesistere con la proprietà con lo stesso nome

val annopart = 1900

var anno = annopart

var mese = 0

var giorni = value

while (giorni >= if (annoBisestile(anno)) 366 else 365) {

giorni -= if (annoBisestile(anno)) 366 else 365

anno++

}

val mesi = arrayOf(31, if(annoBisestile(anno)) 29 else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 )

while (value >= mesi[mese]) {

giorni -= mesi[mese]

mese++

}

_giorno = giorni.toInt() + 1

_mese = mese + 1

_anno = anno

}

}

In questo caso decidere se usare la funzione o la proprietà è ardua. Entrambi i casi sono giusti e corretti, però non possono coesistere insieme. In quanto, per limitazioni della JVM e per retrocompatibilità le proprietà, in fase di compilazione vengono trasformate in funzioni che sono: getNomeVariabile() e setNomeVariabile().

results matching ""

    No results matching ""