Java >> Java tutorial >  >> Java

Funktionel Java efter eksempel | Del 3 - Brug ikke undtagelser til at kontrollere flow

Dette er del 3 af serien kaldet "Functional Java by Example".

Eksemplet, jeg udvikler i hver del af serien, er en slags "feedhandler", som behandler dokumenter. I tidligere dele startede jeg med noget original kode og anvendte nogle refactorings for at beskrive "hvad" i stedet for "hvordan".

For at hjælpe koden fremad, er vi nødt til at slippe af med den gode gamle java.lang.Exception . (disclaimer:vi kan faktisk ikke slippe af med det) Det er her, denne del kommer ind.

Hvis du kom her for første gang, er det bedst at begynde at læse fra begyndelsen. Det hjælper med at forstå, hvor vi startede, og hvordan vi kom videre gennem serien.

Disse er alle delene:

  • Del 1 – Fra imperativ til deklarativ
  • Del 2 – Fortæl en historie
  • Del 3 – Brug ikke undtagelser til at kontrollere flowet
  • Del 4 – Foretrække uforanderlighed
  • Del 5 – Flyt I/O til ydersiden
  • Del 6 – Fungerer som parametre
  • Del 7 – Behandl også fejl som data
  • Del 8 – Flere rene funktioner

Jeg vil opdatere linkene, efterhånden som hver artikel udgives. Hvis du læser denne artikel gennem indholdssyndikering, så tjek venligst de originale artikler på min blog.

Hver gang bliver koden også skubbet til dette GitHub-projekt.

Kom godt i gang med undtagelser

Vores java.lang.Exception har eksisteret siden Java 1.0 – og har dybest set været vores ven i gode tider og nemesis på andre tidspunkter.

