Et av konseptene vi har hatt stor suksess med i Tuts + -laget er serviceobjekter. Vi har brukt tjenesteobjekter for å redusere koblingen i våre systemer, gjøre dem mer testbare og gjøre viktig forretningslogikk mer åpenbar for alle utviklerne på teamet.
Så da vi bestemte oss for å kodifisere noen av begrepene vi har brukt i utviklingen av Rails til en Ruby-perle (kalt Aldous), var tjenesteobjekter på toppen av listen.
Hva jeg vil gjerne gjøre i dag, er å gi en rask oversikt over serviceobjekter som vi har implementert dem i Aldous. Forhåpentligvis vil dette fortelle deg de fleste av tingene du trenger å vite for å kunne bruke Aldous serviceobjekter i dine egne prosjekter.
Et serviceobjekt er i utgangspunktet en metode som er pakket inn i en gjenstand. Noen ganger kan et tjenesteobjekt inneholde flere metoder, men den enkleste versjonen er bare en klasse med en metode, for eksempel:
klasse DoSomething def utføre # gjøre ting slutten
Vi er alle vant til å bruke substantiver for å nevne våre objekter, men noen ganger kan det være vanskelig å finne et godt substantiv for å representere et konsept, mens det å snakke om det i form av en handling (eller verb) er enkelt og naturlig. En tjenesteobjekt er hva vi får når vi går med strømmen og bare setter verbet inn i en gjenstand.
Selvfølgelig, gitt den ovennevnte definisjonen, kan vi omdanne en handling / metode til et serviceobjekt hvis vi ønsker det. Følgende…
klasse Kunde def createPurchase (rekkefølge) # gjøre ting slutten
... kunne bli omgjort til:
klasse CreateCustomerPurchase def initialiser (kunde, rekkefølge) slutten def utføre # gjøre ting slutten
Vi kan skrive flere andre innlegg om effekten av serviceobjekter kan ha på utformingen av systemet ditt, de forskjellige avgangene du skal lage, etc. For nå skal vi bare være oppmerksom på dem som et konsept og betrakte dem bare et annet verktøy Vi har i vårt arsenal.
Som Rails-apps blir større, har våre modeller en tendens til å bli ganske store, og vi ser etter måter å skape litt funksjonalitet ut av dem i "hjelper" -objekter. Men dette er ofte lettere sagt enn gjort. Rails har ikke et konsept, i modelllaget, som er mer granulært enn en modell. Så du ender med å gjøre mange domssamtaler:
lib
mappe?Nå må du kommunisere hva du har gjort med de andre utviklerne på teamet ditt, og til nye personer som blir med senere. Og selvfølgelig, som står overfor en lignende situasjon, kan andre utviklere gjøre forskjellige domssamtaler, noe som fører til inkonsekvenser som kryper inn.
Serviceobjekter gir oss et konsept som er mer granulært enn en modell. Vi kan ha en konsekvent plassering for alle våre tjenester, og du beveger deg bare en metode til en tjeneste. Du nevner denne klassen etter handlingen / metoden som den representerer. Vi kan trekke funksjonalitet ut i mer kornformede gjenstander uten for mange dommesamtaler, som holder hele laget på samme side, slik at vi kan fortsette med å bygge en god applikasjon.
Ved hjelp av serviceobjekter reduseres koblingen mellom modellene dine, og de resulterende tjenestene er svært gjenbrukbare på grunn av deres små størrelse / lyse fotavtrykk.
Serviceobjekter er også svært testbare, da de vanligvis ikke vil kreve så mye testkjele som flere tungvektige objekter, og du bekymrer deg bare om å teste den ene metoden som objektet inneholder.
Både serviceobjektene og deres tester er enkle å lese / forstå, da de er svært sammenhengende (også en bivirkning av deres lille størrelse). Du kan også kaste om og omskrive begge tjenesteobjektene og deres tester nesten i vilje, da kostnadene ved å gjøre det er relativt lave, og det er veldig enkelt å vedlikeholde grensesnittet.
Serviceobjekter har definitivt mye å gjøre for dem, spesielt når du introduserer dem i Apples Rails.
Gitt at serviceobjekter er så enkle, hvorfor trenger vi til og med en perle? Hvorfor ikke bare lage POROer, og du trenger ikke å bekymre deg for en annen avhengighet?
Du kan definitivt gjøre det, og faktisk gjorde vi det ganske lenge i Tuts +, men gjennom omfattende bruk endte vi med å utvikle noen få mønstre for tjenester som gjorde livet enklere, og det er akkurat det vi har presset inn i Aldous. Disse mønstrene er lette og involverer ikke mye magi. De gjør livet enklere, men vi beholder all kontroll hvis vi trenger det.
Første ting først, hvor skal tjenestene dine leve? Vi pleier å sette dem inn app / tjenester
, så du trenger følgende i din app / config / application.rb
:
config.autoload_paths + =% W (# config.root / app / services) config.eager_load_paths + =% W (# config.root / app / tjenester)
Som jeg har nevnt ovenfor, pleier vi å navngi serviceobjekter etter handlinger / verb (f.eks. Opprett bruker
, RefundPurchase
), men vi har også en tendens til å legge til "service" til alle klassenavnene (f.eks. CreateUserService
, RefundPurchaseService
). På denne måten, uansett hvilken kontekst du er i (ser på filene på filsystemet, ser på en tjenesteklasse hvor som helst i kodebase), vet du alltid at du har å gjøre med en tjenesteobjekt.
Dette håndheves ikke av perlen på noen måte, men verdt å ta hensyn til som en lærdom.
Når vi sier uforanderlige, mener vi at etter at objektet er initialisert, vil dets interne tilstand ikke lenger endres. Dette er veldig bra siden det gjør det mye enklere å begrunne om tilstanden til hvert objekt, så vel som systemet som helhet.
For at ovenstående kan være sant, kan serviceobjektmetoden ikke endre tilstanden til objektet, så data må returneres som en utgang fra metoden. Dette er vanskelig å håndheve direkte, siden et objekt alltid vil ha tilgang til sin egen interne tilstand. Med Aldous prøver vi å håndheve det via konvensjon og utdanning, og de neste to seksjonene vil vise deg hvordan.
Et Aldous serviceobjekt må alltid returnere en av to typer objekter:
Aldous :: service :: Resultat :: Suksess
Aldous :: service :: Resultat :: Failure
Her er et eksempel:
klasse CreateUserService < Aldous::Service def perform user = User.new(user_data_hash) if user.save Result::Success.new else Result::Failure.new end end end
Fordi vi arver fra Aldous :: service
, Vi kan konstruere våre returobjekter som Resultat :: Suksess
. Ved å bruke disse objektene som returverdier, kan vi gjøre ting som:
hash = result = CreateUserService.perform (hash) hvis result.success? # gjør suksess ting ellers # result.failure? # gjør feil ting slutten
Vi kunne, i teorien, bare returnere sann eller falsk og få samme oppførsel som vi har over, men hvis vi gjorde det, kunne vi ikke bære noen ekstra data med vår returverdi, og vi vil ofte ha data.
Suksessen eller feilen i en operasjon / tjeneste er bare en del av historien. Ofte vil vi ha opprettet et objekt som vi ønsker å returnere, eller produsert noen feil som vi ønsker å varsle på kallekoden. Dette er grunnen til at gjenværende objekter, som vi har vist ovenfor, er nyttige. Disse objektene brukes ikke bare til å indikere suksess eller feil, de er også dataoverføringsobjekter.
Aldous lar deg overstyre en metode i basetjenesteklassen for å angi et sett med standardverdier som objekter som returneres fra tjenesten, vil inneholde, for eksempel:
klasse CreateUserService < Aldous::Service attr_reader :user_data_hash def initialize(user_data_hash) @user_data_hash = user_data_hash end def default_result_data user: nil end def perform user = User.new(user_data_hash) if user.save Result::Success.new(user: user) else Result::Failure.new end end end
Hastene som er inneholdt i default_result_data
vil automatisk bli metoder på Resultat :: Suksess
og Resultat :: Failure
gjenstander returnert av tjenesten. Og hvis du oppgir en annen verdi for en av nøklene i den metoden, vil den overstyre standarden. Så i tilfelle av den ovennevnte klassen:
hash = result = CreateUserService.perform (hash) hvis result.success? result.user # vil være en forekomst av brukerresultat.blah # ville øke en feil ellers # result.failure? result.user # vil være nul result.blah # ville øke en feil slutt
I virkeligheten har ish nøklene i default_result_data
Metoden er en kontrakt for brukerne av serviceobjektet. Vi garanterer at du vil kunne ringe noen nøkkel i den hash som en metode på et resultatobjekt som kommer ut av tjenesten.
Når vi snakker om feilfrie APIer, mener vi metoder som aldri gir feil, men returnerer alltid en verdi for å indikere suksess eller fiasko. Jeg har skrevet om feilfrie APIer før. Aldous-tjenester er feilfrie, avhengig av hvordan du ringer dem. I eksemplet ovenfor:
result = CreateUserService.perform (hash)
Dette vil aldri oppstå en feil. Intern Aldous bryter din utførelsesmetode i a redde
blokkere og hvis koden din gir en feil, returnerer den en Resultat :: Failure
med default_result_data
som data.
Dette er ganske frigjørende, fordi du ikke lenger trenger å tenke på hva som kan gå galt med koden du har skrevet. Du er bare interessert i å lykkes eller feiler din tjeneste, og eventuelle feil vil resultere i en feil.
Dette er bra for de fleste situasjoner. Men noen ganger vil du ha en feil generert. Det beste eksempelet på dette er når du bruker et tjenesteobjekt i en bakgrunnsarbeider, og en feil vil føre til at bakgrunnsarbeideren forsøker å prøve igjen. Det er derfor en Aldous-tjeneste får også magisk en utføre!
metode og lar deg overstyre en annen metode fra baseklassen. Her er vårt eksempel igjen:
klasse CreateUserService < Aldous::Service attr_reader :user_data_hash def initialize(user_data_hash) @user_data_hash = user_data_hash end def raisable_error MyApplication::Errors::UserError end def default_result_data user: nil end def perform user = User.new(user_data_hash) if user.save Result::Success.new(user: user) else Result::Failure.new end end end
Som du kan se, har vi nå overstyrt raisable_error
metode. Vi ønsker noen ganger en feil å bli produsert, men vi vil heller ikke at det skal være noen feil. Ellers må vår ringerkode bli oppmerksom på enhver mulig feil som tjenesten kan produsere, eller bli tvunget til å fange en av grunnfeilstyper. Det er derfor når du bruker utføre!
Metode, Aldous vil fortsatt fange alle feilene for deg, men vil da heve opp igjen raisable_error
du har angitt og angitt den opprinnelige feilen som årsak. Du kan nå ha dette:
hash = start service = CreateUserService.build (hash) result = service.perform! redningstjeneste.raisable_error => e # feilfeil slutten
Du har kanskje lagt merke til bruk av fabrikkmetoden:
CreateUserService.build (hash) CreateUserService.perform (hash)
Du bør alltid bruke disse, og aldri konstruere serviceobjekter direkte. Fabrikkens metoder er det som gjør at vi kan koble rent i de fine funksjonene som automatisk redning og legge til default_result_data
.
Men når det gjelder tester, ønsker du ikke å bekymre deg for hvordan Aldous øker funksjonaliteten til tjenesteobjektene dine. Så, når du tester, skal du bare konstruere objekter direkte ved hjelp av konstruktøren og deretter teste funksjonaliteten din. Du får spesifikasjoner for logikken du skrev og stoler på at Aldous vil gjøre hva den skal gjøre (Aldous har egne tester for dette) når det gjelder produksjon.
Forhåpentligvis har dette gitt deg en ide om hvordan serviceobjekter (og spesielt Aldous serviceobjekter) kan være et fint verktøy i arsenalet ditt når du arbeider med Ruby / Rails. Gi Aldous et forsøk og gi oss beskjed om hva du synes. Du er også velkommen til å se på Aldous-koden. Vi skrev ikke bare det for å være nyttig, men også å være lesbart og lett å forstå / endre.