Kotlin DSL: Theorie und Praxis

Das Entwickeln von Anwendungstests ist keine angenehme Erfahrung. Dieser Prozess dauert lange, erfordert viel Konzentration und ist äußerst gefragt. Die Kotlin-Sprache bietet eine Reihe von Tools, mit denen Sie ganz einfach Ihre eigene problemorientierte Sprache (DSL) erstellen können. Es gibt Erfahrung, als Kotlin DSL Builder und statische Methoden zum Testen des Ressourcenplanungsmoduls ersetzte, was das Hinzufügen neuer Tests und das Unterstützen alter Tests aus einer Routine zu einem unterhaltsamen Prozess machte.

Im Verlauf des Artikels werden wir alle wichtigen Tools aus dem Entwicklerarsenal analysieren und analysieren, wie sie zur Lösung von Testproblemen kombiniert werden können. Wir werden den gesamten Weg von der Entwicklung des idealen Tests bis zur Einführung des ungefährsten, saubersten und verständlichsten Tests für das auf Kotlin basierende Ressourcenplanungssystem gehen.

Der Artikel ist nützlich für praktizierende Ingenieure, diejenigen, die Kotlin als eine Sprache zum bequemen Schreiben kompakter Tests betrachten, und diejenigen, die den Testprozess in ihrem Projekt verbessern möchten.



Dieser Artikel basiert auf einer Präsentation von Ivan Osipov ( i_osipov ) auf der JPoint-Konferenz. Weitere Erzählungen werden in seinem Namen durchgeführt. Ivan arbeitet als Programmierer bei Haulmont. Das Hauptprodukt des Unternehmens ist CUBA, eine Plattform für die Entwicklung von Unternehmens- und verschiedenen Webanwendungen. Auf dieser Plattform werden insbesondere Outsourcing-Projekte durchgeführt, darunter kürzlich ein Projekt im Bildungsbereich, bei dem Ivan einen Zeitplan für eine Bildungseinrichtung aufstellte. So kam es, dass Ivan in den letzten drei Jahren auf die eine oder andere Weise mit Planern zusammengearbeitet hat und speziell in Haulmont diesen Planer seit einem Jahr testet.

Für diejenigen, die Beispiele ausführen möchten - halten Sie einen Link zu GitHub . Unter dem Link finden Sie den gesamten Code, den wir heute analysieren, ausführen und schreiben werden. Öffnen Sie den Code und gehen Sie!



Heute werden wir diskutieren:

  • Was sind problemorientierte Sprachen?
  • eingebaute problemorientierte Sprachen;
  • Aufbau eines Zeitplans für eine Bildungseinrichtung;
  • wie alles mit Kotlin getestet wird.

Heute werde ich ausführlich über die Tools sprechen, die wir in der Sprache haben, Ihnen einige Demos zeigen und den gesamten Test von Anfang bis Ende schreiben. Gleichzeitig möchte ich objektiver sein, daher werde ich auf einige der Nachteile eingehen, die ich während der Entwicklung für mich selbst festgestellt habe.

Lassen Sie uns zunächst über das Modul zur Erstellung von Zeitplänen sprechen. Die Erstellung des Zeitplans erfolgt also in mehreren Schritten. Jeder dieser Schritte muss separat getestet werden. Sie müssen verstehen, dass wir trotz der Tatsache, dass die Schritte unterschiedlich sind, ein gemeinsames Datenmodell haben.



Dieser Prozess kann wie folgt dargestellt werden: Am Eingang gibt es einige Daten mit einem gemeinsamen Modell, am Ausgang gibt es einen Zeitplan. Die Daten werden validiert, gefiltert und anschließend Trainingsgruppen erstellt. Dies bezieht sich auf den Themenbereich des Stundenplans für die Bildungseinrichtung. Basierend auf den konstruierten Gruppen und auf der Grundlage einiger anderer Daten platzieren wir die Lektion. Heute werden wir nur über die letzte Phase sprechen - über die Platzierung von Klassen.



Ein bisschen über das Testen des Schedulers.

Zunächst müssen, wie Sie bereits verstanden haben, die verschiedenen Stufen separat getestet werden. Man kann einen mehr oder weniger standardmäßigen Prozess zum Starten des Tests herausgreifen: Es gibt eine Dateninitialisierung, es gibt einen Scheduler-Start, es gibt eine Überprüfung der Ergebnisse dieses Schedulers selbst. Es gibt eine große Anzahl unterschiedlicher Geschäftsfälle, die abgedeckt werden müssen, und verschiedene Situationen, die berücksichtigt werden müssen, damit diese Situationen beim Erstellen eines Zeitplans ebenfalls bestehen bleiben.

Ein Modell kann manchmal gewichtig sein. Um eine einzelne Entität zu erstellen, müssen fünf oder mehr zusätzliche Entitäten initialisiert werden. Insgesamt wird also eine große Menge an Code erhalten, die wir für jeden Test immer wieder schreiben. Die Unterstützung solcher Tests nimmt viel Zeit in Anspruch. Wenn Sie das Modell aktualisieren möchten und dies manchmal vorkommt, wirkt sich der Umfang der Änderungen auf die Tests aus.

Schreiben wir einen Test:



Schreiben wir den einfachsten Test, damit Sie das Bild allgemein verstehen.
Was fällt Ihnen zuerst ein, wenn Sie über das Testen nachdenken? Vielleicht sind dies einige primitive Tests dieser Art: Sie erstellen eine Klasse, erstellen eine Methode darin und markieren sie mit dem Annotationstest. Infolgedessen nutzen wir die Funktionen von JUnit und initialisieren einige Daten, Standardwerte und dann testspezifische Werte, machen dasselbe für den Rest des Modells und erstellen schließlich ein Scheduler-Objekt, übertragen unsere Daten darauf. Wir beginnen, wir erhalten Ergebnisse und wir überprüfen sie. Mehr oder weniger Standardverfahren. Aber es gibt offensichtlich eine Codeduplizierung. Das erste, was mir in den Sinn kommt, ist die Fähigkeit, alles in statische Methoden umzuwandeln. Da es eine Reihe von Standardwerten gibt, können Sie diese ausblenden.



