Voi siete qui: Inizio Programmare in Scala

Tratti

Una introduzione ai tratti

Prima di immergerci nella programmazione orientata agli oggetti, c’è un’ulteriore caratteristica essenziale di Scala con cui dovreste familiarizzare: i tratti. Per capire il valore di questa funzionalità è necessario descriverne brevemente il contesto di origine.

In Java una classe può implementare un numero arbitrario di interfacce. Questo modello è molto utile per dichiarare che una classe espone molteplici astrazioni, ma sfortunatamente ha un grosso svantaggio.

Per molte interfacce, buona parte delle funzioni può essere implementata con codice “stereotipato” che sarà valido per tutte le classi che usano l’interfaccia. Java non fornisce alcun meccanismo primitivo per definire e usare questo codice riusabile. Invece, i programmatori Java devono usare convenzioni ad hoc per riutilizzare il codice di implementazione per una data interfaccia. Nel caso peggiore, lo sviluppatore copia e incolla lo stesso codice in ogni classe che ne ha bisogno.

Spesso, l’implementazione di un’interfaccia possiede alcuni membri che non sono correlati (cioè, che sono “ortogonali”) ai rimanenti membri dell’istanza. Il termine mixin viene spesso usato per indicare queste parti specializzate e potenzialmente riusabili di un’istanza che potrebbero essere mantenute indipendentemente.

Date un’occhiata al codice seguente, che implementa un pulsante in un’interfaccia grafica e usa una serie di callback per gestire i “clic”.

// esempi/cap-4/ui/button-callbacks.scala

package ui

class ButtonWithCallbacks(val label: String,
    val clickedCallbacks: List[() => Unit]) extends Widget {

  require(clickedCallbacks != null, "La lista di callback non può essere nulla!")

  def this(label: String, clickedCallback: () => Unit) =
    this(label, List(clickedCallback))

  def this(label: String) = {
    this(label, Nil)
    println("Attenzione: il pulsante non ha callback per i clic!")
  }

  def click() = {
    // ... logica per mostrare visivamente il clic sul pulsante ...
    clickedCallbacks.foreach(f => f())
  }
}

Qui stanno succedendo molte cose. Il costruttore principale prende un argomento label e una lista di callback che vengono invocate quando il metodo click del pulsante viene invocato. Esploreremo i dettagli di questa classe nel capitolo 5. Per ora vogliamo concentrarci su un problema particolare. ButtonWithCallbacks non si limita a gestire il comportamento essenziale di un pulsante (come i clic) ma gestisce anche gli eventi di notifica per i clic invocando le funzioni di callback. Questa è una violazione del principio di singola responsabilità [Martin2003], un mezzo per raggiungere il fine della separazione degli interessi. Vorremmo separare la logica specifica per il pulsante dalla logica di callback, in modo che ogni componente logico diventi più semplice, più modulare e più riusabile. La logica di callback è un buon esempio di mixin.

Questa separazione è difficile da ottenere in Java; anche se definiamo un’interfaccia per il comportamento di callback, dobbiamo ancora incorporare in qualche modo il codice di implementazione nella classe, compromettendo così la modularità. L’unica alternativa è quella di usare uno strumento specializzato come la programmazione orientata agli aspetti (AOP, si veda [AOSD]), implementata per esempio da AspectJ [AspectJ], un’estensione di Java. La AOP è principalmente progettata per separare le implementazioni di interessi “pervasivi” che sono replicate all’interno di un’applicazione. Tenta di rendere modulari questi interessi, pur abilitando, durante la compilazione o l’esecuzione, il “mescolamento” a grana fine dei loro comportamenti con altri interessi, inclusa la logica di dominio dell’applicazione.

I tratti come mixin

Scala fornisce una soluzione mixin completa chiamata tratto. Nel nostro esempio, possiamo definire l’astrazione di callback in un tratto, come in un’interfaccia Java, ma possiamo anche implementare l’astrazione nel tratto (o in un tratto derivato). Possiamo dichiarare classi che “mescolano” il tratto, in maniera molto simile a come si dichiarano classi che implementano un’interfaccia in Java. Tuttavia, in Scala possiamo anche mescolare i tratti nel momento in cui creiamo istanze, senza prima dichiarare una classe che mescola tutti i tratti desiderati. Quindi, i tratti in Scala preservano la separazione degli interessi dandoci allo stesso tempo la capacità di comporre comportamenti su richiesta.

