Versiegelte Klassen. Semantik gegen Leistung

Wahrscheinlich nicht ich allein, nachdem ich die Dokumentation über versiegelte Klassen gelesen hatte, dachte ich: „Okay. Vielleicht wird es eines Tages nützlich sein. “ Später, als ich in meiner Arbeit einige Aufgaben traf, bei denen ich dieses Tool erfolgreich einsetzen konnte, dachte ich: „Nicht schlecht. Sie sollten oft über die Anwendung nachdenken. “ Und schließlich stieß ich auf eine Beschreibung der Aufgabenklasse im Buch Effective Java (Joshua Bloch, 3.) (ja, im Buch über Java).

Schauen wir uns eine Anwendung an und bewerten sie hinsichtlich Semantik und Leistung.


Ich denke, jeder, der mit der Benutzeroberfläche gearbeitet hat, hat die Implementierung der Interaktion der Benutzeroberfläche mit dem Dienst in bestimmten Zuständen erreicht , in denen eine Typmarkierung eines der Attribute war. Die Mechanik der Verarbeitung des nächsten Zustands in solchen Implementierungen hängt normalerweise direkt von der angegebenen Markierung ab. Zum Beispiel eine solche Implementierung der State- Klasse:

class State( val type: Type, val data: String?, val error: Throwable? ) { enum class Type { LOADING, ERROR, EMPTY, DATA } } 

Wir listen die Nachteile einer solchen Implementierung auf (probieren Sie es selbst aus)
Anmerkungen aus Kapitel 23 des Buches „Klassenhierarchien gegenüber markierten Klassen bevorzugen“. Ich schlage vor, sie kennenzulernen.

  1. Speicherverbrauch für Attribute, die nur für bestimmte Typen initialisiert werden. Der Faktor kann bei großen Mengen signifikant sein. Die Situation wird noch verschärft, wenn Standardobjekte zum Auffüllen der Attribute erstellt werden.
  2. Übermäßige semantische Belastung. Der Benutzer der Klasse muss überwachen, welcher Typ und welche Attribute verfügbar sind.
  3. Komplizierte Unterstützung in Geschäftslogikklassen. Angenommen, eine Implementierung, bei der ein Objekt einige Operationen an seinen Daten ausführen kann. Eine solche Klasse sieht aus wie ein Mähdrescher , und das Hinzufügen eines neuen Typs oder einer neuen Operation kann schwierig werden.


Die Verarbeitung eines neuen Status könnte folgendermaßen aussehen:

 fun handleState(state: State) { when(state.type) { State.Type.LOADING -> onLoading() State.Type.ERROR -> state.error?.run(::onError) ?: throw AssertionError("Unexpected error state: $state") State.Type.EMPTY -> onEmpty() State.Type.DATA -> state.data?.run(::onData) ?: throw AssertionError("Unexpected data state: $state") } } fun onLoading() {} fun onError(error: Throwable) {} fun onEmpty() {} fun onData(data: String) {} 

Bitte beachten Sie, dass der Compiler für Zustände wie ERROR und DATA die Sicherheit der Verwendung von Attributen nicht bestimmen kann, sodass der Benutzer redundanten Code schreiben muss. Änderungen in der Semantik können nur zur Laufzeit erkannt werden.

Versiegelte Klasse


Bild

Durch einfaches Refactoring können wir unseren Staat in eine Gruppe von Klassen aufteilen:

 sealed class State //    stateless  -     singleton object Loading : State() data class Error(val error: Throwable) : State() //  ,     ,  stateless  -  singleton object Empty : State() data class Data(val data: String) : State() 

Auf der Benutzerseite erhalten wir eine Statusverarbeitung, bei der die Zugänglichkeit von Attributen auf Sprachebene bestimmt wird und Missbrauch bei der Kompilierung zu Fehlern führt:

 fun handleState(state: State) { when(state) { Loading -> onLoading() is Error -> onError(state.error) Empty -> onEmpty() is Data -> onData(state.data) } } 

