Mixins vs Komposition in Scala

78

In der Java-Welt (genauer gesagt, wenn Sie keine Mehrfachvererbung / Mixins haben) ist die Faustregel ganz einfach: "Bevorzugen Sie die Objektzusammensetzung gegenüber der Klassenvererbung".

Ich würde gerne wissen, ob / wie es geändert wird, wenn Sie auch Mixins berücksichtigen, insbesondere in Scala?
Werden Mixins als ein Weg der Mehrfachvererbung oder als mehr Klassenzusammensetzung angesehen?
Gibt es auch eine Richtlinie "Objektkomposition gegenüber Klassenkomposition bevorzugen" (oder umgekehrt)?

Ich habe einige Beispiele gesehen, bei denen Leute Mixins verwenden (oder missbrauchen), wenn die Objektzusammensetzung auch die Arbeit erledigen könnte, und ich bin mir nicht immer sicher, welches besser ist. Es scheint mir, dass Sie mit ihnen ziemlich ähnliche Dinge erreichen können, aber es gibt auch einige Unterschiede, einige Beispiele:

  • Sichtbarkeit - mit Mixins wird alles Teil der öffentlichen API, was bei Kompositionen nicht der Fall ist.
  • Ausführlichkeit - In den meisten Fällen sind Mixins weniger ausführlich und etwas einfacher zu verwenden, aber dies ist nicht immer der Fall (z. B. wenn Sie auch Selbsttypen in komplexen Hierarchien verwenden).

Ich weiß, die kurze Antwort lautet "Es kommt darauf an", aber wahrscheinlich gibt es eine typische Situation, in der dies oder das besser ist.

Einige Beispiele für Richtlinien, die ich bisher entwickeln konnte (vorausgesetzt, ich habe zwei Merkmale A und B und A möchte einige Methoden aus B verwenden):

  • Wenn Sie die API von A mit den Methoden von B erweitern möchten, mischen Sie, ansonsten Zusammensetzung. Es hilft jedoch nicht, wenn die von mir erstellte Klasse / Instanz nicht Teil einer öffentlichen API ist.
  • Wenn Sie einige Muster verwenden möchten, für die Mixins erforderlich sind (z. B. Stackable Trait Pattern ), ist dies eine einfache Entscheidung.
  • Wenn Sie zirkuläre Abhängigkeiten haben, können Mixins mit Selbsttypen hilfreich sein. (Ich versuche diese Situation zu vermeiden, aber es ist nicht immer einfach)
  • Wenn Sie dynamische Laufzeitentscheidungen zur Komposition und dann zur Objektkomposition wünschen.

In vielen Fällen scheinen Mixins einfacher (und / oder weniger ausführlich) zu sein, aber ich bin mir ziemlich sicher, dass sie auch einige Fallstricke haben, wie die "Gottklasse" und andere, die in zwei Artima-Artikeln beschrieben werden: Teil 1 , Teil 2 (übrigens) scheint mir, dass die meisten anderen Probleme für scala nicht relevant / nicht so ernst sind).

Haben Sie weitere Hinweise wie diese?

Sandor Murakozi
quelle

Antworten:

41

Viele der Probleme, die Menschen mit Mix-Ins haben, können in Scala abgewendet werden, wenn Sie nur abstrakte Merkmale in Ihre Klassendefinitionen einmischen und dann die entsprechenden konkreten Merkmale zur Objektinstanziierungszeit einmischen. Zum Beispiel

trait Locking{
   // abstract locking trait, many possible definitions
   protected def lock(body: =>A):A
}

class MyService{
   this:Locking =>
}

//For this time, we'll use a java.util.concurrent lock
val myService:MyService = new MyService with JDK15Locking 

Dieses Konstrukt hat mehrere Dinge zu empfehlen. Erstens wird verhindert, dass Klassen explodieren, da unterschiedliche Kombinationen von Merkmalsfunktionalitäten erforderlich sind. Zweitens ermöglicht es ein einfaches Testen, da man konkrete "Nichtstun" -Eigenschaften erstellen und einmischen kann, ähnlich wie bei Scheinobjekten. Schließlich haben wir das verwendete Schließmerkmal und sogar das Sperren vor den Verbrauchern unseres Dienstes vollständig verborgen.