Dies ist ein guter erster Schritt zur Reduzierung von Doppelarbeit.



Wenn Sie sich das ansehen, verstehen Sie, dass ich das Modell kompakter halten möchte. Hier haben wir ein Builder-Muster, in dem irgendwo unter der Haube der Standardwert initialisiert wird und die testspezifischen Werte genau dort initialisiert werden. Es wird besser, aber wir schreiben immer noch den Boilerplate-Code und wir schreiben ihn jedes Mal neu. Stellen Sie sich 200 Tests vor - Sie müssen diese drei Zeilen 200 Mal schreiben. Natürlich möchte ich das irgendwie loswerden. Bei der Entwicklung der Idee stoßen wir an eine bestimmte Grenze. So können wir beispielsweise generell für alles einen Pattern Builder erstellen.



Sie können einen Planer von Grund auf bis zum Ende erstellen, alle benötigten Werte festlegen, mit der Planung beginnen und alles ist großartig. Wenn Sie sich dieses Beispiel genauer ansehen und es detailliert analysieren, stellt sich heraus, dass viel unnötiger Code geschrieben wird. Ich möchte die Tests lesbarer machen, damit Sie einen Blick darauf werfen und sofort verstehen können, ohne sich mit den Mustern usw. zu befassen.

Wir haben also unnötigen Code. Einfache Mathematik legt nahe, dass es 55% mehr Buchstaben gibt, als wir brauchen, und ich würde gerne irgendwie davon wegkommen.



Nach einiger Zeit stellt sich heraus, dass die Unterstützung für unsere Tests teurer ist, da Sie mehr Code unterstützen müssen. Wenn wir keine Anstrengungen unternehmen, lässt die Lesbarkeit manchmal zu wünschen übrig oder sie stellt sich als akzeptabel heraus, aber wir möchten noch besser. Vielleicht werden wir später anfangen, eine Art Framework, Bibliotheken, hinzuzufügen, um das Schreiben von Tests zu vereinfachen. Aus diesem Grund erhöhen wir den Einstieg in das Testen unserer Anwendung. Hier haben wir eine bereits komplizierte Anwendung, der Einstieg in die Tests ist erheblich und wir erhöhen sie noch weiter.

Perfekter Test


Es ist toll zu sagen, wie schlecht alles ist, aber lassen Sie uns darüber nachdenken, wie gut es wäre. Ein ideales Beispiel, das wir als Ergebnis erhalten möchten:



Stellen Sie sich vor, es gibt eine Erklärung, in der wir sagen, dass dies ein Test mit einem bestimmten Namen ist, und wir möchten ein Leerzeichen verwenden, um die Wörter im Namen zu trennen, nicht CamelCase. Wir erstellen einen Zeitplan, wir haben einige Daten und die Ergebnisse des Planers werden überprüft. Da wir hauptsächlich mit Java arbeiten und der gesamte Code der Hauptanwendung in dieser Sprache geschrieben ist, möchte ich kompatible Testfunktionen haben. Ich möchte die Daten für den Leser so offensichtlich wie möglich initialisieren. Ich möchte einige allgemeine Daten und einen Teil des Modells initialisieren, die wir benötigen. Erstellen Sie beispielsweise Schüler und Lehrer und beschreiben Sie, wann sie verfügbar sind. Dies ist unser perfektes Beispiel.

Domänenspezifische Sprache




Wenn man alles betrachtet, scheint es wie eine Art problemorientierte Sprache. Sie müssen verstehen, was es ist und was der Unterschied ist. Sprachen können in zwei Typen unterteilt werden: Allzwecksprachen (was wir ständig schreiben, absolut alle Aufgaben lösen und absolut alles bewältigen) und problemorientierte Sprachen. So hilft uns beispielsweise SQL, Daten perfekt aus der Datenbank abzurufen, und einige andere Sprachen helfen auch, andere spezifische Probleme zu lösen.



Eine Möglichkeit, problemorientierte Sprachen zu implementieren, sind eingebettete oder interne Sprachen. Solche Sprachen werden auf der Basis einer Allzwecksprache implementiert. Das heißt, mehrere Konstruktionen unserer Allzwecksprache bilden so etwas wie eine Basis - das verwenden wir, wenn wir mit einer problemorientierten Sprache arbeiten. In diesem Fall ergibt sich natürlich in einer problemorientierten Sprache die Möglichkeit, alle Merkmale und Merkmale einer Allzwecksprache zu nutzen.



Schauen Sie sich noch einmal unser perfektes Beispiel an und überlegen Sie, welche Sprache Sie wählen sollen. Wir haben drei Möglichkeiten.



Die erste Option ist Groovy. Eine wunderbare, dynamische Sprache, die sich beim Aufbau problemorientierter Sprachen bewährt hat. Auch hier können Sie ein Beispiel für eine Build-Datei in Gradle geben, die viele von uns verwenden. Es gibt auch Scala, die eine Vielzahl von Möglichkeiten bietet, etwas Eigenes umzusetzen. Und schließlich gibt es Kotlin, das uns auch beim Aufbau einer problemorientierten Sprache hilft, und heute wird darüber diskutiert. Ich würde keine Kriege züchten und Kotlin mit etwas anderem vergleichen wollen, sondern es bleibt auf deinem Gewissen. Heute werde ich Ihnen zeigen, was Kotlin für die Entwicklung problemorientierter Sprachen hat. Wenn Sie dies vergleichen und sagen möchten, dass eine Sprache besser ist, können Sie zu diesem Artikel zurückkehren und den Unterschied leicht erkennen.



Was gibt uns Kotlin für die Entwicklung einer problemorientierten Sprache?

Erstens handelt es sich um eine statische Typisierung, die sich daraus ergibt. In der Kompilierungsphase wird eine große Anzahl von Problemen erkannt, was erheblich spart, insbesondere wenn Sie keine Probleme mit der Syntax und dem Schreiben in Tests haben möchten.
Dann gibt es ein großartiges Typinferenzsystem, das von Kotlin stammt. Das ist wunderbar, weil es nicht nötig ist, immer wieder Typen zu schreiben, alles wird vom Compiler mit einem Knall angezeigt.

Drittens gibt es eine hervorragende Unterstützung für die Entwicklungsumgebung, und dies ist nicht überraschend, da dasselbe Unternehmen die Hauptentwicklungsumgebung für heute erstellt und Kotlin.
Schließlich können wir in DSL natürlich Kotlin verwenden. Meiner subjektiven Meinung nach ist die Unterstützung von DSL viel einfacher als die Unterstützung von Utility-Klassen. Wie Sie später sehen werden, ist die Lesbarkeit etwas besser als bei Buildern. Was ich unter „besser“ verstehe: Sie erhalten etwas weniger Syntax, die Sie schreiben müssen - jemand, der Ihre problemorientierte Sprache liest, wird es schneller verstehen. Schließlich macht das Schreiben Ihres Fahrrads viel mehr Spaß! Tatsächlich ist die Implementierung einer problemorientierten Sprache jedoch viel einfacher als das Erlernen eines neuen Rahmens.

Ich werde noch einmal an den Link zu GitHub erinnern. Wenn Sie weitere Demos schreiben möchten, können Sie den Code vom Link abholen.

Das Ideal auf Kotlin entwerfen


Fahren wir mit der Gestaltung unseres Ideals fort, aber bereits bei Kotlin. Schauen Sie sich unser Beispiel an:



Und schrittweise werden wir beginnen, es wieder aufzubauen.

Wir haben einen Test, der sich in Kotlin in eine Funktion verwandelt, die mit Leerzeichen benannt werden kann.



Wir werden es mit der Testanmerkung markieren, die uns von JUnit zur Verfügung steht. In Kotlin können Sie die verkürzte Form zum Schreiben von Funktionen verwenden und durch = zusätzliche geschweifte Klammern für die Funktion selbst entfernen.

Zeitplan wir verwandeln uns in einen Block. Das gleiche passiert mit vielen Designs, da wir immer noch bei Kotlin arbeiten.



Fahren wir mit dem Rest fort. Geschweifte Klammern erscheinen wieder, wir werden sie nicht los, aber versuchen zumindest, unserem Beispiel näher zu kommen. Durch das Konstruieren von Konstruktionen mit Leerzeichen könnten wir uns irgendwie verfeinern und irgendwie anders machen, aber es scheint mir, dass es besser ist, die üblichen Methoden zu erstellen, die die Verarbeitung kapseln, aber im Allgemeinen ist dies für den Benutzer offensichtlich .



Unser Schüler verwandelt sich in einen Block, in dem wir mit Eigenschaften und Methoden arbeiten, und wir werden dies weiterhin mit Ihnen analysieren.



Endlich die Lehrer. Hier haben wir einige verschachtelte Blöcke.



Im folgenden Code fahren wir mit den Überprüfungen fort. Wir müssen die Kompatibilität mit Java-Sprachen überprüfen - und ja, Kotlin ist mit Java kompatibel.



Arsenal der DSL-Entwicklung bei Kotlin




Fahren wir mit der Liste der Tools fort, die wir haben. Hier habe ich ein Tablet mitgebracht, vielleicht listet es alles auf, was zur Entwicklung problemorientierter Sprachen in Kotlin benötigt wird. Sie können von Zeit zu Zeit zu ihr zurückkehren und ihr Gedächtnis auffrischen.

Die Tabelle zeigt einen Vergleich der problemorientierten Syntax mit der üblichen Syntax, die in der Sprache verfügbar ist.

Lambdas in Kotlin


val lambda: () -> Unit = { }

Beginnen wir mit dem grundlegendsten Ziegelstein, den wir in Kotlin haben - das sind Lambdas.
Mit Lambda-Typ meine ich heute nur einen funktionalen Typ. Lambdas werden wie folgt bezeichnet: ( ) -> .

Wir initialisieren das Lambda mit Hilfe von geschweiften Klammern, in die wir einen Code schreiben können, der aufgerufen wird. Das heißt, ein Lambda verbirgt diesen Code tatsächlich nur in sich. Das Ausführen eines solchen Lambda sieht aus wie ein Funktionsaufruf, nur in Klammern.



Wenn wir einen Parameter übergeben wollen, müssen wir ihn zunächst im Typ beschreiben.
Zweitens haben wir Zugriff auf die Standardkennung, die wir verwenden können. Wenn dies jedoch nicht zu uns passt, können wir unseren eigenen Parameternamen festlegen und diese verwenden.



Gleichzeitig können wir die Verwendung dieses Parameters überspringen und den Unterstrich verwenden, um keine Bezeichner zu erzeugen. In diesem Fall wäre es zum Ignorieren des Bezeichners möglich, überhaupt nichts zu schreiben, aber im allgemeinen Fall gibt es für mehrere Parameter das erwähnte "_".



Wenn wir mehr als einen Parameter übergeben möchten, müssen wir deren Bezeichner explizit definieren.



Was passiert schließlich, wenn wir versuchen, das Lambda an eine Funktion zu übergeben und dort auszuführen? In der anfänglichen Annäherung sieht es wie folgt aus: Wir haben eine Funktion, an die wir Lambda in geschweiften Klammern übergeben, und wenn in Kotlin Lambda als letzter Parameter geschrieben wird, können wir es aus diesen Klammern heraussetzen.



Wenn in den Klammern nichts mehr vorhanden ist, können wir die Klammern entfernen. Diejenigen, die mit Groovy vertraut sind, sollten damit vertraut sein.



Wo gilt das? Absolut überall. Das heißt, die sehr lockigen Klammern, über die wir bereits gesprochen haben, verwenden wir, das sind genau die Lambdas.



Schauen wir uns nun eine der Lambdasorten an, ich nenne sie Lambdas mit Kontext. Sie werden einige andere Namen finden, zum Beispiel Lambda mit Empfänger, und sie unterscheiden sich von gewöhnlichen Lambdas, wenn Sie einen Typ wie folgt deklarieren: Links fügen wir eine Kontextklasse hinzu, es kann eine beliebige Klasse sein.



Wofür ist das? Dies ist notwendig, damit wir innerhalb des Lambda Zugriff auf das Schlüsselwort this haben - dies ist das Schlüsselwort selbst. Es gibt Auskunft über unseren Kontext, dh über ein Objekt, das wir mit unserem Lambda verknüpft haben. So können wir beispielsweise ein Lambda erstellen, das einen String ausgibt. Natürlich verwenden wir die String-Klasse, um einen Kontext zu deklarieren, und der Aufruf eines solchen Lambda sieht folgendermaßen aus:







Wenn Sie einen Kontext als Parameter übergeben möchten, können Sie dies genauso gut tun. Wir können den Kontext jedoch nicht vollständig vermitteln, dh ein Lambda mit einem Kontext erfordert Aufmerksamkeit! - Kontext, ja. Was passiert, wenn wir ein Lambda mit einem Kontext an eine Methode übergeben? Hier sehen wir uns noch einmal unsere exec-Methode an:



Benennen Sie es in die Student-Methode um - nichts hat sich geändert:



Also bewegen wir uns allmählich zu unserer Konstruktion, der Studentenkonstruktion, die unter den Klammern jede Initialisierung verbirgt.



Lass es uns herausfinden. Wir haben eine Art Studentenfunktion, die ein Lambda mit dem Studentenkontext verbindet.



Natürlich brauchen wir Kontext.



Hier erstellen wir ein Objekt und führen dieses Lambda darauf aus.



Infolgedessen können wir auch einige Standardwerte initialisieren, bevor wir das Lambda starten, sodass wir alles kapseln, was wir für die Funktion benötigen.



Aus diesem Grund erhalten wir im Lambda Zugriff auf das Schlüsselwort this. Deshalb gibt es wahrscheinlich Lambdas mit Kontext.



Natürlich können wir dieses Schlüsselwort loswerden und haben die Möglichkeit, solche Konstruktionen zu schreiben.



Auch wenn wir nicht nur proprietäre, sondern auch einige Methoden haben, die wir auch nennen können, sieht es ziemlich natürlich aus.



Anwendung


Alle diese Lambdas im Code sind Kontext-Lambdas. Es gibt eine Vielzahl von Kontexten, die sich auf die eine oder andere Weise überschneiden und es uns ermöglichen, unsere problemorientierte Sprache aufzubauen.



Zusammenfassend die Lambdas - wir haben gewöhnliche Lambdas, die es mit dem Kontext gibt, und diese und andere können verwendet werden.



Betreiber


Kotlin verfügt über eine begrenzte Anzahl von Operatoren, die Sie mithilfe von Konventionen und dem Schlüsselwort operator überschreiben können.

Schauen wir uns den Lehrer und seine Zugänglichkeit an. Nehmen wir an, der Lehrer arbeitet montags ab 8 Uhr für 1 Stunde. Wir möchten auch sagen, dass es zusätzlich zu dieser einen Stunde ab 13.00 Uhr für 1 Stunde funktioniert. Ich möchte dies mit dem Operator + ausdrücken. Wie kann das gemacht werden?



Es gibt eine Verfügbarkeitsmethode, die ein Lambda mit einem AvailabilityTable Kontext akzeptiert. Dies bedeutet, dass es eine Klasse gibt, die so heißt, und die Montag-Methode wird in dieser Klasse deklariert. Diese Methode gibt DayPointer seitdem zurück Sie müssen unseren Operator an etwas anhängen.



Lassen Sie uns herausfinden, was DayPointer ist. Dies ist ein Hinweis auf die Verfügbarkeitstabelle eines Lehrers, und der Tag steht auf seinem Zeitplan. Wir haben auch eine Zeitfunktion, die einige Zeilen irgendwie in ganzzahlige Indizes verwandelt: In Kotlin haben wir dafür eine IntRange Klasse.

Links ist DayPointer , rechts ist Zeit, und wir möchten sie mit dem Operator + kombinieren. Dazu können Sie unseren Operator in der DayPointer Klasse DayPointer . Es werden verschiedene Werte vom Typ Int benötigt und DayPointer damit wir unser DSL immer wieder DayPointer können.
Schauen wir uns nun das Schlüsseldesign an, mit dem alles beginnt und mit dem unser DSL beginnt. Die Implementierung ist etwas anders, und jetzt werden wir es herausfinden.
Kotlin hat ein Singleton-Konzept, das direkt in die Sprache integriert ist. Zu diesem object wird anstelle des Schlüsselworts class das Schlüsselwort object verwendet. Wenn wir eine Methode in einem Singleton erstellen, können wir so darauf zugreifen, dass keine Instanz dieser Klasse erneut erstellt werden muss. Wir bezeichnen es einfach als statische Methode in einer Klasse.