Da in Kopien nur signifikante Attribute vorhanden sind, können wir über das Speichern von Speicher und vor allem das Verbessern der Semantik sprechen. Benutzer versiegelter Klassen müssen die Regeln für die Arbeit mit Attributen abhängig von der Typmarkierung nicht manuell implementieren. Die Verfügbarkeit von Attributen wird durch die Trennung in Typen sichergestellt.

Ist das alles kostenlos?


Spoiler
Nein, nicht kostenlos.

Die Java-Entwickler, die Kotlin ausprobiert haben, müssen sich den dekompilierten Code angesehen haben, um zu sehen, wie die Kotlin-Java-Begriffe aussehen. Ein Ausdruck mit wann sieht ungefähr so ​​aus:

 public static final void handleState(@NotNull State state) { Intrinsics.checkParameterIsNotNull(state, "state"); if (Intrinsics.areEqual(state, Loading.INSTANCE)) { onLoading(); } else if (state instanceof Error) { onError(((Error)state).getError()); } else if (Intrinsics.areEqual(state, Empty.INSTANCE)) { onEmpty(); } else if (state instanceof Data) { onData(((Data)state).getData()); } } 

Zweige mit einer Fülle von Instanzen können aufgrund von Stereotypen über das „Zeichen für schlechten Code“ und die „Auswirkungen auf die Leistung“ alarmierend sein, aber wir haben keine Ahnung. Es ist notwendig, die Ausführungsgeschwindigkeit irgendwie zu vergleichen, zum Beispiel mit jmh .

Basierend auf dem Artikel „Die Geschwindigkeit von Java-Code korrekt messen“ wurde ein Test zur Verarbeitung von vier Zuständen (LOADING, ERROR, EMPTY, DATA) erstellt. Hier sind die Ergebnisse:

 Benchmark Mode Cnt Score Error Units CompareSealedVsTagged.sealed thrpt 500 940739,966 ± 5350,341 ops/s CompareSealedVsTagged.tagged thrpt 500 1281274,381 ± 10675,956 ops/s 

Es ist ersichtlich, dass die versiegelte Implementierung 25% langsamer arbeitet (es wurde angenommen, dass die Verzögerung 10-15% nicht überschreiten würde).

Wenn wir bei vier Typen eine viertel Verzögerung haben, mit zunehmenden Typen (Anzahl der Überprüfungsinstanzen), sollte die Verzögerung nur zunehmen. Um dies zu überprüfen, erhöhen wir die Anzahl der Typen auf 16 (nehmen wir an, wir haben es geschafft, eine so breite Hierarchie zu erhalten):

 Benchmark Mode Cnt Score Error Units CompareSealedVsTagged.sealed thrpt 500 149493,062 ± 622,313 ops/s CompareSealedVsTagged.tagged thrpt 500 235024,737 ± 3372,754 ops/s 

Zusammen mit einem Rückgang der Produktivität stieg die Verzögerung der versiegelten Verkäufe auf 35% - kein Wunder geschah.

Fazit


In diesem Artikel haben wir Amerika nicht entdeckt, und versiegelte Implementierungen in Branchen funktionieren beispielsweise langsamer als Linkvergleiche.


Dennoch müssen einige Gedanken geäußert werden:

  • In den meisten Fällen möchten wir mit einer guten Codesemantik arbeiten und die Entwicklung aufgrund zusätzlicher Hinweise aus der IDE und den Compilerprüfungen beschleunigen. In solchen Fällen können Sie versiegelte Klassen verwenden
  • Wenn Sie die Leistung einer Aufgabe nicht beeinträchtigen können, sollten Sie die versiegelte Implementierung vernachlässigen und beispielsweise durch eine Tagget-Implementierung ersetzen. Vielleicht sollten Sie Kotlin ganz zugunsten von Sprachen auf niedrigerer Ebene aufgeben

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


All Articles