Da wir die meisten der behaupteten Nachteile von Mix-Ins überwunden haben, bleibt uns immer noch ein Kompromiss zwischen Mix-In und Komposition. Normalerweise treffe ich die Entscheidung für mich selbst basierend darauf, ob ein hypothetisches Delegatenobjekt vollständig von dem enthaltenen Objekt gekapselt wird oder ob es möglicherweise gemeinsam genutzt werden kann und einen eigenen Lebenszyklus hat. Das Sperren ist ein gutes Beispiel für vollständig gekapselte Delegaten. Wenn Ihre Klasse ein Sperrobjekt verwendet, um den gleichzeitigen Zugriff auf ihren internen Status zu verwalten, wird diese Sperre vollständig vom enthaltenen Objekt gesteuert, und weder sie noch ihre Operationen werden als Teil der öffentlichen Schnittstelle der Klasse angekündigt. Für eine vollständig gekapselte Funktionalität wie diese verwende ich Mix-Ins. Verwenden Sie für etwas, das gemeinsam genutzt wird, z. B. eine Datenquelle, die Komposition.

Dave Griffith
quelle
Was bedeutet das Konstrukt this => Logging? Es wird nicht kompiliert.
HRJ
3
Es ist eine Annotation vom Typ Typ, deren Syntax ich immer vergesse. Bearbeitet. In diesem Fall bedeutet dies, dass jedes Objekt, das MyService erweitert, auch Locking erweitern muss
Dave Griffith
11

Andere Unterschiede, die Sie nicht erwähnt haben:

  • Merkmalsklassen haben keine eigenständige Existenz:

( Programmierskala )

Wenn Sie feststellen, dass ein bestimmtes Merkmal am häufigsten als übergeordnetes Merkmal anderer Klassen verwendet wird, sodass sich die untergeordneten Klassen als übergeordnetes Merkmal verhalten, sollten Sie das Merkmal stattdessen als Klasse definieren, um diese logische Beziehung klarer zu machen.
(Wir haben gesagt , dass es sich eher wie ein als verhält , weil das erstere die genauere Definition der Vererbung ist, die auf dem Liskov-Substitutionsprinzip basiert - siehe zum Beispiel [Martin2003].)

[Martin2003]: Robert C. Martin, Agile Softwareentwicklung: Prinzipien, Muster und Praktiken, Prentice-Hall, 2003

  • mixins ( trait) haben keine Konstruktorparameter.

Daher der Rat, immer noch von Programming Scala :

Vermeiden Sie konkrete Felder in Merkmalen, die nicht mit geeigneten Standardwerten initialisiert werden können.
Verwenden Sie stattdessen abstrakte Felder oder konvertieren Sie das Merkmal mit einem Konstruktor in eine Klasse .
Natürlich haben staatenlose Merkmale keine Probleme mit der Initialisierung.

Es ist ein allgemeines Prinzip eines guten objektorientierten Entwurfs, dass sich eine Instanz immer in einem bekannten gültigen Zustand befindet, beginnend mit dem Moment, in dem der Konstruktionsprozess abgeschlossen ist .

Der letzte Teil in Bezug auf den Anfangszustand eines Objektes, hat dazu beigetragen , oft entscheidet zwischen Klasse (und Klassenzusammensetzung) und Zuge (und Mixins) für ein bestimmtes Konzept.

VonC
quelle
Vielen Dank für Ihre Antwort, aber ich denke, es geht mehr um Eigenschaften gegen Klassen. Ich fühle mich in diesem Thema etwas wohler :-) Vielleicht war ich nicht präzise genug, als ich den Begriff "Klassenzusammensetzung" verwendete. Was ich meinte, wird im Artikel über stapelbare Merkmalsmuster beschrieben: "Dieses Muster ähnelt in seiner Struktur dem Dekorationsmuster, außer dass es eine Dekoration zum Zweck der Klassenzusammensetzung anstelle der Objektzusammensetzung beinhaltet", obwohl es Klassen und Merkmale mischt.
Sandor Murakozi
@ Sandor: Ich verstehe. Ich habe den grundlegenden Teil Ihrer Frage angesprochen, da ich mit der Unterscheidung noch nicht ganz zufrieden bin, aber Sie sollten bald genauere Antworten erhalten.
VonC
" mixins ( trait) haben keine Konstruktorparameter. " Interessanterweise unterstützt Dotty (um Scala 3 zu werden) Merkmalsparameter . Infolgedessen kann sich die von Ihnen erwähnte Faustregel von Programming Scala weiterentwickeln.
jub0bs
1
@Jubobs Danke. 8 Jahre später wundert es mich nicht, dass sich diese Regel weiterentwickelt.
VonC