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 avSecurityLevelVoter, returnerACCESS_ABSTAIN. - Dersom vi har en
MyPrincipalog det finnes en støttetConfigAttributesom godtar sikkerhetsnivået tilMyPrincipal, returnerACCESS_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
Den resulterende implementasjonen av vote(..) reflekterer bedre resonnementet bak de ulike forutsetningene for brukertilgang.
På 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.