Refaktorer til funksjonell stil allerede nå!

Project Lambda vil gi språklig støtte for funksjonell programmering i Java 8. Dette er ventet å skje sommeren 2013, og det kan vel ikke sies å være for tidlig på noen måte. Det er likevel ingen grunn til å sitte på gjerdet og vente med å kode funksjonelt selv om man programmerer Java 5, 6, eller kanskje til og med Java 7 til daglig.

Jeg har lyst til å gi et konkret eksempel hvor jeg mener at funksjonelt tenkesett i objekt-orientert Java virkelig skinner, selv om man ikke har direkte språklig støtte for funksjonell programmering enda, og det innebærer mye boilerplate og seremoni.

Original kode – SecurityLevelVoter.java

Spring Security bruker en eller flere AccessDecisionVoters for å avgjøre om en bruker er autorisert til å aksessere en ressurs. Logikken for dette implementeres i vote-metoden, som skal returnere én av tre mulige utfall:

  • brukeren gis tilgang (ACCESS_GRANTED)
  • brukeren gis ikke tilgang (ACCESS_DENIED)
  • AccessDecisionVoteren vil ikke ta noen avgjørelse (ACCESS_ABSTAIN)

Dette er den originale koden:

Vi kjenner igjen de 3 mulige utfallene av autoriseringen, men det er ikke umiddelbart innlysende når de ulike utfallene er gjeldende. (Selvfølgelig finnes det en enhetstest som beskriver dette, men det hadde vært greit å lett kunne lese det ut av produksjonskoden også.) Variabelen result får tildelt en verdi to steder i metoden, og hvilken verdi den ender opp med til syvende og sist avhenger av om samlingen med CollectionAttributer er tom eller ikke, samt at minst én av de må være støttet av denne SecurityLevelVoteren. I tillegg har vi en litt “flau” instanceof-sjekk på principal-objektet for hver eneste iterasjon, selv om den åpenbart ikke kommer til å endre type iløpet av livsløpet sitt. Det er altså egentlig ingen vits i å iterere over ConfigAttributene dersom vi ikke har med en MyPrincipal å gjøre, men resultatet avhenger fremdeles av om samlingen med ConfigAttributer er tom eller ikke. Etter å ha sortert tankene vi gjør oss rundt logikken i denne implementasjonen kommer vi frem til følgende:

  • Dersom det ikke finnes ConfigAttributer som støttes av SecurityLevelVoter, returner ACCESS_ABSTAIN.
  • Dersom vi har en MyPrincipal og det finnes en støttet ConfigAttribute som godtar sikkerhetsnivået til MyPrincipal, returner ACCESS_GRANTED.
  • Ellers, returner ACCESS_DENIED.

Disse tre punktene er utgangspunktet for refaktoreringen til funksjonell stil i neste del av denne bloggposten.

Refaktorering

For å refaktorere benytter jeg biblioteket Larvalabs Collections, en ren port av den mer kjente Apache Commons Collections, men med støtte for Generics. Biblioteket tilbyr et lite antall én-metode-interfaces for å simulere funksjoner, samt en del statiske metoder i CollectionUtils for å benytte funksjoner på collections.

Funksjon: å støttes av en AccessDecisionVoter

For en funksjon med et boolsk resultat kan vi implementere et Predicate. Vi ser i det første punktet at vi trenger en implementasjon for å avgjøre om ConfigAttributer støttes av SecurityLevelVoter. Koden under definerer dette:

Med SupportedBy-predikatet og select-metoden fra Larvalabs Collections kan vi sjekke om det finnes noen ConfigAttributer som støttes av den inneværende SecurityLevelVoteren:

Funksjon: å godta sikkerhetsnivået til MyPrincipal

Det neste punktet tar for seg de støttede ConfigAttributene vi fant i forrige punkt, og sier at det må finnes, altså eksistere minst én som godtar sikkerhetsnivået til MyPrincipal. Vi trenger altså et nytt predikat som definerer hva det vil si å akseptere sikkerhetsnivået til en MyPrincipal:

Predikatet AcceptingSecurityLevelOf er implementert som en indre klasse i SecurityLevelVoter for å ha tilgang til securityLevelPrefix. Med dette predikatet og exists-metoden kan vi enkelt uttrykke hva som er forutsetningen for å gi brukeren tilgang:

I alle andre tilfeller nektes brukeren tilgang, som beskrevet i det siste punktet.

Resultat, vote-metoden

Fork me on GitHub

Den resulterende implementasjonen av vote(..) reflekterer bedre resonnementet bak de ulike forutsetningene for brukertilgang.

siste XP Meetup i Oslo gjorde Gojko Adzic et poeng ut av å forklare tester for andre, og deretter omformulere testen i henhold til hvordan man har forklart den. Det er egentlig det som har blitt gjort her, bortsett fra at det er produksjonskode som har blitt forklart og omformulert.

Den fullstendige koden finnes på Github: https://github.com/runeflobakk/fp-refactoring-example

Men tilbake til Project Lambda