Se provenite da una formazione in Java, potete pensare ai tratti come a interfacce con implementazioni opzionali. Altri linguaggi forniscono costrutti che sono simili ai tratti, come i moduli in Ruby, per esempio.

Proviamo a usare un tratto per separare la gestione delle callback dalla logica del pulsante. Generalizzeremo un poco il nostro approccio. In realtà le callback sono un caso speciale del pattern observer [GOF1995]. Quindi, creeremo un tratto che implementi questo pattern, poi lo useremo per gestire il comportamento delle callback. Per semplificare le cose, cominceremo con una singola callback che conta il numero dei clic sul pulsante.

Per prima cosa, definiamo una semplice classe Button.

// esempi/cap-4/ui/button.scala

package ui

class Button(val label: String) extends Widget {
  def click() = {
    // Logica per mostrare visivamente il clic sul pulsante...
  }
}

Ed ecco qui la classe genitore Widget.

// esempi/cap-4/ui/widget.scala

package ui

abstract class Widget

La logica per gestire le callback (cioè la lista clickedCallbacks) è stata rimossa, così come i due costruttori ausiliari. Nel pulsante rimangono solo il campo label e il metodo click. Il metodo click ora si preoccupa solo di come appare visivamente un pulsante “fisico” quando viene cliccato. La classe Button ha un solo interesse, gestire “l’essenza” dell’essere un pulsante.

Ecco un tratto che implementa la logica del pattern observer

// esempi/cap-4/observer/observer.scala

package observer

trait Subject {
  type Observer = { def receiveUpdate(subject: Any) }

  private var observers = List[Observer]()
  def addObserver(observer:Observer) = observers ::= observer
  def notifyObservers = observers foreach (_.receiveUpdate(this))
}

Tranne che per la parola chiave trait, Subject sembra una classe ordinaria. Subject definisce tutti i membri che dichiara. I tratti possono dichiarare membri astratti, concreti, o di entrambi i tipi proprio come le classi (si veda la sezione Ridefinire i membri di classi e tratti nel capitolo 6 per maggiori dettagli). Come le classi, i tratti possono contenere definizioni annidate di tratti e classi; le classi possono contenere definizioni annidate di tratti.

La prima riga definisce un tipo per Observer. Questo è un tipo strutturale della forma { def receiveUpdate(subject: Any) }. I tipi strutturali specificano solo la struttura che un sottotipo deve supportare; potete pensarli come tipi “anonimi”.

In questo caso, il tipo strutturale è definito da un metodo con una firma particolare. Qualsiasi tipo che sia dotato di un metodo con questa firma può essere usato come osservatore. Impareremo di più sui tipi strutturali nel capitolo 12. Se vi state chiedendo perché non abbiamo usato Subject come tipo dell’argomento invece di Any, rivisiteremo questa scelta nella sezione Annotazioni self-type e membri tipo astratti del capitolo 13.

Per ora, la cosa principale da notare è come questo tipo strutturale minimizzi le dipendenze tra il tratto Subject e qualsiasi potenziale utente del tratto.

Subject mantiene ancora una dipendenza dal nome del metodo in Observer attraverso il tipo strutturale, cioè dal nome del metodo receiveUpdate. Ci sono molti modi in cui possiamo ridurre questa dipendenza residua. Vedremo come fare nella sezione Ridefinire i tipi astratti del capitolo 6.

Successivamente, dichiariamo una lista di osservatori usando var anziché val perché List è immutabile, quindi dobbiamo creare una nuova lista quando un osservatore viene aggiunto tramite il metodo addObserver.

Parleremo ulteriormente della classe List di Scala nella sezione La gerarchia di tipi di Scala del capitolo 7 e anche nel capitolo 8. Per ora, notate che addObserver usa il metodo “operatore” :: della lista (detto “cons”) per aggiungere un osservatore in testa alla lista degli osservatori. Il compilatore Scala è abbastanza intelligente da trasformare la seguente istruzione:

observers ::= observer

in questa istruzione:

observers = observer :: observers

Notate che abbiamo scritto observer :: observers collocando la lista observers esistente sul lato destro. Ricordatevi che qualsiasi metodo il cui nome finisce con : lega l’argomento alla propria destra. Quindi, l’istruzione precedente è equivalente a questa istruzione:

observers = observers.::(observer)

