Heim >Java >javaLernprogramm >Implementieren von Mixins (oder Merkmalen) in Kotlin mithilfe von Delegation

Implementieren von Mixins (oder Merkmalen) in Kotlin mithilfe von Delegation

Mary-Kate Olsen
Mary-Kate OlsenOriginal
2024-10-18 20:11:031058Durchsuche

Implementing Mixins (or Traits) in Kotlin Using Delegation

(Lesen Sie diesen Artikel auf Französisch auf meiner Website)

In der objektorientierten Programmierung ist ein Mixin eine Möglichkeit, einer Klasse eine oder mehrere vordefinierte und autonome Funktionalitäten hinzuzufügen. Einige Sprachen bieten diese Funktion direkt an, während andere beim Codieren von Mixins mehr Aufwand und Kompromisse erfordern. In diesem Artikel erkläre ich eine Implementierung von Mixins in Kotlin mithilfe der Delegation.

  • Objektiv
    • Definition des „Mixins“-Musters
    • Eigenschaften und Einschränkungen
  • Durchführung
    • Naiver Ansatz durch Komposition
    • Nutzung der Vererbung
    • Delegation zur Eindämmung des Mixin-Staates
    • Endgültige Implementierung
  • Einschränkungen
  • Beispiele
    • Überprüfbar
    • Beobachtbar
    • Entität/Identität
  • Fazit

Objektiv

Definition des „Mixins“-Musters

Das Mixin-Muster ist nicht so genau definiert wie andere Designmuster wie Singleton oder Proxy. Je nach Kontext kann es leichte Unterschiede in der Bedeutung des Begriffs geben.

Dieses Muster kann auch den „Traits“ ähneln, die in anderen Sprachen (z. B. Rust) vorhanden sind, aber ähnlich bedeutet der Begriff „Trait“ je nach verwendeter Sprache nicht unbedingt dasselbe1.

Das heißt, hier ist eine Definition aus Wikipedia:

In der objektorientierten Programmierung ist ein Mixin (oder Mix-In) eine Klasse, die Methoden enthält, die von anderen Klassen verwendet werden, ohne die übergeordnete Klasse dieser anderen Klassen sein zu müssen. Die Art und Weise, wie diese anderen Klassen auf die Methoden des Mixins zugreifen, hängt von der Sprache ab. Mixins werden manchmal als „eingeschlossen“ und nicht als „vererbt“ beschrieben.

Definitionen finden Sie auch in verschiedenen Artikeln zum Thema Mixin-basierte Programmierung (2, 3, 4). Diese Definitionen bringen auch den Gedanken der Klassenerweiterung ohne die durch die klassische Vererbung bereitgestellte Eltern-Kind-Beziehung (oder is-a) mit sich. Sie sind außerdem mit der Mehrfachvererbung verbunden, die in Kotlin (und auch nicht in Java) nicht möglich ist, aber als eines der Vorteile der Verwendung von Mixins dargestellt wird.

Eigenschaften und Einschränkungen

Eine Implementierung des Musters, die diesen Definitionen genau entspricht, muss die folgenden Einschränkungen erfüllen:

  • Wir können einer Klasse mehrere Mixins hinzufügen. Wir befinden uns in einem objektorientierten Kontext, und wenn diese Einschränkung nicht erfüllt wäre, wäre das Muster im Vergleich zu anderen Entwurfsmöglichkeiten wie Vererbung kaum interessant.
  • Die Mixin-Funktionalität kann von außerhalb der Klasse genutzt werden. Wenn wir diese Einschränkung außer Acht lassen, bringt das Muster ebenfalls nichts, was wir mit einer einfachen Komposition nicht erreichen könnten.
  • Das Hinzufügen eines Mixins zu einer Klasse zwingt uns nicht dazu, Attribute und Methoden in der Klassendefinition hinzuzufügen. Ohne diese Einschränkung könnte das Mixin nicht länger als „Blackbox“-Funktionalität angesehen werden. Wir könnten uns nicht nur auf den Schnittstellenvertrag des Mixins verlassen, um ihn einer Klasse hinzuzufügen, sondern müssten auch seine Funktionsweise verstehen (z. B. über Dokumentation). Ich möchte in der Lage sein, ein Mixin zu verwenden, während ich eine Klasse oder eine Funktion verwende.
  • Ein Mixin kann einen Zustand haben. Einige Mixins müssen für ihre Funktionalität möglicherweise Daten speichern.
  • Mixins können als Typen verwendet werden. Ich kann zum Beispiel eine Funktion haben, die jedes Objekt als Parameter akzeptiert, solange es ein bestimmtes Mixin verwendet.

Durchführung

Naiver Ansatz durch Komposition

Die einfachste Möglichkeit, einer Klasse Funktionalitäten hinzuzufügen, besteht darin, eine andere Klasse als Attribut zu verwenden. Auf die Funktionalitäten des Mixins kann dann durch Aufrufen von Methoden dieses Attributs zugegriffen werden.

class MyClass {
    private val mixin = Counter()

