Geben Sie Klassen in Scala ein


In der Community der Scala-Entwickler wurde in letzter Zeit dem Entwurfsmuster der Typklassen immer mehr Aufmerksamkeit geschenkt . Es hilft, mit unnötigen Abhängigkeiten umzugehen und gleichzeitig den Code sauberer zu machen. Im Folgenden werde ich anhand von Beispielen zeigen, wie es angewendet wird und welche Vorteile dieser Ansatz hat.

Der Artikel richtet sich nicht nur an Programmierer, die in Scala schreiben, sondern auch in Java - vielleicht erhalten sie eine Antwort für sich selbst, da er zumindest theoretisch wie eine Lösung für viele angewandte Probleme aussieht, bei denen die Komponenten nicht miteinander verbunden und nach dem Schreiben erweiterbar sind . Es kann auch für Entwickler und Designer in anderen Sprachen interessant sein.

Hintergrund


Jeder hatte das Problem, wenn eine Klasse das mit dieser Klasse verknüpfte Verhalten hinzufügen muss: Vergleich, Serialisierung, Lesen aus der Serialisierung, Instanziieren einer Klasse, Rendern. Dies kann ein weiteres Verhalten sein, das Ihre sehr nützliche Bibliothek benötigt.

In der Java-Welt gibt es verschiedene Methoden zur Lösung dieser Probleme:
  • Schreiben Sie eine Schnittstelle und implementieren Sie sie in der Klasse selbst (z. B. Comparable).
  • Schreiben Sie eine separate Schnittstelle, von der eine bestimmte Instanz das Klassenobjekt empfängt und die erforderlichen Aktionen ausführt (z. B. Comparator).
  • Es gibt sogar spezielle Methoden für einzelne Aufgaben:
    • zum Erstellen von Objekten - Factory-Vorlage,
    • Für die Verknüpfung mit Bibliotheken von Drittanbietern wird der Adapter verwendet.
    • Verwenden Sie zum Serialisieren und Lesen manchmal Reflection. Dies erinnert an eine Autopsie aus einem kürzlich veröffentlichten Beitrag , aber manchmal ist auch eine Bauchoperation erforderlich. Ich werde sofort eine Reservierung vornehmen. Typklassen können diese nicht ersetzen.

Im Wesentlichen haben wir jetzt zwei Alternativen: entweder eine Funktion eines Drittanbieters eng mit einer Klasse verknüpfen oder die Funktion in einem separaten Objekt herausnehmen und sich dann um deren Übertragung kümmern. Nehmen wir als Beispiel die Anwendungsbenutzer. Dies könnte so aussehen, als ob Sie die Möglichkeit hinzufügen könnten, unsere Benutzer zu vergleichen:

// Определение метода, которому нужно сравнение
def sort[T <: Comparable[T]](elements: Seq[T]): Seq[T]
// Определение класса
class Person(val id: Id[Person], val name: String) extends Comparable[Person] {
  def compareTo(Person anotherPerson): Int = {
    return this.name.compareTo(anotherPerson.name)
  }
}
// Использование
sort(persons)

Es ist gut, dass die spezifische Anwendung der Sortierung sehr klar aussieht. Die Vergleichslogik ist jedoch eng mit der Klasse verbunden (und wahrscheinlich mag es niemand, dass die Objekte des Modells beispielsweise von der Bibliothek abhängen, die XML generiert und in die Datenbank schreibt). Darüber hinaus tritt ein weiteres Problem auf: Es ist unmöglich, mehr als eine Vergleichsmethode zu definieren. Wenn wir morgen Benutzer an anderer Stelle im Programm anhand ihrer ID vergleichen möchten, ist dies nicht erfolgreich und wir können die Vergleichsmethode für private Klassen nicht neu schreiben.

Java verfügt zu diesem Zweck über eine Comparator-Klasse, mit der Sie mehr Flexibilität erhalten:

// Определение метода, которому нужно сравнение
def sort[T](elements: Seq[T], comparator: Comparator[T]): Seq[T]
// Определение класса
class Person(val id: Id[Person], val name: String)
trait Comparator[T] {
  def compare(object1: T, object2 T): Int
}
class PersonNameComparator extends Comparator[Person] {
  def compare(onePerson: Person, anotherPerson: Person): Int = {
    return onePerson.name.compareTo(anotherPerson.name)
  }
}
// Использование
val nameComparator = new PersonNameComparator()
sort(persons, nameComparator)