Il metodo notifyObservers itera attraverso la lista di osservatori usando il metodo foreach e invoca receiveUpdate su ognuno. (Notate che stiamo usando la notazione “infissa” per gli operatori invece di observers.foreach.) Usiamo il segnaposto _ per abbreviare l’espressione seguente:

(obs) => obs.receiveUpdate(this)

in questa espressione:

_.receiveUpdate(this)

Questa espressione in realtà è il corpo di una “funzione anonima”, chiamata letterale funzione in Scala. È simile a una lambda e a costrutti simili usati in molti altri linguaggi. I letterali funzione e il concetto associato di chiusura sono discussi nella sezione Letterali funzione e chiusure del capitolo 8.

In Java, il metodo foreach avrebbe probabilmente accettato un’interfaccia e gli avreste passato un’istanza della classe che implementa l’interfaccia (questo è il tipico modo in cui viene usata Comparable, per esempio).

In Scala, il metodo List[A].foreach si aspetta un argomento di tipo (A) => Unit, cioè una funzione che accetta un’istanza di tipo A, dove A rappresenta il tipo degli elementi nella lista (Observer in questo caso), e che restituisce Unit (simile a void in Java).

In questo esempio, abbiamo scelto di usare var con istanze immutabili di List per memorizzare gli osservatori. Avremmo potuto usare val con un tipo mutabile come ListBuffer. Questa scelta sarebbe stata più sensata per un’applicazione reale, ma volevamo evitare di distrarvi spiegando nuove classi di libreria.

Ancora una volta, da un piccolo esempio abbiamo imparato molte caratteristiche di Scala. Ora mettiamo all’opera il nostro tratto Subject. Ecco ObservableButton, che estende Button e mescola Subject.

// esempi/cap-4/ui/observable-button.scala

package ui
import observer._

class ObservableButton(name: String) extends Button(name) with Subject {
  override def click() = {
    super.click()
    notifyObservers
  }
}

Cominciamo importando tutto il contenuto del package observer usando _ come wildcard. In realtà il package contiene solo la definizione del tratto Subject.

La nuova classe usa la parola chiave with per aggiungere il tratto Subject alla classe. ObservableButton ridefinisce il metodo click. Usando la parola chiave super (si veda la sezione Ridefinire i metodi astratti e concreti nel capitolo 6), questo metodo prima invoca il metodo Button.click della “superclasse”, poi informa gli osservatori. Dato che il nuovo metodo click ridefinisce l’implementazione concreta di Button, la parola chiave override è obbligatoria.

La parola chiave with è analoga alla parola chiave implements di Java per le interfacce. Potete specificare tutti i tratti che volete, ognuno con la propria parola chiave with.

Una classe può estendere un tratto e un tratto può estendere una classe. In effetti, la nostra classe Widget definita in precedenza avrebbe potuto essere dichiarata come un tratto.

Se dichiarate una classe che usa uno o più tratti e che non estende un’altra classe, dovete usare la parola chiave extends per il primo tratto elencato.

Se non usate extends per il primo tratto, cioè se scrivete in questo modo:

// ERRORE:
class ObservableButton(name: String) with Button(name) with Subject {…}

otterrete il seguente errore:

… error: ';' expected but 'with' found.
     class ObservableButton(name: String) with Button(name) with Subject {…}
                                          ^

Il messaggio di errore in realtà dovrebbe dire “extends expected but with found”.1

Per vedere questo codice in azione, cominciamo creando una classe che osserva i clic sul pulsante per contarne semplicemente il numero.

// esempi/cap-4/ui/button-count-observer.scala

package ui
import observer._

class ButtonCountObserver {
  var count = 0
  def receiveUpdate(subject: Any) = count += 1
}

Infine, scriviamo un test che utilizzi tutte queste classi. Useremo la libreria Specs (discussa nella sezione Specs del capitolo 14) per scrivere una “specifica” di sviluppo guidato dal comportamento ([BDD]) che adoperi i tipi Button e Subject combinati tra loro.

// esempi/cap-4/ui/button-observer-spec.scala

package ui
import org.specs._
import observer._

object ButtonObserverSpec extends Specification {
  "Un osservatore di un pulsante" should {
    "osservare i clic sul pulsante" in {
      val observableButton = new ObservableButton("Okay")
      val buttonObserver = new ButtonCountObserver
      observableButton.addObserver(buttonObserver)

      for (i <- 1 to 3) observableButton.click()
      buttonObserver.count mustEqual 3
    }
  }
}

