Introduzione
In Kotlin, le classi astratte e le interfacce sono due strumenti fondamentali per la programmazione orientata agli oggetti che permettono di definire comportamenti comuni e contratti che le classi devono implementare.
Classi Astratte
Cosa sono
Una classe astratta è una classe che non può essere istanziata direttamente, ma deve essere estesa da altre classi. Viene dichiarata con la parola chiave abstract.
A cosa servono
- Fornire una base comune per classi correlate
- Definire comportamenti parzialmente implementati
- Obbligare le classi derivate a implementare certi metodi
abstract class Animale(val nome: String) {
// Proprietà astratta (deve essere implementata dalle sottoclassi)
abstract val verso: String
// Metodo astratto (deve essere implementato dalle sottoclassi)
abstract fun muovi()
// Metodo concreto (già implementato)
fun presenta() {
println("Sono un $nome e faccio $verso")
}
}
class Cane(nome: String) : Animale(nome) {
override val verso = "Bau bau!"
override fun muovi() {
println("$nome sta correndo su quattro zampe")
}
}
fun main() {
val cane = Cane("Fido")
cane.presenta() // Sono un Fido e faccio Bau bau!
cane.muovi() // Fido sta correndo su quattro zampe
}
Interfacce
Cosa sono
Un’interfaccia è un contratto che definisce un insieme di metodi e proprietà che una classe deve implementare. A differenza delle classi astratte, un’interfaccia non può contenere stato (cioè non può avere proprietà con backing field). Normalmente una classe può estendere un’unica superclasse, nel caso delle interfacce, una classe può estendere infinite interfacce
Nel caso delle interfacce quando si estende un interfaccia si è obbligati ad estendere tutti i metodi presenti. Nella dichiarazione di un interfaccia non è presente il costruttore poiché non è una classe ma un entità diversa, possono non avere body.
In generale vengono molto utilizzate in Android, vengono utilizzate come listener:
L’Interfaccia Listener: Definisce un’interfaccia Kotlin (ad esempio, ClickListener, SensorEventListener, LocationUpdateListener). Questa interfaccia conterrà uno o più metodi astratti. Ognuno di questi metodi corrisponde a un tipo di evento che il listener può gestire.
interface MyEventListener {
fun onEventOccurred(data: String)
fun onError(errorCode: Int, message: String)
}
/*La Classe che Genera l'Evento (Sorgente dell'Evento): Questa è la classe che è in grado di rilevare l'evento. Quando l'evento si verifica, questa classe ha il compito di notificare tutti i listener che si sono "registrati" per quell'evento. Per fare ciò, la classe avrà un modo per aggiungere (e possibilmente rimuovere) istanze della tua interfaccia listener.*/
class EventSource {
private val listeners = mutableListOf<MyEventListener>()
fun addListener(listener: MyEventListener) {
listeners.add(listener)
}
fun removeListener(listener: MyEventListener) {
listeners.remove(listener)
}
fun doSomethingThatTriggersEvent() {
// ... logica che potrebbe causare un evento ...
val someData = "Dati dell'evento!"
// Notifica tutti i listener registrati
listeners.forEach { it.onEventOccurred(someData) }
}
fun simulateError() {
val code = 500
val msg = "Errore generico!"
listeners.forEach { it.onError(code, msg) }
}
}
/*La Classe che Vuole Ricevere la Notifica (Gestore dell'Evento): Questa è la parte della tua applicazione che è interessata a sapere quando l'evento si verifica. Questa classe implementerà l'interfaccia listener e fornirà l'implementazione per i metodi definiti in essa. Poi, registrerà se stessa (o un'istanza di sé) con la sorgente dell'evento.*/
class EventManager : MyEventListener {
override fun onEventOccurred(data: String) {
println("Evento ricevuto! Dati: $data")
// Qui puoi implementare la logica che vuoi eseguire quando l'evento si verifica
}
override fun onError(errorCode: Int, message: String) {
println("Errore ricevuto! Codice: $errorCode, Messaggio: $message")
}
}
fun main() {
val source = EventSource()
val manager = EventManager()
// Registra il manager come listener
source.addListener(manager)
// Simula il verificarsi di un evento
source.doSomethingThatTriggersEvent()
source.simulateError()
// È buona pratica rimuovere il listener quando non è più necessario
source.removeListener(manager)
}
/* A cosa servono i Listener?
I listener sono un pattern di progettazione fondamentale per:
Disaccoppiamento: Permettono alla classe che genera l'evento di non dover conoscere i dettagli delle classi che gestiranno l'evento. La sorgente dell'evento sa solo che deve chiamare un metodo su un'interfaccia listener, ma non le importa chi implementa quell'interfaccia.
Reattività: Rendono le applicazioni reattive agli eventi esterni (input dell'utente, dati in arrivo dalla rete, cambiamenti di stato, ecc.).
Modularità: Aiutano a creare codice più modulare, in cui diverse parti dell'applicazione possono reagire agli stessi eventi in modi diversi, senza influenzarsi a vicenda.*/
Di seguito un piccolo schema per comprendere quando creare una super classe, una classe una classe astratta o un interfaccia:
- Crea una classe senza una superclasse quando la tua nuova classe non supera il test “IS-A” (È-UN) per nessun altro tipo.
- Crea una sottoclasse che eredita da una superclasse quando hai bisogno di una versione più specifica di una classe e devi sovrascrivere o aggiungere nuovi comportamenti.
- Crea una classe astratta quando vuoi definire un modello per un gruppo di sottoclassi. Rendi la classe astratta quando vuoi garantire che nessuno possa creare oggetti di quel tipo.
- Crea un’interfaccia quando vuoi definire un comportamento comune, o un ruolo che altre classi possono assumere, indipendentemente dalla loro posizione nell’albero di ereditarietà.