    fun myFunction() {
        mixin.increment()

        // ...
    }
}

Diese Methode liefert keine Informationen zum Typsystem von Kotlin. Beispielsweise ist es mit Counter unmöglich, eine Liste von Objekten zu erstellen. Die Verwendung eines Objekts vom Typ „Counter“ als Parameter hat kein Interesse, da dieser Typ nur das Mixin darstellt und somit ein Objekt ist, das für den Rest der Anwendung wahrscheinlich nutzlos ist.

Ein weiteres Problem bei dieser Implementierung besteht darin, dass die Funktionalitäten des Mixins nicht von außerhalb der Klasse zugänglich sind, ohne diese Klasse zu ändern oder das Mixin öffentlich zu machen.

Nutzung der Vererbung

Damit Mixins auch einen in der Anwendung verwendbaren Typ definieren, müssen wir von einer abstrakten Klasse erben oder eine Schnittstelle implementieren.

Die Verwendung einer abstrakten Klasse zum Definieren eines Mixins kommt nicht in Frage, da es uns nicht erlauben würde, mehrere Mixins für eine einzelne Klasse zu verwenden (es ist unmöglich, von mehreren Klassen in Kotlin zu erben).

Es wird somit ein Mixin mit einer Schnittstelle erstellt.

interface Counter {
    var count: Int
    fun increment() {
        println("Mixin does its job")
    }
    fun get(): Int = count
}

class MyClass: Counter {
    override var count: Int = 0 // We are forced to add the mixin's state to the class using it

    fun hello() {
        println("Class does something")
    }
}

Dieser Ansatz ist aus mehreren Gründen zufriedenstellender als der vorherige:

  • Die Klasse, die das Mixin verwendet, muss dank Standardmethoden das Verhalten des Mixins nicht implementieren
  • Eine Klasse kann mehrere Mixins verwenden, da Kotlin es einer Klasse ermöglicht, mehrere Schnittstellen zu implementieren.
  • Jedes Mixin erstellt einen Typ, der zum Bearbeiten von Objekten basierend auf den in seiner Klasse enthaltenen Mixins verwendet werden kann.

Allerdings gibt es bei dieser Implementierung noch eine erhebliche Einschränkung: Mixins können keinen Status enthalten. Tatsächlich können Schnittstellen in Kotlin zwar Eigenschaften definieren, diese jedoch nicht direkt initialisieren. Jede Klasse, die das Mixin verwendet, muss daher alle Eigenschaften definieren, die für den Betrieb des Mixins erforderlich sind. Dies berücksichtigt nicht die Einschränkung, dass die Verwendung eines Mixins uns nicht dazu zwingen soll, der Klasse, die es verwendet, Eigenschaften oder Methoden hinzuzufügen.

Wir müssen daher eine Lösung dafür finden, dass Mixins einen Status haben und gleichzeitig die Schnittstelle als einzige Möglichkeit beibehalten, sowohl einen Typ als auch die Möglichkeit zu haben, mehrere Mixins zu verwenden.

Delegation zur Eindämmung des Mixin-Staates

Diese Lösung ist etwas komplexer, um ein Mixin zu definieren; Es hat jedoch keine Auswirkungen auf die Klasse, die es verwendet. Der Trick besteht darin, jedem Mixin ein Objekt zuzuordnen, das den Status enthält, den das Mixin möglicherweise benötigt. Wir werden dieses Objekt verwenden, indem wir es mit der Delegationsfunktion von Kotlin verknüpfen, um dieses Objekt für jede Verwendung des Mixins zu erstellen.

Hier ist die grundlegende Lösung, die dennoch alle Einschränkungen erfüllt:

class MyClass {
    private val mixin = Counter()

    fun myFunction() {
        mixin.increment()

        // ...
    }
}

Endgültige Implementierung

Wir können die Implementierung weiter verbessern: Die CounterHolder-Klasse ist ein Implementierungsdetail, und es wäre interessant, ihren Namen nicht kennen zu müssen.

Um dies zu erreichen, verwenden wir ein Begleitobjekt auf der Mixin-Schnittstelle und das Muster „Factory Method“, um das Objekt zu erstellen, das den Status des Mixins enthält. Wir werden auch ein wenig schwarze Kotlin-Magie verwenden, sodass wir den Namen dieser Methode nicht kennen müssen:

interface Counter {
    var count: Int
    fun increment() {
        println("Mixin does its job")
    }
    fun get(): Int = count
}

class MyClass: Counter {
    override var count: Int = 0 // We are forced to add the mixin's state to the class using it

    fun hello() {
        println("Class does something")
    }
}

Einschränkungen