Der er ikke meget at tale om dem, men hvis du vil læse op på et par kilder, er her mine favoritter:

  • Undtagelser i Java (JavaWorld)
  • Undtagelser i Java – GeeksforGeeks (geeksforgeeks.org)
  • 9 bedste fremgangsmåder til håndtering af undtagelser i Java (stackify.com)
  • Bedste praksis for håndtering af undtagelser (onjava.com)
  • Spørgsmål og svar på Java Exception Interview (journaldev.com)
  • Undtagelseshåndtering i java med eksempler (beginnersbook.com)
  • Java-undtagelseshåndtering (Try-catch) (hackerrank.com)
  • Top 20 bedste praksis for Java-undtagelseshåndtering – HowToDoInJava (howtodoinjava.com)
  • Exception Handling &Assertion in Java – NTU (ntu.edu.sg)
  • Undtagelseshåndtering:En vejledning til bedste praksis (dzone.com)
  • 9 bedste fremgangsmåder til håndtering af undtagelser i Java (dzone.com)
  • Rettelse af 7 almindelige Java-undtagelseshåndteringsfejl (dzone.com)
  • Java-praksis -> Markerede kontra ikke-markerede undtagelser (javapractices.com)
  • Almindelige fejl med undtagelser i Java | Mikael Ståldals tekniske blog (staldal.nu)
  • 11 fejl, som Java-udviklere begår, når de bruger undtagelser (medium.com/@rafacdelnero)
  • Er markerede undtagelser gode eller dårlige? (JavaWorld)
  • Afkrydsede undtagelser:Javas største fejl | Læsere Java (literatejava.com)
  • Ukontrollerede undtagelser – The Controversy (docs.oracle.com)
  • Problemet med kontrollerede undtagelser (artima.com)
  • Undtagelser i Java:Du gør det (sandsynligvis) forkert (dzone.com)
  • Java teori og praksis:Debatten om undtagelser – IBM (ibm.com)
  • Javas kontrollerede undtagelser var en fejl (og her er, hvad jeg gerne vil gøre ved det (radio-weblogs.com)
  • Buggy Java Code:Top 10 mest almindelige fejl, som Java-udviklere begår | Toptal (toptal.com)

Er du allerede på Java 8? Livet blev så meget bedre! Jeg... Err...åh, vent.

  • Fejlhåndtering med Java-inputstrømme – Javamex (javamex.com)
  • Håndtering af kontrollerede undtagelser i Java-streams (oreilly.com)
  • Exceptionel undtagelseshåndtering i JDK 8-streams (azul.com)
  • Java 8 funktionelle grænseflader med undtagelser (slieb.org)
  • Ompakning af undtagelser i streams – blog@CodeFX (blog.codefx.org)
  • Hvordan håndterer man undtagelser i Java 8 Stream? – Stack Overflow (stackoverflow.com)
  • Markerede undtagelser og streams | Benjis blog (benjiweber.co.uk)
  • En historie om Checked Exceptions og Java 8 Lambda Expressions (javadevguy.wordpress.com) – god krigshistorie!
  • hgwood/java8-streams-and-exceptions (github.com)
  • ...

Ok, det ser ud til, at du ikke kan gøre det rigtigt .

I det mindste, efter at have læst ovenstående liste, er vi nu fuldstændig up-to-speed om emnet ��

Heldigvis behøver jeg ikke skrive et blogindlæg mere om, hvad der er dækket for 95 % allerede i ovenstående artikler, men jeg vil her fokusere på den ene Exception vi har faktisk i koden ��

Bivirkninger

Siden du læser dette indlæg, er du sikkert interesseret i, hvorfor det hele har at gøre med funktionel programmering .

På vejen til at nærme dig din kode på en mere "funktionel måde", er du muligvis stødt på udtrykket "bivirkning", og at det er en "dårlig ting".

I den virkelige verden er en bivirkning noget, du ikke havde til hensigt at ske , og du kan sige, at det svarer til en "ekstraordinær" situation (du ville angive med en undtagelse), men det har en mere streng betydning i en funktionel programmeringskontekst.

Wikipedia-artiklen om en bivirkning siger:

Bivirkning (datalogi) Inden for datalogi siges en funktion eller et udtryk at have en bivirkning, hvis det ændrer en tilstand uden for sit omfang eller har en observerbar interaktion med sine kaldende funktioner eller omverdenen udover at returnere en værdi. … I funktionel programmering bruges bivirkninger sjældent.

Så lad os se, hvordan vores FeedHandler-kode i øjeblikket ser ud efter de første to artikler i denne serie:

class FeedHandler {

  Webservice webservice
  DocumentDb documentDb

  void handle(List<Doc> changes) {

    changes
      .findAll { doc -> isImportant(doc) }
      .each { doc ->

      try {
        def resource = createResource(doc)
        updateToProcessed(doc, resource)
      } catch (e) {
        updateToFailed(doc, e)
      }
    }
  }

  private Resource createResource(doc) {
    webservice.create(doc)
  }

  private boolean isImportant(doc) {
    doc.type == 'important'
  }

  private void updateToProcessed(doc, resource) {
    doc.apiId = resource.id
    doc.status = 'processed'
    documentDb.update(doc)
  }

  private void updateToFailed(doc, e) {
    doc.status = 'failed'
    doc.error = e.message
    documentDb.update(doc)
  }

}

Der er ét sted, hvor vi prøvefanger undtagelser, og det er her, vi går gennem de vigtige dokumenter og prøv at skabe en "ressource" (hvad det end er) til det.

try {
  def resource = createResource(doc)
  updateToProcessed(doc, resource)
} catch (e) {
  updateToFailed(doc, e)
}

I koden over catch (e) er Groovy stenografi for catch (Exception e) .

Ja, det er den generiske java.lang.Exception som vi fanger. Kunne være enhver undtagelse, inklusive NPE.

Hvis der ikke er nogen undtagelse fra createResource metode, opdaterer vi dokumentet ("doc") til 'behandlet', ellers opdaterer vi det til 'mislykkedes'. BTW, endda updateToProcessed kan også give en undtagelse, men for den aktuelle diskussion er jeg faktisk kun interesseret i en vellykket ressourceoprettelse.

Så ovenstående kode virker (Jeg har enhedstestene til at bevise det :-)), men jeg er ikke tilfreds med try-catch erklæringen, som den er nu. Jeg er kun interesseret i vellykket ressourceoprettelse, og dumt mig, jeg kunne kun finde på createResource enten returnere en vellykket ressource eller kaster en undtagelse.

Kaster en undtagelse for at signalere, at noget gik galt, kom for helvede ud af at undvige, få opkalderen til at fange undtagelsen for at håndtere det, er hvorfor undtagelser blev opfundet ikke? Og det er bedre end at returnere null ret?

Det sker hele tiden. Tag nogle af vores foretrukne rammer, såsom EntityManager#find fra JPA-specifikationen:

Arg! Returnerer null .

Returneringer:
den fundne enhedsinstans eller null, hvis entiteten ikke eksisterer

Forkert eksempel.

Funktionel programmering tilskynder til bivirkningsfrie metoder (eller:funktioner), for at gøre koden mere forståelig og lettere at ræsonnere om. Hvis en metode bare accepterer bestemt input og returnerer det samme output hver gang – hvilket gør den til en ren funktion – alle former for optimeringer kan ske under emhætten f.eks. af compileren, eller caching, parallelisering osv.

Vi kan erstatte ren fungerer igen ved deres (beregnede) værdi, som kaldes referentiel transparens.

I tidligere artikel har vi allerede udtrukket noget logik i deres egne metoder, såsom isImportant under. Givet det samme dokument (med samme type ejendom) som input, får vi det samme (boolesk) output hver gang.

boolean isImportant(doc) {
  doc.type == 'important'
}

Her er der ingen observerbar bivirkning, ingen globale variabler muteres, ingen logfil opdateres – det er bare ting ind, ting ud .

