Funktionsfehlerbehandlung in Kotlin mit Arrow

Bild

Hallo Habr!

Jeder liebt Laufzeitausnahmen. Es gibt keinen besseren Weg, um herauszufinden, dass beim Schreiben des Codes etwas nicht berücksichtigt wurde. Insbesondere - wenn Ausnahmen die Anwendung bei Millionen von Benutzern löschen und diese Nachricht in einer Panik-E-Mail vom Analyseportal eingeht. Samstagmorgen. Wenn Sie auf einer Landreise sind.

Danach denken Sie ernsthaft über die Fehlerbehandlung nach - und welche Möglichkeiten bietet uns Kotlin?

Das erste, was mir in den Sinn kommt, ist Try-Catch. Für mich - eine großartige Option, aber sie hat zwei Probleme:

  1. Dies ist immerhin ein zusätzlicher Code (ein erzwungener Wrapper um den Code beeinträchtigt die Lesbarkeit nicht optimal).
  2. Es ist nicht immer (insbesondere bei Verwendung von Bibliotheken von Drittanbietern) möglich, aus dem catch-Block eine informative Nachricht darüber zu erhalten, was genau den Fehler verursacht hat.

Mal sehen, aus Try-Catch Code wird, wenn versucht wird, die oben genannten Probleme zu lösen.

Zum Beispiel die einfachste Ausführungsfunktion für Netzwerkabfragen

fun makeRequest(request: RequestBody): List<ResponseData>? { val response = httpClient.newCall(request).execute() return if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { null } } 

wird wie

 fun makeRequest(request: RequestBody): List<ResponseData>? { try { val response = httpClient.newCall(request).execute() return if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { null } } catch (e: Exception) { log.error("SON YOU DISSAPOINT: ", e.message) return null } } 

"Es ist nicht so schlimm", könnte jemand sagen, "Sie und Ihr Kotlin wollen den ganzen Code Zucker", fügt er hinzu (dies ist ein Zitat) - und er wird ... zweimal Recht haben. Nein, heute wird es keine Holivars geben - jeder entscheidet für sich. Ich persönlich habe den Code des selbstgeschriebenen JSON-Parsers festgelegt, bei dem das Parsen jedes Felds in try-catch eingeschlossen war, während jeder der catch-Blöcke leer war. Wenn jemand mit diesem Zustand zufrieden ist - eine Flagge in seinen Händen. Ich möchte einen besseren Weg anbieten.

Die meisten typisierten funktionalen Programmiersprachen bieten zwei Klassen für die Behandlung von Fehlern und Ausnahmen: Try und Entweder . Versuchen Sie, Ausnahmen zu behandeln, und entweder, um Geschäftslogikfehler zu behandeln.

In der Pfeilbibliothek können Sie diese Abstraktionen mit Kotlin verwenden. Daher können Sie die obige Anfrage wie folgt umschreiben:

 fun makeRequest(request: RequestBody): Try<List<ResponseData>> = Try { val response = httpClient.newCall(request).execute() if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { emptyList() } } 

Wie unterscheidet sich dieser Ansatz von der Verwendung von Try-Catch?

Zuallererst kann jeder, der diesen Code nach Ihnen liest (und höchstwahrscheinlich auch), bereits anhand der Signatur verstehen, dass die Ausführung des Codes zu einem Fehler führen kann - und einen Code für die Verarbeitung schreiben. Darüber hinaus wird der Compiler schwören, wenn dies nicht getan wird.

Zweitens gibt es Flexibilität bei der Behandlung des Fehlers.

In Try wird der Fehler oder Erfolg der Ausführung als Fehler- bzw. Erfolgsklasse dargestellt. Wenn die Funktion bei Fehlern immer etwas zurückgeben soll, können wir den Standardwert festlegen:

 makeRequest(request).getOrElse { emptyList() } 

Wenn eine komplexere Fehlerbehandlung erforderlich ist, hilft Fold:

 makeRequest(request).fold( {ex -> //  -       emptyList() }, { data -> /*    */ } ) 

Sie können die Wiederherstellungsfunktion verwenden. Der Inhalt wird vollständig ignoriert, wenn Try Success zurückgibt.

 makeRequest(request).recover { emptyList() } 

Sie können es für das Verständnis verwenden (von den Arrow-Erstellern von Scala ausgeliehen), wenn Sie das Erfolgsergebnis mithilfe einer Folge von Befehlen verarbeiten müssen, indem Sie die Factory .monad () unter Try aufrufen:

 Try.monad().binding { val r = httpclient.makeRequest(request) val data = r.recoverWith { Try.pure(emptyList()) }.bind() val result: MutableList<Data> = data.toMutableList() result.add(Data()) yields(result) } 

Die obige Option kann ohne Bindung geschrieben werden, wird dann aber anders gelesen:

 httpcilent.makeRequest(request) .recoverWith { Try.pure(emptyList()) } .flatMap { data -> val result: MutableList<Data> = data.toMutableList() result.add(Data()) Try.pure(result) } 

Am Ende kann das Ergebnis der Funktion verarbeitet werden mit:

 when(response) { is Try.Success -> response.data.toString() is Try.Failure -> response.exception.message } 

Mit Arrow können Sie also das alles andere als ideale Try-Catch-Konstrukt durch etwas Flexibles und sehr Praktisches ersetzen. Ein zusätzlicher Vorteil der Verwendung von Arrow besteht darin, dass Sie trotz der Tatsache, dass sich die Bibliothek als funktional positioniert, einzelne Abstraktionen von dort verwenden können (z. B. denselben Versuch), während Sie weiterhin guten alten OOP-Code schreiben. Aber ich warne Sie - es könnte Ihnen gefallen und Sie werden sich engagieren. In ein paar Wochen werden Sie anfangen, Haskell zu studieren, und Ihre Kollegen werden bald aufhören, Ihre Überlegungen zur Struktur des Codes zu verstehen.

PS: Es lohnt sich :)

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


All Articles