Wenn Sie sich das Ergebnis der Dekompilierung ansehen (dh in der Entwicklungsumgebung auf Extras -> Kotlin -> Kotlin-Bytecode anzeigen -> Dekompilieren klicken), sehen Sie die folgende Singleton-Implementierung:



Dies ist nur eine gewöhnliche Klasse, und hier passiert nichts Übernatürliches.
Ein weiteres interessantes Tool ist die invoke Anweisung. Stellen Sie sich vor, wir haben eine Klasse A, wir haben ihre Instanz, und wir möchten diese Instanz ausführen, dh Klammern für ein Objekt dieser Klasse aufrufen, und dies können wir dank des invoke tun.



In Klammern können wir die Aufrufmethode aufrufen und haben einen Operator-Modifikator. Wenn wir diesem Operator ein Lambda mit Kontext übergeben, erhalten wir eine solche Konstruktion.



Jedes Mal Instanzen zu erstellen ist eine andere Aktivität, sodass wir vorheriges und aktuelles Wissen kombinieren können.

Lassen Sie uns einen Singleton erstellen, ihn als Zeitplan bezeichnen, innerhalb dessen wir den Aufrufoperator deklarieren, innerhalb einen Kontext erstellen und ein Lambda mit dem hier erstellten Kontext akzeptieren. Es stellt sich heraus, dass es sich um einen einzelnen Einstiegspunkt in unser DSL handelt, und als Ergebnis erhalten wir den gleichen Konstruktionsplan mit geschweiften Klammern.



Nun, wir haben über den Zeitplan gesprochen. Schauen wir uns unsere Schecks an.
Wir haben Lehrer, wir haben eine Art Zeitplan erstellt, und wir möchten überprüfen, ob im Zeitplan dieses Lehrers an einem bestimmten Tag in einer bestimmten Lektion ein Objekt vorhanden ist, mit dem wir arbeiten werden.



Ich möchte eckige Klammern verwenden und auf unseren Zeitplan so zugreifen, dass er optisch wie der Zugriff auf Arrays aussieht.



Dies kann mit dem Operator erfolgen: get / set:



Hier machen wir nichts Neues, folgen Sie einfach den Konventionen. Im Fall des Set-Operators müssen wir die Werte zusätzlich an unsere Methode übergeben:



Die eckigen Klammern zum Lesen werden also zu get, und die eckigen Klammern, durch die wir sie zuweisen, werden zu set.

Demo: Objekt, Operatoren


Sie können entweder weiteren Text lesen oder das Video hier ansehen . Das Video hat eine eindeutige Startzeit, es ist jedoch keine Endzeit angegeben. Grundsätzlich können Sie es nach dem Start vor dem Ende des Artikels ansehen.

Der Einfachheit halber werde ich kurz auf die Essenz des Videos direkt im Text eingehen.

Schreiben wir einen Test. Wir haben ein Zeitplanobjekt, und wenn wir über Strg + b zu dessen Implementierung gehen, werden wir alles sehen, worüber ich zuvor gesprochen habe.



Innerhalb des Zeitplanobjekts möchten wir die Daten initialisieren, dann einige Überprüfungen durchführen, und innerhalb der Daten möchten wir Folgendes sagen:

  • Unsere Schule ist ab 8 Uhr morgens geöffnet.
  • Es gibt eine Reihe von Elementen, für die wir einen Zeitplan erstellen werden.
  • Es gibt einige Lehrer, die eine Art Zugänglichkeit beschrieben haben.
  • einen Studenten haben;
  • Grundsätzlich müssen wir für einen Studenten nur sagen, dass er ein bestimmtes Fach studiert.



Und hier zeigt sich einer der Nachteile von Kotlin und problemorientierten Sprachen im Prinzip: Es ist ziemlich schwierig, einige Objekte zu adressieren, die wir zuvor erstellt haben. In dieser Demo werde ich alles als Indizes angeben, dh Rus ist Index 0, Mathe ist Index 2. Und der Lehrer führt natürlich auch etwas. Er geht nicht nur zur Arbeit, sondern beschäftigt sich mit etwas. Für Leser dieses Artikels möchte ich eine weitere Option für die Adressierung anbieten. Sie können eindeutige Tags erstellen und Entitäten in Map speichern. Wenn Sie auf eines davon zugreifen müssen, können Sie es immer nach Tag suchen. Zerlegen Sie das DSL weiter.

Hier ist Folgendes zu beachten: Erstens haben wir den Operator +, zu dessen Implementierung wir auch sehen können, dass wir tatsächlich die DayPointer-Klasse haben, die uns hilft, all dies mit Hilfe des Operators zu binden.

Und dank der Tatsache, dass wir Zugriff auf den Kontext haben, sagt uns die Entwicklungsumgebung, dass wir in unserem Kontext über dieses Schlüsselwort Zugriff auf eine Sammlung haben und diese verwenden werden.



Das heißt, wir haben eine Sammlung von Ereignissen. Das Ereignis enthält eine Reihe von Eigenschaften, zum Beispiel: Es gibt einen Schüler, einen Lehrer, an welchem ​​Tag sie sich in welcher Lektion treffen.



Wir schreiben den Test weiter.



Auch hier verwenden wir den get-Operator. Es ist nicht so einfach, zu seiner Implementierung zu gelangen, aber wir können es tun.



Tatsächlich folgen wir einfach der Vereinbarung, damit wir Zugriff auf dieses Design erhalten.
Kehren wir zur Präsentation zurück und setzen das Gespräch über Kotlin fort. Wir wollten, dass die Überprüfungen bei Kotlin durchgeführt werden, und haben diese Ereignisse durchlaufen:



Ein Ereignis ist im Wesentlichen eine gekapselte Menge von 4 Eigenschaften. Ich möchte dieses Ereignis in eine Reihe von Eigenschaften wie ein Tupel zerlegen. Im Russischen wird eine solche Konstruktion als Mehrfachdeklaration (ich habe nur eine solche Übersetzung gefunden) oder als Strukturierungsdeklaration bezeichnet und funktioniert wie folgt:



Wenn einer von Ihnen mit dieser Funktion nicht vertraut ist, funktioniert sie folgendermaßen: Sie können das Ereignis aufnehmen und an der Stelle, an der es verwendet wird, mithilfe von Klammern in eine Reihe von Eigenschaften zerlegen.



Dies funktioniert, weil wir eine componentN-Methode haben, dh eine Methode, die vom Compiler dank des Datenmodifikators generiert wird, den wir vor der Klasse schreiben.



Gleichzeitig fliegen eine Vielzahl anderer Methoden zu uns. Wir interessieren uns für die componentN-Methode, die basierend auf den in der Parameterliste des Primärkonstruktors aufgeführten Eigenschaften generiert wird.



Wenn wir keinen Datenmodifikator hätten, müsste manuell ein Operator geschrieben werden, der dasselbe tut.





Wir haben also einige componentN-Methoden, die sich in einen solchen Aufruf zerlegen:



Im Wesentlichen ist es syntaktischer Zucker über den Aufruf mehrerer Methoden.

Wir haben bereits über eine Verfügbarkeitstabelle gesprochen, und tatsächlich habe ich Sie getäuscht. Es passiert. Es gibt keine avaiabilityTable , sie liegt nicht in der Natur, aber es gibt eine Matrix von Booleschen Werten.



Es ist keine zusätzliche Klasse erforderlich: Sie können die Matrix der Booleschen Werte zur besseren Übersichtlichkeit umbenennen. Dies kann mit den sogenannten Typealias oder Type Alias ​​erfolgen . Leider erhalten wir keine zusätzlichen Boni, es ist nur eine Umbenennung. Wenn Sie die Verfügbarkeit übernehmen und sie wieder in die Matrix der Booleschen Werte umbenennen, ändert sich überhaupt nichts. Der Code hat funktioniert und wird funktionieren.

Werfen wir einen Blick auf den Lehrer, genau diese Zugänglichkeit, und sprechen über ihn:



Wir haben einen Lehrer, und die Verfügbarkeitsmethode wird aufgerufen (haben Sie den Faden der Argumentation noch nicht verloren? :-). Woher kam er? Das heißt, ein Lehrer ist eine Art Entität, die eine Klasse hat, und dies ist ein Geschäftscode. Und es kann keine zusätzliche Methode geben.



Diese Methode wird aufgrund von Erweiterungsfunktionen angezeigt. Wir übernehmen eine andere Funktion, die wir für Objekte dieser Klasse ausführen können, und befestigen sie an unserer Klasse.
Wenn wir dieser Funktion etwas Lambda übergeben und es dann für eine vorhandene Eigenschaft ausführen, ist alles in Ordnung - die Verfügbarkeitsmethode in ihrer Implementierung initialisiert die Verfügbarkeitseigenschaft. Sie können dies loswerden. Wir kennen bereits den Aufrufoperator, der an einen Typ angehängt werden kann und gleichzeitig eine Erweiterungsfunktion ist. Wenn Sie diesem Operator ein Lambda übergeben, können wir dieses Lambda genau dort mit dem Schlüsselwort this ausführen. Wenn wir mit einem Lehrer arbeiten, ist die Barrierefreiheit daher eine Eigenschaft des Lehrers und keine zusätzliche Methode, und hier findet keine Rassynchronisation statt.



Als Bonus können Erweiterungsfunktionen für nullbare Typen erstellt werden. Dies ist gut, denn wenn es eine Variable mit einem nullbaren Typ gibt, die einen Nullwert enthält, ist unsere Funktion bereits dafür bereit und fällt nicht von NullPointer ab. Innerhalb dieser Funktion kann dies null sein, und dies muss behandelt werden.



Zusammenfassung der Erweiterungsfunktionen: Sie müssen verstehen, dass nur auf die öffentliche API der Klasse zugegriffen werden kann und die Klasse selbst in keiner Weise geändert wird. Eine Erweiterungsfunktion wird durch den Typ der Variablen und nicht durch den tatsächlichen Typ bestimmt. Darüber hinaus wird ein Mitglied der Klasse mit derselben Signatur priorisiert. Sie können eine Erweiterungsfunktion für eine Klasse erstellen, diese jedoch in eine völlig andere Klasse schreiben. Innerhalb dieser Erweiterungsfunktion können Sie gleichzeitig auf zwei Kontexte zugreifen. Es stellt sich die Schnittstelle von Kontexten heraus. Und schließlich ist dies eine großartige Gelegenheit, die Bediener im Allgemeinen an jedem Ort zu befestigen, an dem wir wollen.



Das nächste Tool sind Infix-Funktionen. Ein weiterer gefährlicher Hammer in den Händen des Entwicklers. Warum gefährlich? Was Sie sehen, ist Code. Ein solcher Code kann in Kotlin geschrieben werden, und tun Sie es nicht! Bitte nicht. Trotzdem ist der Ansatz gut. Dank dessen ist es möglich, Punkte und Klammern zu entfernen - von all der lauten Syntax, von der wir versuchen, so weit wie möglich wegzukommen und unseren Code ein wenig sauberer zu machen.



