Modularer Code durch Verkettung von Mixins

Scala Bytes: Stapelweise Traits

Arno Haase

Traits können – anders als Interfaces in Java – auch Methoden implementieren. Dadurch kann z. B. der Ordered Trait auf Basis der Implementierung von compare eine Reihe weiterer Vergleichsoperationen bereitstellen.

Dass Traits auch Methoden implementieren können, ist extrem nützlich, besonders weil eine Klasse von mehreren Traits erben kann. Traits können aber noch erheblich mehr.

Beispiel: ein IntSet

Nehmen wir beispielsweise an, dass wir für die Verarbeitung von Flags ein IntSet implementieren. Dazu definieren wir zunächst eine abstrakte Klasse mit den drei Methoden, die wir benötigen:

abstract class IntSet {
  def add(i: Int): Boolean
  def remove(i: Int): Boolean
  def contains(i: Int): Boolean
}

Außerdem brauchen wir natürlich eine Implementierung (Listing 1).

Listing 1
class SimpleIntSet extends IntSet {
  private val inner = scala.collection.mutable.Set[Int]()
  
  override def add(i: Int) = inner.add(i)
  override def remove(i: Int) = inner.remove(i)
  override def contains(i: Int) = inner.contains(i)
  
  override def toString = inner.toString
}

So weit, so gut – wir können jetzt Int-Werte in einem Set verwalten. Viele typische Anwendungsfälle sind damit abgedeckt. Nehmen wir aber an, dass wir uns manchmal nur für eine bestimmte Anzahl Bits interessieren und alle höherwertigen Bits einfach abschneiden wollen. Das entspricht einer Modulo-Operation auf den Int-Werten. Wir könnten die entsprechende Funktionalität natürlich in unser SimpleIntSet einbauen. Das ist aber keine schöne Lösung. Erstens vermengt sie zwei an sich getrennte Aspekte im Code, der dadurch schlechter verständlich und testbar wird. Und zweitens müssten wir das identische Verhalten nachprogrammieren, wenn wir eine zweite IntSet-Implementierung erstellen – z. B. auf Basis eines BitSet.

Stackable Traits

Da wäre es doch schöner, einen Modulo Trait zu erstellen, den man bei Bedarf in ein IntSet hinein mixen kann. Das geht tatsächlich, bringt aber ein paar Besonderheiten mit sich (Listing 2).

Listing 2
trait Modulo extends IntSet {
  def modBase: Int = 8
  
  abstract override def add(i: Int) = super.add(i % modBase) 
  abstract override def remove(i: Int) = super.remove(i % modBase) 
  abstract override def contains(i: Int) = super.contains(i % modBase)
}

Diesen Trait können wir zu einem beliebigen IntSet einfach als Mixin hinzufügen:

val s = new SimpleIntSet with Modulo

Doch zunächst noch einmal zum Trait selbst. Er erbt vom IntSet. Das bedeutet, dass man ihn nur zu IntSets hinzu mixen kann. Außerdem sind die Methoden mit den Schlüsselworten abstract override gekennzeichnet, und der Code enthält die Aufrufe super.add(), super.remove()und super.contains(). Beides gehört zusammen und kommt nur im Kontext von so genannten Stackable Traits vor. Um sie zu verstehen, müssen wir uns zunächst vor Augen führen, dass Scala den Code von Traits als Wrapper um ein inneres Objekt legt. Wenn ein Trait super.add()aufruft, bezieht sich das also nicht auf eine Basisklasse, sondern auf das nächste innere Objekt, im konkreten Fall also auf die Instanz von SimpleIntSet. Damit ein Trait einen solchen super-Aufruf machen darf, muss er selbst eine abstract override-Implementierung der Methode enthalten. Dadurch überprüft der Compiler, dass die Methode im Basistyp des Traits enthalten ist – hier also im IntSet. Zusätzlich stellt er bei der Verwendung von Modulo sicher, dass der „innere“ Typ eine konkrete Implementierung der Methode hat. SimpleIntSet muss also selbst die add-Methode implementieren, es würde nicht reichen, wenn wir sie erst bei der Instanziierung bereitstellen würden.

Geschrieben von
Arno Haase
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.