Se avete scaricato gli esempi di codice dal sito di O’Reilly, allora potete seguire le direttive nei file README per assemblare ed eseguire gli esempi di questo capitolo. Usando specs come “obiettivo” dell’assemblaggio otterrete un’uscita che dovrebbe includere il testo seguente.

Specification "ButtonCountObserverSpec"
  Un osservatore di un pulsante should
  + osservare i clic sul pulsante

Total for specification "ButtonCountObserverSpec":
Finished in 0 second, 10 ms
1 example, 1 expectation, 0 failure, 0 error

Notate che le stringhe "Un osservatore di un pulsante" e "osservare i clic sul pulsante" corrispondono alle stringhe nell’esempio. L’uscita di un’esecuzione di Specs offre un apprezzabile riepilogo dei requisiti per gli elementi che vengono collaudati, supponendo che le stringhe siano state scelte con cura.

Il corpo del test crea un ObservableButton con il nome "Okay" e un ButtonCountObserver che rappresenta l’osservatore per il pulsante. Il pulsante viene cliccato tre volte usando il ciclo for. L’ultima riga richiede che il campo count dell’osservatore sia uguale a 3. Se avete familiarità nell’usare uno strumento di collaudo come JUnit [JUnit] o ScalaTest [ScalaTestTool] (si veda anche la sezione ScalaTest nel capitolo 14), allora l’ultima riga è equivalente alla seguente asserzione in JUnit.

assertEquals(3, buttonObserver.count)

La libreria Specs (si veda la sezione Specs) e la libreria ScalaTest (si veda la sezione ScalaTest) supportano entrambe lo sviluppo guidato dal comportamento [BDD], uno stile di sviluppo guidato dai test (in inglese, test-driven development) [TDD] che enfatizza il ruolo di “specifica” giocato dai test.

Pensate di aver bisogno di una sola istanza di ObservableButton? In realtà non c’è bisogno di dichiarare una classe che estenda Button con Subject, ma è possibile incorporare il tratto quando si crea l’istanza.

L’esempio successivo mostra un file Specs revisionato che crea un’istanza della classe Button mescolando Subject come parte della dichiarazione.

// esempi/cap-4/ui/button-observer-anon-spec.scala

package ui
import org.specs._
import observer._

object ButtonObserverAnonSpec extends Specification {
  "Un osservatore di un pulsante" should {
    "osservare i clic sul pulsante" in {
      val observableButton = new Button("Okay") with Subject {
        override def click() = {
          super.click()
          notifyObservers
        }
      }

      val buttonObserver = new ButtonCountObserver
      observableButton.addObserver(buttonObserver)

      for (i <- 1 to 3) observableButton.click()
      buttonObserver.count mustEqual 3
    }
  }
}

La dichiarazione revisionata di observableButton crea effettivamente una classe anonima in cui ridefiniamo il metodo click, come prima. La differenza principale rispetto alla creazione di classi anonime in Java è che possiamo incorporare i tratti in questo processo. Java non vi permette di implementare una nuova interfaccia mentre istanziate una classe.

Infine, notate che un’istanza può avere una gerarchia di ereditarietà complessa se mescola tratti che estendono altri tratti, &c. Discuteremo i dettagli della gerarchia nella sezione Linearizzare la gerarchia di un oggetto del capitolo 7.

Tratti impilabili

Possiamo ritoccare un paio di aspetti del nostro lavoro per migliorarne la riusabilità e per facilitare l’impiego di più di un tratto alla volta, cioè per “impilarli”.

Come prima cosa, introduciamo un nuovo tratto chiamato Clickable per rappresentare qualsiasi elemento grafico che risponde ai clic.

// esempi/cap-4/ui2/clickable.scala

package ui2

trait Clickable {
  def click()
}

Creiamo un nuovo package ui2 per facilitare il compito di mantenere separate le vecchie versioni degli esempi da quelle nuove nel codice scaricabile.

Il tratto Clickable sembra proprio un’interfaccia Java: è completamente astratto. Definisce un singolo metodo astratto di nome click; il metodo è astratto perché non ha un corpo. Se Clickable fosse stata una classe, avremmo dovuto aggiungere la parola chiave abstract davanti alla parola chiave class. Questo non è necessario per i tratti.

Ecco il pulsante rielaborato per usare il tratto.

// esempi/cap-4/ui2/button.scala

package ui2

import ui.Widget

class Button(val label: String) extends Widget with Clickable {
  def click() = {
    // Logica per mostrare visivamente il clic sul pulsante...
  }
}

Questo codice è simile a codice Java che implementa un’interfaccia Clickable.

Quando abbiamo definito ObservableButton in precedenza (nella sezione I tratti come mixin), abbiamo ridefinito Button.click per informare gli osservatori. Abbiamo dovuto duplicare quella logica in ButtonObserverAnonSpec quando abbiamo dichiarato observableButton come un’istanza di Button che ha mescolato direttamente il tratto Subject. Ora possiamo eliminare quella duplicazione.

Quando rielaboriamo il codice in questo modo, ci accorgiamo di non essere realmente interessati a osservare i pulsanti; ci interessa osservare i clic. Ecco un tratto che si concentra esclusivamente sulla osservazione dei Clickable.

// esempi/cap-4/ui2/observable-clicks.scala

package ui2
import observer._

trait ObservableClicks extends Clickable with Subject {
  abstract override def click() = {
    super.click()
    notifyObservers
  }
}

Il tratto ObservableClicks estende Clickable e mescola Subject. Poi ridefinisce il metodo click con un’implementazione che sembra quasi uguale a quella del metodo ridefinito mostrato nella sezione I tratti come mixin. La differenza importante è la parola chiave abstract.

Osservate da vicino questo metodo: invoca super.click(), ma cos’è super in questo caso? A questo punto, potrebbe solamente essere Clickable, che dichiara il metodo click ma non lo definisce, oppure potrebbe essere Subject, che non possiede un metodo click. Quindi, super non può essere legata, almeno non ancora.

In effetti, super verrà legata nel momento in cui questo tratto viene mescolato in un’istanza che definisce un metodo click concreto, come un’istanza di Button. Di conseguenza, abbiamo bisogno di usare la parola chiave abstract su ObservableClicks.click per dire al compilatore (e al lettore) che click non è ancora completamente implementato, anche se ObservableClicks.click ha un corpo.

Tranne che per dichiarare classi astratte, la parola chiave abstract è obbligatoria solo su un metodo di un tratto nel caso in cui il metodo abbia un corpo ma usi super per chiamare un metodo che non ha un’implementazione concreta nei genitori del tratto.

Usiamo questo tratto insieme a Button e al suo metodo click concreto in un test Specs.

// esempi/cap-4/ui2/button-clickable-observer-spec.scala

package ui2
import org.specs._
import observer._
import ui.ButtonCountObserver

object ButtonClickableObserverSpec extends Specification {
  "Un osservatore di un pulsante" should {
    "osservare i clic sul pulsante" in {
      val observableButton = new Button("Okay") with ObservableClicks
      val buttonClickCountObserver = new ButtonCountObserver
      observableButton.addObserver(buttonClickCountObserver)

      for (i <- 1 to 3) observableButton.click()
      buttonClickCountObserver.count mustEqual 3
    }
  }
}

Confrontate questo codice con ButtonObserverAnonSpec. Istanziamo Button mescolando il tratto ObservableClicks, ma ora non c’è alcuna ridefinizione obbligatoria di click. Quindi, questo cliente di Button non deve preoccuparsi di ridefinire opportunamente click; la fatica è già stata fatta da ObservableClicks. Il comportamento desiderato viene composto dichiarativamente quando è necessario.

Concludiamo il nostro esempio aggiungendo un secondo tratto. La specifica JavaBeans [JavaBeansSpec] introduce il concetto di eventi “vietabili” in cui gli osservatori dei cambiamenti in un componente possono porre il veto sul cambiamento. Implementiamo qualcosa di simile con un tratto che vieta un numero di clic maggiore rispetto a una soglia prefissata.

// esempi/cap-4/ui2/vetoable-clicks.scala

package ui2
import observer._

trait VetoableClicks extends Clickable {
  val maxAllowed = 1  // valore predefinito
  private var count = 0

  abstract override def click() = {
    if (count < maxAllowed) {
      count += 1
      super.click()
    }
  }
}

Ancora una volta, ridefiniamo il metodo click. Come prima, il metodo ridefinito deve essere dichiarato abstract. Il massimo numero predefinito di clic permessi è 1. Potreste chiedervi cosa intendiamo qui con “predefinito”: il campo non è stato dichiarato come val? E non c’è alcun costruttore definito per inizializzarlo a un altro valore. Rivisiteremo queste domande nella sezione Ridefinire i membri di classi e tratti del capitolo 6.