Wie funktioniert es Nehmen wir ein einfacheres Beispiel - eine ganzzahlige Variable. Lassen Sie uns eine Erweiterungsfunktion dafür erstellen, nennen wir es shouldBeEqual, es wird etwas tun, aber das ist nicht interessant. Wenn wir den Infix-Modifikator links davon hinzufügen, ist das genug. Sie können Punkte und Klammern entfernen, aber es gibt ein paar Nuancen.



Auf dieser Grundlage wird nur das Daten- und Assertionskonstrukt implementiert und zusammengefügt.


Lass es uns herausfinden. Wir haben einen SchedulingContext - den allgemeinen Kontext für die Planung des Starts. Es gibt eine Datenfunktion, die das Ergebnis dieser Planung zurückgibt. Gleichzeitig erstellen wir eine Erweiterungsfunktion und gleichzeitig die Zusicherungen der Infixfunktion, die ein Lambda starten, das unsere Werte überprüft.



Es gibt ein Subjekt, ein Objekt und eine Handlung, und Sie müssen sie irgendwie verbinden. In diesem Fall ist das Ergebnis der Ausführung von Daten mit geschweiften Klammern das Thema. Das Lambda, das wir an die Assertions-Methode übergeben, ist ein Objekt, und die Assertions-Methode selbst ist eine Aktion. All dies scheint zusammenzuhalten.



Apropos Funktionsinfix: Es ist wichtig zu verstehen, dass dies ein Schritt ist, um verrauschte Syntax zu beseitigen. Wir müssen jedoch ein Subjekt und ein Objekt dieser Aktion haben und den Infix-Modifikator verwenden. Es kann genau einen Parameter geben - das heißt, Nullparameter können nicht sein, zwei können nicht sein, drei - nun, Sie verstehen. Sie können beispielsweise Lambdas an diese Funktion übergeben und auf diese Weise Konstrukte erhalten, die Sie zuvor noch nicht gesehen haben.

Fahren wir mit der nächsten Demo fort. Es ist besser, das Video anzuschauen und den Text nicht zu lesen.



Jetzt sieht alles fertig aus: Das Funktionsinfix, das Sie gesehen haben, die Erweiterung der Funktion, die Sie gesehen haben, die Destrukturierungsdeklaration ist fertig.

Kehren wir zu unserer Präsentation zurück, und hier kommen wir zu einem ziemlich wichtigen Punkt beim Aufbau problemorientierter Sprachen - Sie sollten über Kontextkontrolle nachdenken.



Es gibt Situationen, in denen wir DSL verwenden und direkt darin wiederverwenden können, aber wir möchten dies nicht tun. Unser Benutzer (möglicherweise ein unerfahrener Benutzer) schreibt Daten in Daten, und das macht keinen Sinn. Wir möchten ihm das irgendwie verbieten.

Vor Kotlin Version 1.1 mussten wir Folgendes tun: Als Reaktion auf die Tatsache, dass wir eine Datenmethode in SchedulingContext , mussten wir eine andere DataContext in DataContext erstellen, in die wir ein Lambda akzeptieren (wenn auch ohne Implementierung), sollten wir diese Methode markieren Anmerkung @Deprecated und @Deprecated Sie den Compiler an, dies nicht zu kompilieren. Sie sehen, dass diese Methode startet - nicht kompilieren. Mit diesem Ansatz erhalten wir sogar eine aussagekräftige Nachricht, wenn wir bedeutungslosen Code schreiben.



Nach der Version Kotlin 1.1 erschien eine wundervolle Anmerkung @DslMarker . Diese Anmerkung wird benötigt, um abgeleitete Anmerkungen zu kennzeichnen. Mit ihnen werden wir wiederum problemorientierte Sprachen markieren. Für jede problemorientierte Sprache können Sie eine Anmerkung erstellen, die Sie als @DslMarker markieren, und sie an jeden benötigten Kontext hängen. Es besteht keine Notwendigkeit mehr, zusätzliche Methoden zu schreiben, deren Kompilieren verboten werden muss - alles funktioniert einfach. Nicht kompiliert.

Es gibt jedoch einen solchen Sonderfall, wenn wir mit unserem Geschäftsmodell arbeiten. Es ist normalerweise in Java geschrieben.Es gibt einen Kontext, es gibt eine Anmerkung, die als Kontext markiert werden muss. Was ist Ihrer Meinung nach der Kontext des Schülers innerhalb der Methode? Klasse Student. Dies ist ein Teil unseres Geschäftsmodells, Kotlin ist nicht da.





Wir möchten diese Situation auch irgendwie kontrollieren, da in diesem Fall Zugriff auf das folgende Design besteht: Erstellen Sie einen Schüler innerhalb der Schüler. Ich möchte Sie nicht dazu bringen, falsche Assoziationen herzustellen, aber wir möchten es verbieten, es ist falsch.



Wir haben drei Möglichkeiten.

  1. Erstellen Sie einen ganzen Kontext, der für unseren Schüler verantwortlich ist. Nennen wir es StudentContext. Wir beschreiben dort alle Eigenschaften und erstellen darauf basierend einen Schüler. Eine Art Wahnsinn - eine Menge Code wird geschrieben, wahrscheinlich mehr als für die Produktion.
  2. – , , . . StudentContext , IStudent . , Student, IStudent StudentContext. DslMarker , .
  3. : deprecated . , . , . extension-, . .



Selbst auf dieser Ebene können Sie den Kontext steuern, jedoch mit einigen Einschränkungen, die umgangen werden müssen.



Zusammenfassung der Kontextsteuerung. Schützen Sie Ihre Benutzer vor Fehlern. Es ist klar, dass Benutzer keine Fehler machen werden, da dies offensichtlich ist, aber es immer noch wünschenswert ist, dies zu kontrollieren. Darüber hinaus kostet die Implementierung einer solchen Kontrolle nicht so viel Geld und Zeit. Verwenden Sie die Annotation @DslMarker, mit der Sie Ihre eigenen Annotationen versehen. In Situationen, in denen Sie die Annotation @DslMarker nicht verwenden können, verwenden Sie die Annotation @Deprecated. Auf diese Weise können Sie die Fälle umgehen, die noch nicht funktionieren.

Also, die Kontextkontroll-Demo:





Nachteile und Probleme


Erstens die Wiederverwendung von DSL-Teilen. Sie haben bereits heute gesehen, dass das Adressieren von mit DSL erstellten Entitäten problematisch sein kann. Es gibt Möglichkeiten, dies zu umgehen, aber es ist ratsam, im Voraus darüber nachzudenken, um einen Plan dafür zu haben.

Stellen Sie sich vor, Sie haben einen Code und möchten ihn beispielsweise nur in einem Zyklus wiederholen, um Schüler, viele, viele Male dieselben Schüler oder eine andere Entität erstellen zu können. Wie kann man das machen? Sie können die for-Schleife verwenden - nicht die beste Option. Sie können eine zusätzliche Methode in Ihrem DSL erstellen. Dies ist eine bessere Lösung. Sie müssen diese Probleme jedoch direkt auf DSL-Ebene lösen. Achten Sie auf das Schlüsselwort this und die Standardbenennung des Parameters it. Glücklicherweise haben wir mit der Kotlin-Version des Plugins 1.2.20 Hinweise, die direkt in der Entwicklungsumgebung sichtbar sind. Der graue Code sagt uns, mit welchem ​​Kontext wir arbeiten oder was es ist.

Verschachtelung kann ein Problem sein. Sie haben eine schöne DSL-Datei erstellt, aber die Initialisierung des Modells geht tiefer, tiefer, tiefer. Daher verwenden Sie häufig eher einen horizontalen als einen vertikalen Bildlauf. Es ist ratsam, Standardwerte unter der Standardimplementierung auszublenden. Ein Benutzer, der nur einen Schüler benötigt, möchte nichts über ein Schulungsprogramm oder etwas anderes wissen. Er möchte nur einen Schüler ohne Details erstellen und nicht einmal einen Namen angeben. Versuchen Sie, die Syntax zu verkürzen. Geben Sie beispielsweise einige Standardwerte an, übergeben Sie ein leeres Lambda usw.

Zum Schluss die Dokumentation. Meiner subjektiven Meinung nach ist die beste Dokumentation für Ihre problemorientierte Sprache mehr als die Anzahl der Beispiele für dieses DSL. Großartig, wenn Sie Kotlin-Docks haben. Dies ist ein guter Bonus. Wenn der DSL-Benutzer jedoch keine Ahnung hat, welche Designs verfügbar sind, können er und die Kotlin-Docks nirgendwo suchen. Hast du das jemals gefühlt? Wenn Sie gleich zu Beginn eine Gradle-Datei schreiben, verstehen Sie nicht, was darin enthalten ist, und benötigen einige Beispiele. Sie kümmern sich nicht um Kontexte, Sie möchten Beispiele, und dies ist die beste Dokumentation, die von neuen Benutzern Ihres DSL verwendet werden kann.



Setzen Sie DSLs bitte nicht in alle Ritzen. Ich möchte das wirklich tun, wenn Sie dieses Tool besitzen. Ich möchte sagen, lassen Sie uns hier, vielleicht hier und hier, ein DSL erstellen. Erstens ist dies ein undankbarer Job. Zweitens ist es immer noch wünschenswert, dies am Zielort anzuwenden. Wo es Ihnen wirklich hilft, ein Problem zu lösen.
Schließlich lernen Kotlin. Entdecken Sie die Möglichkeiten, die diese Sprache bietet, neue Funktionen, damit Ihr Code sauberer, kürzer und kompakter wird und viel einfacher zu lesen ist. Und wenn Sie wieder zum Testen zurückkehren (z. B. wenn Sie etwas hinzugefügt haben, müssen Sie einen Test dafür durchführen), werden Sie dies viel mehr tun, da DSL so kompakt und komfortabel wie möglich ist und Sie kein Problem damit haben, ein Dutzend zu erstellen Studenten. Nur in ein paar Zeilen ist dies erledigt.

Trainiere "Katzen" als Held eines berühmten Films. Meiner Meinung nach ist es zunächst einfacher, Kotlin als Test in Ihr Projekt einzubeziehen. Dies ist eine gute Gelegenheit, die Sprache zu überprüfen, auszuprobieren und sich die Funktionen anzusehen. Dies ist so ein Schlachtfeld, auf dem man es auch dann benutzen kann, wenn nichts funktioniert.
Schließlich entwerfen Sie das DSL vor. Heute habe ich ein perfektes Beispiel gezeigt, und wir sind schrittweise gegangen, um eine problemorientierte Sprache aufzubauen. Wenn Sie das DSL vorab entwerfen, wird es letztendlich viel einfacher. Sie werden es nicht zehnmal umgestalten. Sie müssen sich keine Sorgen machen, dass sich die Kontexte irgendwie überschneiden und logisch stark miteinander verbunden sind. Entwerfen Sie das DSL einfach vorab - es ist ziemlich einfach, es auf einem Blatt Papier zu tun, wenn Sie die Entwürfe kennen, die ich Ihnen heute erzählt habe.

Und schließlich Kontakte für die Kommunikation. Mein Name ist Ivan Osipov, Telegramm: @ivan_osipov , Twitter: @_osipov_ , Habr: i_osipov . Ich werde auf Ihre Kommentare warten.

Minute der Werbung. JPoint — , 19-20 - Joker 2018 — Java-. . , .

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


All Articles