denne artikkelen ble opprinnelig publisert På pat Shaughnessys blogg. Pat ber ga tillatelse til å la oss dele det her For Kodeskip lesere.
timelister jeg brukte på min første programmingjob Sommeren 1986 så akkurat slik ut.
Nylig har jeg brukt litt av fritiden min på å studere Clojure og Haskell. Jeg har lært hvordan et program bygget med en rekke små, rene funksjoner kan være svært robust og vedlikeholdsbar.
jeg vil Imidlertid Ikke gi Opp Ruby. Jeg vil beholde Uttrykksevnen, skjønnheten og lesbarheten Til Ruby, mens du skriver enkle funksjoner uten bivirkninger.
men hvordan kan dette være mulig? I motsetning til funksjonelle språk, Oppfordrer Ruby deg til å skjule tilstand inne i objekter og å skrive funksjoner (metoder) som har bivirkninger, for eksempel å endre en forekomstvariabel. Bruker ikke et objektorientert språk som Ruby, Python eller Java en beslutning om å forlate fordelene med funksjonell programmering?
Nei. Faktisk, Et par uker siden Ruby objekt modell hjalp meg refactor en forvirrende funksjon i en serie av små enkle seg. I dag skal jeg vise deg hva som skjedde: hvordan bruk Av En Ruby-klasse hjalp meg med å skrive mer funksjonell kode.
Parsing Timeregistreringsdata
la oss anta at Du Er En ScrumMaster™ Og vil sørge for at teamet ditt av utviklere, inkludert meg, legger inn nok timer på prosjektet ditt (i stedet for å ta lange lunsjer eller skrive blogginnlegg). For eksempel, anta at jeg rapporterer mine timer som dette:
Du kan analysere mine timeregistreringsdata ved hjelp av dette enkle Ruby-programmet:
Dette er enkelt nok til å forstå og fungerer fint. parse1 er liten funksjon; hvis du fjerner anropene til setter den bare inneholder tre linjer med kode, to enkle samtaler for å dele. Hvordan kan dette være enklere?
Et Første Pass På En Funksjonell Løsning
deretter bestemmer Du deg for å se etter en mer funksjonell løsning ved å spørre Ruby om hva Du vil, i stedet for å fortelle Det hva du skal gjøre. Du prøver å bryte problemet opp i små funksjoner som returnerer det du trenger. Men hvilke funksjoner skal du skrive? Hvilke verdier skal de returnere? I dette enkle eksemplet er svaret åpenbart: Du kan skrive en funksjon for å analysere hver verdi i timeregistreringsdataene.
du har delt problemet opp i små biter. Hver funksjon vil returnere en forutsigbar verdi basert på noen innspill og har ingen bivirkninger. Disse vil være rene funksjoner: De vil alltid returnere det samme resultatet gitt de samme argumentene. Du vet at hvis du passerer en linje fra min timeregistrering, vil last_name alltid returnere «Shaughnessy.»Du har slått problemet rundt; du har formulert problemet som en rekke spørsmål i stedet for som en liste over instruksjoner.
Refactoring parse1 ovenfor implementerer du funksjonene, i hvert fall på en noe verbose og stygg måte:
Testing Av Rene Funksjoner
Som En Sertifisert ScrumMaster™, tror DU PÅ TDD og andre ekstreme programmeringspraksiser. Opprinnelig, mens du skrev parse1 ovenfor, skjedde det ikke engang for deg å skrive tester (og hvis det hadde, ville det vært veldig vanskelig). Men nå etter å ha brutt problemet opp i en rekke funksjoner, virker det naturlig å skrive tester for dem.
deretter uttrykker du dine forventninger til disse funksjonene Ved Hjelp Av Minitest-spesifikasjoner, for eksempel:
fordi funksjonene er små, er testene små. Fordi testene er små, du faktisk ta deg tid til å skrive dem. Fordi funksjonene er koblet fra hverandre, er det enkelt for deg å bestemme hvilke tester du skal skrive.
til din overraskelse finner du faktisk en feil!
Tidligere i parse1 ble den ekstra plassen tapt i puts-utgangen, og du la ikke merke til det. Å skille dette til en liten funksjon og nøye teste det viste et mindre problem. Du justerer to av funksjonene for å fjerne ekstra plass:
Pushing Ruby Ut Av Sin Komfortsone
du er fornøyd med dine nye tester. Ruby tillot deg å beskrive oppførselen til funksjonene på en veldig naturlig, lesbar måte. Ruby på sitt beste. Som en ekstra bonus, tester nå også passere!
funksjonene dine er imidlertid ikke så vakre. Det er mye åpenbar duplisering: kontoret, employee_id og last_name fungerer alle anropslinjer.Split(‘,’). For å fikse dette, bestemmer du deg for å trekke ut linje.split (‘,’) i en egen funksjon, fjerne duplisering:
Dette ser ikke noe bedre ut; faktisk er det et dypere problem her. For å se hva jeg mener, la oss refactor parse1 fra tidligere for å bruke våre nye funksjoner:
Dette er rent og enkelt å følge, Men nå har du en ytelsesfeil: hver gang rundt løkken, passerer koden din samme linje til employee_id, office og last_name. Nå Ruby vil kalle verdiene funksjon igjen og igjen. Dette er unødvendig og unødvendig; faktisk hadde vår opprinnelige parse1-kode ikke dette problemet. Ved å introdusere funksjoner har vi redusert koden vår.
men fordi disse er enkle, rene funksjoner, vet du at de alltid vil returnere samme verdi gitt samme inngangsargument, samme tekstlinje i dette eksemplet. Dette betyr teoretisk at du kan unngå å ringe split om og om igjen ved å caching resultatene.
først prøver du å cache returverdien for split ved å bruke et hashtabell som dette:
dette ser greit ut: tastene i split_lines er linjene, og verdiene er de tilsvarende delte linjene. Du bruker Ruby elegante / / = operatør enten å returnere en bufret verdi fra hash eller faktisk ringe split, oppdatere hash.
det eneste problemet med dette er at det ikke fungerer. Koden inne i verdier-funksjonen kan ikke få tilgang til split_lines hash, som ligger utenfor metoden. Og hvis du flytter split_lines inne i verdier, vil det bli en lokal variabel og ikke beholde verdier på tvers av metodekall.
for å omgå dette problemet kan du passere cachen som et ekstra argument til verdier, men dette vil gjøre programmet enda mer verbose enn det er nå. Eller du kan opprette verdimetoden ved hjelp av define_method i stedet for def, slik:
denne forvirrende Ruby-syntaksen tillater koden inne i den nye verdimetoden å få tilgang til det omkringliggende omfanget, inkludert hash-tabellen.
men å ta et skritt tilbake, føles noe om programmet ditt nå feil.
i Stedet for å gjøre koden enklere og lettere å forstå, har funksjonell programmering begynt å gjøre Ruby-koden mer forvirrende og vanskeligere å lese. Du har innført en ny datastruktur å cache resultater og tydd til forvirrende metaprogramming å gjøre det arbeidet. Og funksjonene dine er fortsatt ganske repeterende.
hva har gått galt? Muligens Ruby er ikke riktig språk å bruke med funksjonell programmering.
Vi Presenterer En Ruby-Klasse
deretter bestemmer du deg for å glemme alt om funksjonell programmering og prøve igjen ved å bruke En Ruby-klasse. Du skriver En Linje klasse, som representerer en enkelt linje med tekst fra timeregistreringen tekstfil:
og du bestemmer deg for å flytte funksjonene dine inn i den nye Linjeklassen:
nå har du mye mindre støy. Den største forbedringen er at nå er det ikke nødvendig å sende tekstlinjen rundt som en parameter til hver funksjon. I stedet gjemmer du det bort i en forekomstvariabel, noe som gjør koden mye enklere å lese. Også dine funksjoner har blitt metoder. Nå vet du at alle funksjonene knyttet til parsing linjer er I Linjeklassen. Du vet hvor du finner dem og mer eller mindre hva de er for. Ruby har hjulpet deg med å organisere koden din ved hjelp av en klasse, som egentlig bare er en samling funksjoner.
Fortsetter å forenkle, du refactor verdien metoden nederst for å fjerne forvirrende define_method syntaks:
nå vil hver forekomst Av Linjeklassen, hver tekstlinje du bruker, ha sin egen kopi av @ values. Ved Å bruke En Ruby-klasse trenger du ikke å ty til et hash-bord for å kartlegge mellom linjer (nøkler) og splitte linjer (verdier). I stedet bruker du et veldig vanlig Rubin idiom, som kombinerer en instansvariabel @ verdier, med / / = operatøren. Instansvariabler er det perfekte stedet å cache informasjon som metode returverdier.
Bryte Alle Reglene
nå er koden din mye lettere å lese.
med din objektorienterte løsning har du brutt noen av de viktigste reglene for funksjonell programmering: Først opprettet du skjult tilstand, @line instance-variabelen, pakket den opp og gjemte den inne I Linjeklassen. Instansvariabelen @ verdier inneholder enda mer tilstandsinformasjon. Og for det andre, initialiser og verdier metoder har bivirkninger: de endrer verdien av @ line og @ verdier. Til slutt er alle De andre Metodene For Linje ikke lenger rene funksjoner! De returnerer verdier som avhenger av tilstanden som ligger utenfor hver funksjon: @line-variabelen. Faktisk kan de returnere forskjellige verdier selv om de ikke tar noen argumenter i det hele tatt.
Men jeg tror disse er tekniske. Du har ikke mistet fordelene med funksjonell programmering med denne refactoring. Mens Metodene For Linje avhenger av ekstern tilstand (@linje og @verdier), ligger ikke denne tilstanden veldig langt unna. Det er fortsatt lett å forutsi, forstå og teste hva disse små funksjonene gjør. Også, mens @line er teknisk en foranderlig streng som du endrer i programmet ditt, er det i praksis ikke. Mens du kan oppdatere @ verdier hver gang verdier kalles, er det bare en ytelsesoptimalisering. Det endrer ikke den generelle oppførselen til verdier.
Du har brutt reglene og omskrevet ditt rene, funksjonelle program på en mer idiomatisk, Rubin måte. Du har imidlertid ikke mistet ånden i funksjonell programmering. Koden din er like lett å forstå, vedlikeholde og teste.
Opprette Et Objekt Pipeline
Innpakning opp, du refactor det opprinnelige programmet til å bruke den nye Linjen klassen som dette:
Selvfølgelig er det ikke mye forskjell her. Du bare lagt til en linje med kode for å opprette nye linjeobjekter og deretter kalt sine metoder i stedet for de opprinnelige funksjonene.
Til slutt bestemmer du deg for å ta et skritt videre og refactor igjen ved å kartlegge rekke tekstlinjer til en rekke linjeobjekter:
Igjen, ikke mye forskjell i koden. Men måten du tenker på programmet har endret seg dramatisk. Nå implementerer koden din en rørledning av former, og sender data gjennom en rekke operasjoner eller transformasjoner. Du starter med en rekke tekstlinjer fra en fil, konvertere dem til En rekke Ruby objekter, og til slutt behandle hvert objekt ved hjelp av parse funksjoner.
dette mønsteret for å sende data gjennom en rekke operasjoner er vanlig i språk som Haskell og Clojure.
det som er interessant her er Hvordan Ruby-objekter er det perfekte målet for disse operasjonene. Du har brukt En Ruby-klasse til å implementere et funksjonelt programmeringsmønster.
Oppdatering: Oren Dobzinski foreslo å legge til en to_s-metode på Linje, noe som ville tillate oss å presse object pipeline-ideen enda lenger. Takk Oren! Se Dave Thomas artikkel «Telling, Spør, og Kraften I Sjargong» for mer bakgrunn på «Fortell, Ikke Spør».