Questo tratto dichiara anche una variabile count per tenere traccia del numero di clic osservati. Essa viene dichiarata private in modo che sia invisibile al di fuori del tratto (si veda la sezione Regole di visibilità nel capitolo 5). Il metodo click ridefinito incrementa count e invoca il metodo super.click() solo se il conto è minore o uguale al valore maxAllowed.

Ecco un oggetto Specs che mostra come ObservableClicks e VetoableClicks lavorano insieme. Notate che è obbligatorio usare una parola chiave with separata per ogni tratto anziché usare una parola chiave e separare i nomi con una virgola, come fa Java per le clausole implements.

// esempi/cap-4/ui2/button-clickable-observer-vetoable-spec.scala

package ui2
import org.specs._
import observer._
import ui.ButtonCountObserver

object ButtonClickableObserverVetoableSpec extends Specification {
  "Un osservatore di un pulsante con clic vietabili" should {
    "osservare solo il primo clic sul pulsante" in {
      val observableButton =
          new Button("Okay") with ObservableClicks with VetoableClicks
      val buttonClickCountObserver = new ButtonCountObserver
      observableButton.addObserver(buttonClickCountObserver)

      for (i <- 1 to 3) observableButton.click()
      buttonClickCountObserver.count mustEqual 1
    }
  }
}

Il conto atteso per l’osservatore è 1. Il pulsante osservabile observableButton è dichiarato come segue.

new Button("Okay") with ObservableClicks with VetoableClicks

Possiamo dedurre che il metodo click ridefinito in VetoableClicks viene invocato prima del metodo click ridefinito in ObservableClicks. Generalmente parlando, dato che la nostra classe anonima non definisce il proprio metodo click, la ricerca del metodo procede da destra a sinistra nella dichiarazione. In realtà le cose sono più complicate di così, come vedremo più avanti nella sezione Linearizzare la gerarchia di un oggetto del capitolo 7.

Nel frattempo, cosa succede se usiamo i tratti nell’ordine inverso?

// esempi/cap-4/ui2/button-vetoable-clickable-observer-spec.scala

package ui2
import org.specs._
import observer._
import ui.ButtonCountObserver

object ButtonVetoableClickableObserverSpec extends Specification {
  "Un pulsante vietabile con un osservatore di clic" should {
    "osservare tutti i clic sul pulsante, anche se alcuni sono vietati" in {
      val observableButton =
          new Button("Okay") with VetoableClicks with ObservableClicks
      val buttonClickCountObserver = new ButtonCountObserver
      observableButton.addObserver(buttonClickCountObserver)

      for (i <- 1 to 3) observableButton.click()
      buttonClickCountObserver.count mustEqual 3
    }
  }
}

Ora il conto atteso per l’osservatore è 3. Il tratto ObservableClicks ora ha precedenza su VetoableClicks, quindi il conto dei clic viene incrementato anche quando alcuni clic vengono successivamente vietati!

Quindi, l’ordine della dichiarazione è importante, e dovete tenerlo presente per prevenire comportamenti inattesi quando i tratti si influenzano a vicenda. Forse un’altra lezione da ricordare è che suddividere gli oggetti in troppi tratti a grana fine può rendere oscuro l’ordine di esecuzione del vostro codice!

Dividere la vostra applicazione in piccoli tratti specializzati è un modo potente per creare astrazioni e “componenti” scalabili e riusabili. È possibile assemblare comportamenti complessi attraverso la composizione dichiarativa dei tratti. Esploreremo molto dettagliatamente questa idea nella sezione Astrazioni scalabili del capitolo 13.

Costruire i tratti

I tratti non supportano i costruttori ausiliari e non accettano una lista di argomenti per il costruttore principale. I tratti possono estendere classi o altri tratti, ma non possono passare argomenti al costruttore della classe genitore (nemmeno valori letterali), perciò possono solo estendere classi che hanno un costruttore principale o ausiliario senza argomenti.

Tuttavia, come per le classi, il corpo di un tratto viene eseguito ogni volta che viene creata un’istanza che usa il tratto, come mostrato nello script seguente.

// esempi/cap-4/trait-construction-script.scala

trait T1 {
  println("  in T1: x = " + x)
  val x = 1
  println("  in T1: x = " + x)
}
trait T2 {
  println("  in T2: y = " + y)
  val y = "T2"
  println("  in T2: y = " + y)
}

class Base12 {
  println("  in Base12: b = " + b)
  val b = "Base12"
  println("  in Base12: b = " + b)
}
class C12 extends Base12 with T1 with T2 {
  println("  in C12: c = " + c)
  val c = "C12"
  println("  in C12: c = " + c)
}
println("Sto creando C12:")
new C12
println("Dopo aver creato C12")

L’esecuzione di questo script con il comando scala produce l’uscita seguente.

Sto creando C12:
  in Base12: b = null
  in Base12: b = Base12
  in T1: x = 0
  in T1: x = 1
  in T2: y = null
  in T2: y = T2
  in C12: c = null
  in C12: c = C12
Dopo aver creato C12

Notate l’ordine di invocazione dei costruttori della classe e del tratto. Dato che la dichiarazione di C12 è extends Base12 with T1 with T2, l’ordine di costruzione per questa semplice gerarchia di classi va da sinistra a destra, cominciando con la classe base Base12, seguita dai tratti T1 e T2, e terminando con il corpo del costruttore di C12. (Per la costruzione di gerarchie arbitrariamente complesse si veda la sezione Linearizzare la gerarchia di un oggetto nel capitolo 7.)

Quindi, pur non potendo passare parametri di costruzione ai tratti, potete inizializzare i campi con valori predefiniti o lasciarli astratti. In realtà questa è una cosa che abbiamo già visto nel nostro tratto Subject, dove il campo Subject.observers veniva inizializzato con una lista vuota.

Se un campo concreto in un tratto non ha un valore predefinito appropriato, non c’è alcun modo “intrinsecamente sicuro” per inizializzarne il valore. Tutti gli approcci alternativi richiedono che gli utenti del tratto effettuino qualche operazione ad hoc, ma questa pratica è soggetta a errori perché gli utenti potrebbero sbagliarsi o dimenticarsene completamente. Forse il campo dovrebbe essere lasciato astratto, in modo che le classi o gli altri tratti che usano questo tratto siano obbligati a definirne opportunamente il valore. Tratteremo nel dettaglio la ridefinizione di membri astratti e concreti nel capitolo 6.

Un’altra soluzione consiste nello spostare quel campo in una classe separata dove il processo di costruzione possa garantire che i dati di inizializzazione corretti vengano forniti dall’utente. Potrebbe darsi che l’intero tratto in realtà debba essere una classe, e che quindi sia possibile definire un costruttore per inizializzarne i campi.

Classe o tratto?

Quando valutate se un “concetto” debba essere un tratto o una classe, tenete a mente che i tratti usati come mixin hanno più senso per il comportamento “aggiuntivo”. Se scoprite che un tratto particolare viene usato più spesso come genitore di altre classi, in modo che le classi figlie si comportino come il tratto genitore, allora considerate la possibilità di definire il tratto come una classe, per rendere questa relazione logica più chiara. (Abbiamo detto si comporta come piuttosto che è un perché la prima espressione è una definizione più precisa di ereditarietà, basata sul principio di sostituzione di Liskov — si veda [Martin2003], per esempio.)

Nei tratti, evitate di usare campi concreti che non possono essere inizializzati con valori predefiniti appropriati. Invece, usate campi astratti, oppure convertite il tratto in una classe dotata di un costruttore. Naturalmente, i tratti senza stato non hanno alcun problema di inizializzazione.

È un principio generale della buona progettazione orientata agli oggetti che un’istanza debba sempre trovarsi in uno stato valido e noto a partire dal momento in cui il processo di costruzione termina.

Riepilogo, e poi?

In questo capitolo abbiamo imparato come usare i tratti per incapsulare e condividere interessi trasversali tra le classi. Abbiamo parlato di quando e come usare i tratti, di come “impilare” molteplici tratti e delle regole per inizializzare valori all’interno dei tratti.

Nel prossimo capitolo esploreremo il funzionamento dei fondamenti della programmazione orientata agli oggetti in Scala. Anche se siete veterani della programmazione orientata agli oggetti, vorrete leggere i prossimi capitoli per capire i particolari dell’approccio di Scala alla OOP.


  1. [NdT] Letteralmente, il messaggio si potrebbe tradurre con “atteso extends ma trovato with”.

© 2008–9 O’Reilly Media
© 2009–10 Giulio Piancastelli per la traduzione italiana