Således vil jeg sige, at funktioner, der interagerer med omverdenen gennem vores traditionelle undtagelser, er sjældent bruges i funktionel programmering.

Jeg vil gerne gøre det bedre end det. Vær bedre.

Valgfrit til undsætning

Som Benji Weber udtrykker det:

Der er forskellige synspunkter på, hvordan man bruger undtagelser effektivt i Java. Nogle mennesker kan lide kontrollerede undtagelser, nogle hævder, at de er et mislykket eksperiment og foretrækker eksklusiv brug af ukontrollerede undtagelser. Andre undgår undtagelser helt til fordel for afleverings- og returtyper som Valgfri eller Måske.

Ok, lad os prøve Java 8s Optional så signaler om en ressource kan eller ikke kan oprettes.

Lad os ændre vores webservice-grænseflade og createResource metode til at pakke og returnere vores ressource i en Optional :

//private Resource createResource(doc) {
private Optional<Resource> createResource(doc) {
  webservice.create(doc)
}

Lad os ændre den originale try-catch :

try {
  def resource = createResource(doc)
  updateToProcessed(doc, resource)
} catch (e) {
  updateToFailed(doc, e)
}

til map (behandlingsressource) og orElseGet (behandler tom valgfri):

createResource(doc)
  .map { resource ->
    updateToProcessed(doc, resource)
  }
  .orElseGet { /* e -> */
    updateToFailed(doc, e)
  }

Fantastisk createResource metode:enten kommer korrekt resultat tilbage, eller et tomt resultat.

Vent et øjeblik! Undtagelsen e vi skal gå ind i updateToFailed er væk :vi har en tom Optional i stedet. Vi kan ikke gemme årsagen hvorfor det mislykkedes - hvilket vi har brug for.

Kan være en Optional signalerer bare "fravær" og er et forkert værktøj til vores formål her.

Enestående færdiggørelse

Uden try-catch og med map-orElseGet i stedet gør jeg ligesom den måde, hvorpå koden begyndte at afspejle "flowet" af operationer mere. Desværre bruger Optional var mere passende til "få noget" eller "få intet" (hvilke navne som map og orElseGet også foreslået) og gav os ikke mulighed for at registrere en årsag til at fejle.

Hvad er en anden måde at enten få det vellykkede resultat eller få årsagen til at fejle, stadig nærmer sig vores pæne måde at læse på?

En Future . Endnu bedre:en CompletableFuture .

En CompletableFuture (CF) ved, hvordan man returnerer en værdi, på denne måde ligner den en Optional . Normalt bruges en CF til at få en værdi sat i fremtiden , men det er ikke det, vi vil bruge det til...

Fra Javadoc:

En fremtid, der … understøtter … handlinger, der udløser ved dens afslutning.

Jip, det kan signalere "exceptionel" fuldførelse — giver mig mulighed for at handle på det.

Lad os ændre map og orElseGet :

createResource(doc)
  .map { resource ->
    updateToProcessed(doc, resource)
  }
  .orElseGet { /* e -> */
    updateToFailed(doc, e)
  }

til thenAccept (behandlingssucces) og exceptionally (behandlingsfejl):

createResource(doc)
  .thenAccept { resource ->
    updateToProcessed(doc, resource)
  }
  .exceptionally { e ->
    updateToFailed(doc, e)
  }

CompletableFuture#exceptionally metode accepterer en funktion med vores undtagelse e med den egentlige årsag til fiasko.

Du tænker måske:tomayto, tomahto. Først havde vi try-catch og nu har vi thenAccept-exceptionally , så hvad er den store forskel?

Nå, vi kan naturligvis ikke slippe af med de ekstraordinære situationer, men vi tænker nu, som en beboer i Functionalville ville:vores metoder begynder at blive funktioner , fortæller os, at noget går ind og noget går ud.

Betragt det som en lille refaktorering, vi har brug for til del 4, hvilket begrænser mængden af ​​bivirkninger i vores kode endnu mere, og del 5.

Dette er det for nu

Til reference, her er den fulde version af den refaktorerede kode.

class FeedHandler {

  Webservice webservice
  DocumentDb documentDb

  void handle(List<Doc> changes) {

    changes
      .findAll { doc -> isImportant(doc) }
      .each { doc ->
        createResource(doc)
        .thenAccept { resource ->
          updateToProcessed(doc, resource)
        }
        .exceptionally { e ->
          updateToFailed(doc, e)
        }
      }
  }

  private CompletableFuture<Resource> createResource(doc) {
    webservice.create(doc)
  }

  private boolean isImportant(doc) {
    doc.type == 'important'
  }

  private void updateToProcessed(doc, resource) {
    doc.apiId = resource.id
    doc.status = 'processed'
    documentDb.update(doc)
  }

  private void updateToFailed(doc, e) {
    doc.status = 'failed'
    doc.error = e.message
    documentDb.update(doc)
  }

}


Java tag