Innledningsvis nevnte jeg at Java får støtte for funksjoner i versjon 8 som er ventet sommeren 2013. Som følge av det vil for eksempel Collection Framework sitt API bli utvidet for å støtte manipulering ved hjelp av funksjoner fremfor eksplisitt iterering, og andre biblioteker vil nok garantert følge etter. Ved å ta ibruk et bibliotek som beskrevet i denne bloggposten kan man allerede nå forberede seg på denne måten å tenke implementasjon på. I tillegg vil interfaces som Predicate få en spesiell rolle i Java 8 sitt typesystem, som såkalte SAMs (Single Abstract Methods). En SAM-type er som navnet tilsier en type med kun én abstract metode, og i Java 8 vil lambda-uttrykk automatisk bli oversatt til implementasjoner av SAM-typer. Dette vil si at dersom man lager metoder med SAM-typer som parametre, vil denne koden umiddelbart kunne håndtere lambda-uttrykk i Java 8.

For eksempel kan man tenke seg med Java 8 at man sletter hele SupportedBy-predikatet, og endrer koden som sjekker på tilstedeværelse av støttede ConfigAttributer til følgende:

Her vil attr -> this.supports(attr) bli oversatt til en implementasjon av Predicate, ettersom select(..) tar det som parameter. Mest sannsynlig vil man nok i dette tilfellet bruke en ny metode i Collection Framework for å velge ut elementer, men dette viser at metoder man selv skriver og som tar denne typen interfaces som parametre vil fungere veldig bra sammen med lambda-uttrykk i Java 8, når man omsider får mulighet til å oppgraderer til det.

  • http://twitter.com/eivindw Eivind B Waaler

    Veldig interessant innlegg!

    Jeg er litt forvirret over hvilke rammeverk man burde bruke når det gjelder funksjonell stil med Java, og hvor det evt. er overlapp. Hamcrest, LambdaJ, Larvalabs, Guava er forskjellige ting jeg har hørt om. Har du gjort deg opp noen mening om hva man bør bruke og ikke av disse? Er Guava like tilpasset funksjonell stil som det du viser her for eksempel?

  • http://twitter.com/rflob Rune Flobakk

    Hamcrest er i bunn og grunn et interface, Matcher, som egentlig er det
    samme som Predicate omtalt i bloggposten, og en stor samling nyttige
    implementasjoner av dette. Biblioteket er stort sett forbunnet med
    testing og er det som brukes i assertThat(something, is(whatYouExpect))
    statements, hvor da is(..) er en statisk metode som returnerer en
    Matcher-implementasjon som avgjør om something kan passere som
    whatYouExpect. Det er ingen grunn til å ikke bruke Hamcrest i
    produksjonskode, men Hamcrest selv har ingen metoder for å “apply’e”
    matchere på collections. I tillegg vil ikke metoder som forventer en
    Hamcrest matcher som argument være kompatibel med Project Lambda sin
    lambda-syntax, ettersom Matcher-interfacet inneholder 2 metoder, en som
    gir true/false tilsvarende Predicate, og en som gir en tekstlig
    beskrivelse av hva matcheren “matcher”. Denne brukes til å gi fine
    feilmeldinger for hva som var forventet i.f.m. assertion failures.

    LambdaJ har jeg brukt på prosjekt tidligere, først sneket inn via test
    og senere forsiktig inn i frontend-Javakode ;) Funker som bare det, men
    det har en del begrensninger fordi det baserer seg på dynamiske
    proxyer/subklasser. Den gir deg mulighet til å skrive ganske
    uttrykksfulle funksjonelle one-liners uten å måtte gå veien om å
    implementere interfaces/abstract classes. F.eks. kan man få en del
    overraskelser hvis man bruker det sammen med java.lang.String ettersom
    den klassen er final (kan ikke subklasses). LambdaJ bruker samme teknikk
    som Mockito for å statisk “referere” til metoder, uten å invokere de.
    Har laget et miniprosjekt her som kun har til hensikt å demonstrere
    denne teknikken https://github.com/runeflobakk/method-ref

    Guava er et mer general purpose bibliotek, og det favner vel typisk
    slike ting man også finner i Apache Commons Lang, Larvalabs Collections
    og sikkert andre ting. Guava har nok et mer moderne API enn de
    betraktelig eldre Apache-bibliotekene. Guava har de tilsvarende
    funksjonelle interfacene man finner i Larvalabs, men jeg har ikke funnet
    metoder tilsvarende de som er CollectionUtils i Larvalabs for å spørre og manipulere collections med predikater og andre typer funksjoner. Det burde derimot ikke være store problemet å skrive disse selv hvis man vil bruke Guava.

  • http://www.tfnico.com Thomas Ferris Nicolaisen

    Hvis du vil bruke Guava:
    org.apache.commons.collections15.CollectionUtils.select => com.google.common.collect.Iterables.filter

    org.apache.commons.collections15.CollectionUtils.exists
    => com.google.common.collect.Iterables.indexOf

    Signaturene er så godt som helt like, bortsett fra at indexOf returnerer en int (-1 hvis det ikke finnes).

    Jeg drar som regel alltid med meg inn Guava når jeg jobber i et prosjekt. Er noen kloke hoder som har utviklet det, og tenker på ting som performance og concurrency, også releaser de temmelig regelmessig.

    Hvis du vil jobbe ekstremt funksjonelt, kan du også kikke på functionaljava.org.

    Edit: Her har jeg forøvrig en samling med Guava ressurser: http://www.tfnico.com/presentations/google-guava

    • http://twitter.com/rflob Rune Flobakk

      Takk for Guava-tips! Skal nok vurdere å ta det i bruk neste gang jeg begynner på scratch.

      For øvrig, det siste jeg har sett av funksjonell “magi” for Java er dette: https://github.com/hraberg/enumerable

  • http://openid.anonymity.com/jhjhg ashfd

    velkommen etter.