Erweiterungen in Kotlin. Gefährlicher Atavismus oder nützliches Werkzeug?



Kotlin ist noch eine junge Sprache, hat aber bereits Einzug gehalten. Aus diesem Grund ist nicht immer klar, wie dieses oder jenes Funktionsmerkmal korrekt implementiert werden soll und welche bewährten Methoden angewendet werden sollen.

Besonders schwierig ist der Fall bei den Fähigkeiten der Sprache, die nicht in Java sind. Einer dieser Stolpersteine ​​war die Expansion .

Dies ist ein praktisches Tool, mit dem der Code besser lesbar ist und für das fast nichts zurückgegeben werden muss. Gleichzeitig kenne ich aber mindestens einen Menschen, der, wenn er Expansion nicht als böse ansieht, diesen durchaus skeptisch gegenübersteht. Im Folgenden möchte ich die Merkmale dieses Mechanismus erörtern, die zu Kontroversen und Missverständnissen führen können.

Erweiterungen zu DTO - Verstoß gegen die Datenübertragungsobjektvorlage


Zum Beispiel gibt es eine Klasse User

class User(val name: String, val age: Int, val sex: String) 

Ein ziemlicher DTO! Ferner ist in dem Code an mehreren Stellen eine Überprüfung erforderlich, um festzustellen, ob der Benutzer ein Erwachsener ist. Am einfachsten ist es, an allen Stellen eine Bedingung zu stellen

 if (user.age >= 18) { ... } 

Da es jedoch beliebig viele solcher Stellen geben kann, ist es sinnvoll, diese Prüfung in die Methode aufzunehmen.
Hier gibt es drei Möglichkeiten:

  1. Funktionsspaß isAdult (Benutzer: Benutzer) - Dienstprogrammklassen bestehen normalerweise aus solchen Funktionen.
  2. Fügen Sie die isAdult-Funktion in die User-Klasse ein

     class User(val name: String, val age: Int, val sex: String) { fun isAdult() = age >= 18 } 

  3. Schreiben Sie einen Wrapper für User, der ähnliche Funktionen enthält.

Alle drei Optionen sind technisch lebensberechtigt. Der erste Nachteil besteht jedoch darin, dass alle Utility-Funktionen bekannt sein müssen, obwohl dies natürlich kein großes Problem darstellt.
Die zweite Option scheint das Datenübertragungsobjektmuster zu verletzen, da es sich bei der Klasse nicht nur um Getter und Setter handelt. Aber Muster zu brechen ist schlecht.

Die dritte Option verstößt weder gegen die Prinzipien von OOP noch gegen Vorlagen. Sie müssen jedoch jedes Mal einen Wrapper erstellen, wenn Sie ähnliche Funktionen verwenden möchten. Diese Option ist auch nicht sehr beliebt. Am Ende stellt sich heraus, dass Sie noch Opfer bringen müssen.

Meiner Meinung nach ist es einfacher, eine DTO-Vorlage zu opfern. Erstens habe ich keine einzige Erklärung dafür gefunden, warum Funktionen (mit Ausnahme von Gettern und Setzern) nicht im DTO erstellt werden können. Und zweitens ist ein solcher Code nur in Bezug auf die Bedeutung bequem neben den Daten zu haben, mit denen wir arbeiten.

Es ist jedoch nicht immer möglich, einen solchen Code in den DTO-Shek einzufügen, da der Entwickler nicht immer die Möglichkeit hat, die Klassen zu bearbeiten, mit denen er arbeitet. Dies können beispielsweise Klassen sein, die aus xsd generiert wurden. Darüber hinaus kann es für jemanden ungewöhnlich und unangenehm sein, solchen Code in Datenklassen zu schreiben. Kotlin bietet für solche Situationen eine Lösung in Form von Funktionen und Erweiterungsfeldern:

 fun User.isAdult() = age >= 18 

Dieser Code kann so verwendet werden, als ob er in der Benutzerklasse deklariert wäre:

 if(user.isAdult()) {...} 

Das Ergebnis ist eine ziemlich genaue Lösung, die mit dem geringsten Kompromiss unsere Anforderungen erfüllt. Wenn wir über die Tatsache sprechen, dass die DTO-Vorlage verletzt wird, möchten wir daran erinnern, dass es sich in Java um eine reguläre statische Methode des Formulars handelt:

 public static final boolean isAdult(@NotNull User receiver) 