Jetzt können Sie mehrere Vergleichsmethoden definieren, die Vergleichslogik ist der Modellklasse nicht mehr zugeordnet. Auch hindert Sie nichts daran, einen eigenen Komparator zu schreiben, selbst wenn die Klasse und die Definition des Sortieralgorithmus für uns geschlossen sind. Es ist erwähnenswert, dass der Sortieraufruf etwas komplizierter geworden ist.

Die Verwendung von Fabriken und Adaptern impliziert eine ähnliche Verwaltung des Lebenszyklus und die Übertragung ihrer Instanzen. Und dennoch ist es notwendig, sich all diese coolen Namen für im Allgemeinen eine Standardaufgabe zu merken.

Und dann erscheinen Typklassen


Hier kommt Scalas Fähigkeit zum Einsatz, Parameter implizit zu übergeben. Nehmen wir unser vorheriges Beispiel als Grundlage, aber definieren Sie den Algorithmus anders. Wir werden den Komparator implizit übergeben:

def sort[T](elements: Seq[T])(implicit comparator: Comparator[T]): Seq[T]

Das heißt, wenn es im Bereich einen geeigneten Komparator mit dem gewünschten Wert des Typparameters gibt, wird dieser vom Compiler automatisch ohne zusätzlichen Aufwand des Programmierers in die Methode eingesetzt. Stellen Sie also den entsprechenden Komparator in den Geltungsbereich:

implicit val personNameComparator = Comparator[Person] {
  def compare(onePerson: Person, anotherPerson: Person): Int = {
    return onePerson.name.compareTo(anotherPerson.name)
  }
}

Das implizite Schlüsselwort stellt sicher, dass der Wert bei der Ersetzung verwendet wird. Es ist wichtig zu beachten, dass unsere impliziten Implementierungen zustandslos sein müssen, da im Prozess des Programms nur eine Instanz jedes Typs erstellt wird.

Jetzt kann die Sortierung auf dieselbe Weise aufgerufen werden wie in der Originalversion mit der Implementierung von Comparable:

// Вызываем сортировку
sort(persons)

Und das trotz der Tatsache, dass die Vergleichsmethode selbst in keiner Weise mit dem Modellobjekt verbunden ist. Wir können beliebige Vergleichsmethoden in den Geltungsbereich einfügen, die für unsere Objekte verwendet werden.

Eine etwas interessantere Option ergibt sich, wenn der Typparameter selbst eingegeben werden soll. Das heißt, für Map [Id [Person], List [Permission]] möchten wir MapJsonSerializer , IdJsonSerializer , ListJsonSerializer und PermissionJsonSerializer , die in beliebiger Reihenfolge wiederverwendet werden können, anstatt PersonPermissionsMapJsonSerializer , Analoga, von denen wir jedes Mal schreiben. In diesem Fall unterscheidet sich die Methode zur Bestimmung des impliziten Objekts geringfügig. Jetzt haben wir kein Objekt, sondern eine Funktion:

implicit def ListComparator[V](implicit comparator: Comparator[V]) = new Comparator[List[V]] {
  def compare(oneList: List[V], anotherList: List[V]): Int = {
    for((one, another) <- oneList.zip(anotherList)) {
      val elementsCompared = comparator,compare(one, another)
      if(elementsCompared > 0) return 1
      else if(elementsCompared < 0) return -1
    }
    return 0
  }
}

Das ist die ganze Methode. Das Beste daran ist, dass Sie alle Arten von JSONParsern und XMLSerializern zusammen mit PersonFactory erhalten können, ohne die Korrespondenz von Klassen und Objekten irgendwo zu speichern - der Scala-Compiler erledigt alles für uns.

In TCS verwenden wir diese Methode beispielsweise, um Ausnahmen in die Klassen unseres Modells zu verpacken. Mit Typklassen können Sie Ausnahmeinstanzen des Typs erstellen, in den Sie den ausgelösten Block einschließen möchten. Wenn dies nach der herkömmlichen Methode erfolgen würde, müsste eine Ausnahmefabrik erstellt und übertragen werden, sodass es einfacher wäre, Ausnahmen auf die alte Weise von Hand zu werfen. Jetzt ist alles schön.

Was weiter?


Tatsächlich endet das Thema Typklassen hier nicht, und als Fortsetzung empfehle ich die Video- Typklassen in Scala .

Das Thema wird in diesem Artikel noch grundlegender dargestellt .

Jetzt auch beliebt: