Le Interfacce
Molto spesso le interfacce vengono confuse con le classi astratte dato che dal punto di vista logico sono molto simili, ma le interfacce possono essere intese come un’evoluzione delle classi astratte e permettono, di fatto, di simulare l’ereditarietà multipla.
Il vantaggio principale di una interfaccia, essendo comunque una classe anche se con proprietà particolari, è quello che oltre ad essere estesa può essere implementata. La differenza tra estendere e implementare è molto grande in quanto una classe può essere ereditata solo ed esclusivamente una volta, mentre una classe può implementare infinite interfacce permettendo così l’ereditarietà multipla.
Per quanto riguarda le interfacce, abbiamo alcune limitazioni rispetto ad una classe astratta, in quanto in una interfaccia possono essere dichiarati esclusivamente metodi astratti e non possono essere dichiarate variabili, ma solo costanti.
Queste limitazioni non devono essere viste come una sorta di mancanza del JVM, in quanto risultano essere, in realtà, un altro punto di forza del linguaggio. Dato che con le interfacce si può simulare l’eredità multipla e potendo le interfacce solo dichiarare costanti e metodi astratti, avremo che l’ereditarietà multipla risultante sarà un’ereditarietà multipla controllata. Questo è un grosso vantaggio in quanto l’ereditarietà multipla selvaggia, ovvero il fatto di ereditarie da più classi, variabili e metodi già implementati, spesso è uno dei fattori principali che minano la robustezza dell’intera applicazione.
Vediamo adesso, dal punto di vista del codice, come si dichiara un’interfaccia e come la si implementa. La dichiarazione avviene usando la parola chiave interfacce in questo modo:
interface MyInterface {
fun bar()
fun foo() {
// optional body
}
}
Una funzione all'interno dell'interfaccia può avere dei metodi già implementati. L’implementazione dell’interfaccia è invece possibile utilizzando l'operatore ::
class Child : MyInterface {
override fun bar() {
// body
}
}
Per ottenere l’ereditarietà multipla basta accodare il nome delle interfacce che si vuole implementare dopo l'operatore : separate da virgola, nella maniera più simile possibile a quella delle classi normali.
Le interfacce conferiscono ad un’applicazione uno dei concetti principali su cui si basa la programmazione ad oggetti: l’astrazione. Infatti, come abbiamo detto, l’astrazione consente di definire un concetto in maniera molto sintetica e focalizzando l’attenzione solo agli aspetti fondamentali che modellano un determinato concetto. Dunque cosa c’è di più astratto di un interfaccia nella quale non può essere implementato nessun metodo? Praticamente niente, dunque l’interfaccia risulta essere il massimo livello di astrazione possibile per i dati. Proprio per questo Kotlin offre una sintassi migliorata: interface Vuota {} diventa: interface Vuota.
Interfacce e proprietà
Kotlin, oltre ai metodi astratti offre anche le proprietà astratte:
interface MyInterface {
val prop: Int // abstract
val propertyWithImplementation: String
get() = "foo"
fun foo() {
print(prop)
}
}
class Child : MyInterface {
override val prop: Int = 29
}
Una proprietà dichiarata in un'interfaccia può essere astratta o può fornire implementazioni per gli accessori. Attenzione che le proprietà non possono usare dei campi di backup per il salvataggio del dato.
Ereditarietà multipla
Kotlin tramite le interfacce e le classi permette anche l'ereditarietà multipla:
open class Veicolo {
//contenuto
}
interface Auto {
//contenuto
}
interface Euro6 // classe vuota
class Autoveicolo : Veicolo(), Auto, Euro6 {
//contenuto
}
Una piccola nota: spesso, in Kotlin, si distingue l'implementazione di una interfaccia da una ereditarietà di una classe dalla assenza e/o presenza delle (), ma come dimostra lo Snippet 7.8 la regola non è sempre rispettata.
Risolvere i conflitti di ereditarietà
Con l'ereditarietà multipla nasce il problema dei conflitti: Infatti quando si dichiarano molti metodi nelle interfacce, può capitare che una classe che implementa più interfacce più abbia un conflitto: due metodi con lo stesso nome. E spesso questi offrono anche delle implementazioni di default, Kotlin proprio per questo offre la possibilità di risalire ai singoli metodi tramite la keyword super.
interface A {
fun foo() { print("A") }
fun bar()
}
interface B {
fun foo() { print("B") }
fun bar() { print("bar") }
}
class C : A {
override fun bar() { print("bar") }
}
class D : A, B {
override fun foo() {
super<A>.foo()
super<B>.foo()
}
override fun bar() {
super<B>.bar()
}
}
Quando usare le classi astratte e quando le Interfacce
Come abbiamo visto, sia le classi astratte che le interfacce rappresentano un modello, un contratto che la classe derivata deve rispettare implementando i metodi e le proprietà definite nel tipo base, ma allora al di là delle piccole differenze di sintassi qual è la sostanziale differenza tra classi astratte pure ed interfacce, come capire quando usare l’una e quando le altre? La risposta migliore è a seconda del tipo di contratto che la classe derivata deve implementare col tipo base, le classi astratte pure definiscono un legame più forte con la classe derivata poiché ne rappresentano il tipo base definendone il comportamento comune. Mentre le interfacce possono essere usate per definire un modello generico, che implementa un comportamento comune a classi di vario genere e natura, ad esempio il metodo calc() dell’interfaccia Calc potrebbe essere comune sia ad un’istanza di una classe calcolatrice che a quello di un’istanza punto geometrico.
Se mi permettete una definizione personale, direi che le classi astratte pure definiscono un contratto di tipo verticale (dedicato) con le istanze delle classi figlie, mentre le interfacce rappresentano di più un contratto di tipo orizzontale (generico) con gli oggetti che le implementeranno.
https://kotlinlang.org/docs/reference/
https://www.mattepuffo.com/blog/categoria/21-kotlin/0.html
https://stackoverflow.com/documentation/kotlin/topics
https://medium.com/@DarrenAtherton/intro-to-data-classes-in-kotlin-7f956d54365c
https://proandroiddev.com/creating-multiple-constructors-for-data-classes-in-kotlin-32ad27e58cac