Wie Sie sehen, wird formal auch die Vorlage nicht verletzt. Die Verwendung dieser Funktion sieht so aus, als ob sie in User deklariert wurde, und Idea bietet sie in automatischer Vervollständigung an. Es ist sehr bequem.

Erweiterungen sind spezifisch. Sie können nicht über deren Existenz Bescheid wissen und Methoden und Felder der Entität mit Erweiterungen verwechseln


Die Idee ist, dass der Entwickler zu dem Projekt gekommen ist und in Bezug auf den Code, der in Erweiterungen implementiert ist, nicht klar ist, welche Methode original ist und welche die Erweiterungsmethode ist.

Dies ist kein Problem, da Idea dem Entwickler in dieser Angelegenheit hilft und solche Funktionen hervorhebt. Obwohl fairerweise gesagt werden muss, dass der Unterschied im Darcula-Thema besser erkennbar ist. Wenn Sie es in Light ändern, wird alles weniger offensichtlich und die Erweiterung unterscheidet sich nur in kursiver Schrift.

Unten sehen wir ein Beispiel für den Aufruf von zwei Methoden: isAdult ist die Erweiterungsmethode, isMale ist die übliche Methode innerhalb der User-Klasse. Der Screenshot links zeigt das Darcula-Thema, rechts das übliche Light-Thema.



Etwas schlimmer ist es mit den Feldern. Wenn wir uns zum Beispiel dafür entscheiden, isAdult als Erweiterungsfeld zu implementieren, können wir es nur nach Schriftart von einem regulären Feld unterscheiden. In diesem Beispiel ist name ein reguläres Feld. Ein Erweiterungsfeld erzeugt nur Kursivschrift.



Mithilfe der Idea-Entwicklungsumgebung können Sie ermitteln, welche Methode eine Erweiterung ist und welche beim automatischen Vervollständigen das Original ist. Das ist bequem.



Ähnlich ist die Situation bei Feldern.



"Für Benutzer in <root>" bedeutet, dass es sich um eine Erweiterung handelt.

Die Tatsache, dass Idea eine Erweiterung an eine erweiterbare Entität „bindet“, trägt außerdem erheblich zur Entwicklung bei, da Erweiterungsmethoden und -felder für die automatische Vervollständigung vorgeschlagen werden.

Erweiterungen sind über das gesamte Projekt verteilt und bilden eine Mülltonne


Wir haben kein solches Problem bei Projekten, da wir keine willkürlichen Erweiterungen einfügen und Code mit öffentlichen Erweiterungen in separaten Dateien oder Paketen entfernen.

Beispielsweise könnte die Funktion isAdult aus dem obigen Beispiel in der Benutzerdatei des Erweiterungspakets angezeigt werden. Wenn das Paket nicht ausreicht und Sie nur nicht verwechseln möchten, wo sich die Klasse befindet und wo sich die Funktionsdatei befindet, können Sie sie beispielsweise _User.kt nennen. So auch die Entwickler von JetBrains für Kollektionen. Wenn das Gewissen das Starten der Datei mit einem Unterstrich verbietet, können Sie user.kt aufrufen. Tatsächlich gibt es keinen Unterschied in der Art und Weise, die Hauptsache ist, dass es eine Einheitlichkeit gibt, an der das gesamte Team festhält.

Die Ersteller der Sprache haben sie bei der Entwicklung von Erweiterungsmethoden für Sammlungen in die Datei _Collections.kt eingefügt .

Hierbei handelt es sich im Allgemeinen um die Organisation des Codes, nicht um das Problem von Erweiterungen. Statische Funktionen in Java und nicht nur statische Funktionen können nicht weniger zufällig verteilt werden als Erweiterungen.

Übersehen Sie während des Gerätetests nicht die Erweiterungsfunktionen


Meiner Meinung nach besteht keine Notwendigkeit, Funktionen von Erweiterungen zu benetzen, genauso wie es keine Notwendigkeit gibt, statische Methoden zu benetzen. In der Erweiterungsfunktion sollten Sie die Logik des Arbeitens mit vorhandenen Daten einfügen. Im Fall der isAdult-Funktion für die User-Klasse befindet sich beispielsweise alles, was Sie benötigen, in isAdult. Sie müssen nicht nass werden.

Betrachten Sie ein etwas komplexeres Beispiel. Es gibt eine bestimmte Komponente, die dazu dient, Benutzer von einem externen System abzurufen - UserComponent. Die Methode zum Abrufen von Benutzern heißt getUsers. Angenommen, es mussten alle aktiven Benutzer abgerufen werden, und es wurde beschlossen, eine Filterlogik in Form einer Funktion - einer Erweiterung - hinzuzufügen. Als Ergebnis haben wir die Funktion:

 fun UserComponent.getActiveUsers(): List<Users> = this.getUsers().filter{it.status == “Active”} 

Es mag so aussehen, als ob es sich um eine Situation handelt, in der Sie einen Schein für eine Erweiterung benötigen. Wenn Sie sich jedoch daran erinnern, dass getActiveUsers nur eine statische Methode ist, stellt sich heraus, dass kein Mock benötigt wird. Dip sollte die Methoden und Funktionen sein, die in der Erweiterung aufgerufen werden, und nicht mehr.

Es ist möglich, dass sich die Erweiterungsfunktion mit der gleichnamigen Funktion innerhalb der erweiterten Klasse überschneidet


Wir werden diesen Fall anhand des Beispiels aus dem ersten Absatz betrachten. Angenommen, es gibt eine Funktionserweiterung isAdult, die überprüft, ob der Benutzer ein Erwachsener ist:

 fun User.isAdult() = age >= 18 

Danach implementieren wir die gleichnamige Funktion in User:

 class User(val name: String, val age: Int, val sex: String){ fun isAdult() = age >= 21 } 

Wenn user.isAdult () aufgerufen wird, wird eine Funktion aus der Klasse aufgerufen, obwohl es eine gleichnamige Erweiterung und eine geeignete Funktion gibt. Ein solcher Fall kann verwirrend sein, da Benutzer, die die in der Klasse deklarierte Funktion nicht kennen, auf den Abschluss der Erweiterungsfunktion warten. Dies ist eine unangenehme Situation, die äußerst schwerwiegende Folgen haben kann. In diesem Fall geht es nicht um die mögliche Unannehmlichkeit einer Überprüfung oder einer Vorlagenverletzung, sondern um das möglicherweise fehlerhafte Verhalten des Codes.

Die oben beschriebene Situation zeigt, dass bei Verwendung der Erweiterungsfunktionen echte Probleme auftreten können.

Um dies zu vermeiden, sollten Sie nicht vergessen, die Erweiterungsfunktionen so weit wie möglich mit Unit-Tests abzudecken. Im schlimmsten Fall, wenn die Tests fehlschlagen, gibt es zwei Funktionen, die auf die gleiche Weise funktionieren. Einer ist eine Erweiterung und der andere ist in der Klasse selbst. Wenn die Tests fehlschlagen, wird darauf hingewiesen, dass eine Funktion eine andere überlappt.

Die Erweiterung ist an eine Klasse und nicht an ein Objekt gebunden, was zu Verwirrung führen kann


Betrachten Sie beispielsweise die Benutzerklasse aus dem ersten Absatz. Lassen Sie uns es öffnen und seinen Nachfolger Student erstellen:

 class Student(name: String, age: Int, sex: String): User(name, age, sex) 

Wir definieren die Erweiterungsfunktion für Student, die auch bestimmt, ob der Student ein Erwachsener ist oder nicht. Nur für den Schüler ändern wir die Bedingung:

 fun Student.isAdult() = this.age >= 16 

Und jetzt schreiben wir den folgenden Code:

 val user: User = Student("", 17, "M") 

Was gibt user.isAdult ()) zurück?
Es scheint, dass ein Objekt vom Typ Student und die Funktion true zurückgeben sollten. Aber es ist nicht so einfach. Erweiterungen werden an die Klasse angehängt, nicht an das Objekt, und das Ergebnis ist falsch.

Wenn wir uns daran erinnern, dass Erweiterungen statische Methoden sind und eine erweiterbare Entität der erste Parameter in dieser Methode ist, ist dies nichts Seltsames. Dies ist ein weiterer Punkt, den Sie bei der Verwendung dieses Mechanismus berücksichtigen sollten. Andernfalls können Sie einen unangenehmen und unerwarteten Effekt erzielen.

Anstelle der Ausgabe


Diese kontroversen Punkte scheinen nicht gefährlich zu sein, wenn Sie sich daran erinnern, dass wir Extension sagen - wir meinen eine statische Methode. Wenn Sie diese Funktionen mit Komponententests abdecken, können Sie außerdem mögliche Unklarheiten im Zusammenhang mit der statischen Natur von Erweiterungen minimieren.

Meiner Meinung nach sind Erweiterungen ein leistungsstarkes und praktisches Tool, mit dem sich die Qualität und Lesbarkeit des Codes verbessern lässt, ohne dass dafür fast nichts erforderlich ist. Deshalb liebe ich sie:

  • Mit Erweiterungen können Sie Logik schreiben, die für den Kontext einer erweiterbaren Klasse spezifisch ist. Dank dessen werden die Felder und Erweiterungsmethoden so gelesen, als wären sie immer in der erweiterten Entität vorhanden, was wiederum das Verständnis des Codes auf oberster Ebene verbessert. In Java ist dies leider nicht möglich. Darüber hinaus haben Erweiterungen dieselben Zugriffsmodifikatoren wie reguläre Funktionen. Auf diese Weise können Sie ähnlichen Code mit dem Gültigkeitsbereich schreiben, der für eine bestimmte Funktion wirklich erforderlich ist.
  • Es ist praktisch, die Erweiterungsfunktionen für das Mapping zu verwenden, die Sie bei der Lösung alltäglicher Aufgaben oft sehen müssen. Zum Beispiel gibt es im Projekt eine Klasse UserFromExternalSystem, die beim Aufrufen eines externen Systems verwendet wird, und es wäre großartig, das Mapping in die Erweiterungsfunktion einzufügen, es zu vergessen und es so zu verwenden, als ob es ursprünglich in User wäre.

     callExternalSystem(user.getUserFromExternalSystem()) 

    Das gleiche kann natürlich mit der üblichen Methode gemacht werden, aber diese Option ist weniger lesbar:

     callExternalSystem(getUserFromExternalSystem(user)) 

    oder eine solche Option:

     val externalUser = getUserFromExternalSystem(user) callExternalSystem(externalUser) 

    Tatsächlich passiert keine Magie, aber dank solcher Kleinigkeiten ist es angenehmer, mit dem Code zu arbeiten.
  • Unterstützung für Idee und automatische Vervollständigung. Im Gegensatz zu Methoden aus Utility-Klassen werden Erweiterungen von der Entwicklungsumgebung gut unterstützt. Bei der automatischen Vervollständigung werden Erweiterungen von der Umgebung als "native" Funktionen und Felder angeboten. Dies ermöglicht eine gute Steigerung der Entwicklerproduktivität.
  • Für Erweiterungen spricht, dass ein Großteil der Kotlin-Bibliotheken als Erweiterungen geschrieben ist. Viele praktische und beliebte Methoden zum Arbeiten mit Sammlungen sind Erweiterungen (Filter, Map usw.). Sie können dies überprüfen, indem Sie die Datei _Collections.kt untersuchen .

Vorteile von Erweiterungen decken mögliche Nachteile ab. Natürlich besteht die Gefahr eines Missbrauchs dieses Mechanismus und der Versuchung, den gesamten Code in Erweiterungen zu packen. Hier geht es aber eher um die Organisation des Codes und den kompetenten Umgang mit dem Tool. Bei korrekter Verwendung werden Erweiterungen zu einem echten Freund und Helfer beim Schreiben von gut gelesenem und gepflegtem Code.

Unten finden Sie Links zu Materialien, die zur Vorbereitung dieses Artikels verwendet wurden:

  1. proandroiddev.com/kotlin-extension-functions-more-than-sugar-1f04ca7189ff - hier werden interessante Gedanken darüber aufgegriffen, dass wir mithilfe von Erweiterungen enger mit dem Kontext arbeiten.
  2. www.nikialeksey.com/2017/11/14/kotlin-is-bad.html - hier lehnt der Autor Erweiterungen ab und gibt ein interessantes Beispiel, das in einem der obigen Punkte diskutiert wird.
  3. medium.com/@elizarov/i-do-not-see-much-reason-to-mock-extension-functions-7f24d88a188a - Roman Elizarovs Meinung zur Benetzung von Extensionsmethoden.

Ich möchte mich auch bei Kollegen bedanken, die mit interessanten Fällen und Gedanken zu diesem Material geholfen haben.

Source: https://habr.com/ru/post/de485760/


All Articles