Diese Implementierung von Mixins ist nicht perfekt (meiner Meinung nach könnte keine perfekt sein, ohne auf Sprachebene unterstützt zu werden). Es weist insbesondere die folgenden Nachteile auf:

  • Alle Mixin-Methoden müssen öffentlich sein. Einige Mixins enthalten Methoden, die von der Klasse verwendet werden sollen, die das Mixin verwendet, andere sind sinnvoller, wenn sie von außen aufgerufen werden. Da das Mixin seine Methoden auf einer Schnittstelle definiert, ist es unmöglich, den Compiler zu zwingen, diese Einschränkungen zu überprüfen. Wir müssen uns dann auf Dokumentation oder statische Code-Analysetools verlassen.
  • Mixin-Methoden haben keinen Zugriff auf die Instanz der Klasse, die das Mixin verwendet. Zum Zeitpunkt der Delegierungsdeklaration ist die Instanz nicht initialisiert und wir können sie nicht an das Mixin übergeben.
interface Counter {
    fun increment()
    fun get(): Int
}

class CounterHolder: Counter {
    var count: Int = 0
    override fun increment() {
        count++
    }
    override fun get(): Int = count
}

class MyClass: Counter by CounterHolder() {
    fun hello() {
        increment()
        // The rest of the method...
    }
}

Wenn Sie dies innerhalb des Mixins verwenden, verweisen Sie auf die Instanz der Holder-Klasse.

Beispiele

Um das Verständnis des Musters, das ich in diesem Artikel vorschlage, zu verbessern, finden Sie hier einige realistische Beispiele für Mixins.

Überprüfbar

Dieses Mixin ermöglicht einer Klasse das „Aufzeichnen“ von Aktionen, die auf einer Instanz dieser Klasse ausgeführt werden. Das Mixin bietet eine weitere Methode zum Abrufen der neuesten Ereignisse.

class MyClass {
    private val mixin = Counter()

    fun myFunction() {
        mixin.increment()

        // ...
    }
}

Beobachtbar

Das Entwurfsmuster Observable kann einfach mithilfe eines Mixins implementiert werden. Auf diese Weise müssen beobachtbare Klassen nicht mehr die Abonnement- und Benachrichtigungslogik definieren oder die Liste der Beobachter selbst verwalten.

interface Counter {
    var count: Int
    fun increment() {
        println("Mixin does its job")
    }
    fun get(): Int = count
}

class MyClass: Counter {
    override var count: Int = 0 // We are forced to add the mixin's state to the class using it

    fun hello() {
        println("Class does something")
    }
}

In diesem speziellen Fall gibt es jedoch einen Nachteil: Auf die notifyObservers-Methode kann von außerhalb der Catalog-Klasse zugegriffen werden, auch wenn wir sie wahrscheinlich lieber privat halten würden. Aber alle Mixin-Methoden müssen öffentlich sein, um von der Klasse verwendet zu werden, die das Mixin verwendet (da wir keine Vererbung, sondern Komposition verwenden, auch wenn die durch Kotlin vereinfachte Syntax es wie Vererbung aussehen lässt).

Entität / Identität

Wenn Ihr Projekt persistente Geschäftsdaten verwaltet und/oder Sie zumindest teilweise DDD (Domain Driven Design) praktizieren, enthält Ihre Anwendung wahrscheinlich Entitäten. Eine Entität ist eine Klasse mit einer Identität, die häufig als numerische ID oder UUID implementiert wird. Diese Eigenschaft passt gut zur Verwendung eines Mixins, und hier ist ein Beispiel.

interface Counter {
    fun increment()
    fun get(): Int
}

class CounterHolder: Counter {
    var count: Int = 0
    override fun increment() {
        count++
    }
    override fun get(): Int = count
}

class MyClass: Counter by CounterHolder() {
    fun hello() {
        increment()
        // The rest of the method...
    }
}

Dieses Beispiel ist etwas anders: Wir sehen, dass uns nichts daran hindert, die Holder-Klasse anders zu benennen, und nichts uns daran hindert, Parameter während der Instanziierung zu übergeben.

Abschluss

Die Mixin-Technik ermöglicht die Anreicherung von Klassen durch das Hinzufügen häufig transversaler und wiederverwendbarer Verhaltensweisen, ohne dass diese Klassen geändert werden müssen, um diese Funktionalitäten zu berücksichtigen. Trotz einiger Einschränkungen tragen Mixins dazu bei, die Wiederverwendung von Code zu erleichtern und bestimmte Funktionalitäten zu isolieren, die mehreren Klassen in der Anwendung gemeinsam sind.

Mixins sind ein interessantes Werkzeug im Kotlin-Entwickler-Toolkit, und ich empfehle Ihnen, diese Methode in Ihrem eigenen Code zu erkunden und sich dabei der Einschränkungen und Alternativen bewusst zu sein.


  1. Unterhaltsame Tatsache: Kotlin hat ein Trait-Schlüsselwort, aber es ist veraltet und wurde durch interface ersetzt (siehe https://blog.jetbrains.com/kotlin/2015/05/kotlin-m12-is-out/#traits -are-now-interfaces) ↩

  2. Mixin-basierte Vererbung ↩

  3. Klassen und Mixins ↩

  4. Objektorientierte Programmierung mit Flavors ↩

Das obige ist der detaillierte Inhalt vonImplementieren von Mixins (oder Merkmalen) in Kotlin mithilfe von Delegation. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn