Unul dintre conceptele pe care le-am avut cu succes în echipa Tuts + sunt obiectele de serviciu. Am folosit obiecte de serviciu pentru a reduce cuplarea în sistemele noastre, pentru a le face mai testabile și pentru a face logica de afaceri importantă mai evidentă pentru toți dezvoltatorii din echipă.
Așa că atunci când am decis să codificăm câteva dintre conceptele pe care le-am folosit în dezvoltarea noastră Rails într-o bijuterie Ruby (numită Aldous), obiectele de serviciu se aflau în topul listei.
Ceea ce aș vrea să fac astăzi este să dau o scurtă trecere în revistă a obiectelor de serviciu după cum le-am implementat în Aldous. Sperăm că acest lucru vă va spune cele mai multe lucruri pe care trebuie să le cunoașteți pentru a utiliza obiecte de serviciu Aldous în proiectele proprii.
Un obiect de serviciu este în esență o metodă care este înfășurată într-un obiect. Uneori un obiect de serviciu poate conține mai multe metode, dar cea mai simplă versiune este doar o clasă cu o metodă, de ex .:
clasa DoSomething def îndeplini # face chestii sfârșitul final
Cu toții suntem obișnuiți să folosim substantive pentru a ne numi obiectele, dar uneori poate fi greu să găsești un substantiv bun pentru a reprezenta un concept, în timp ce vorbește despre el în termeni de acțiune (sau verb) este simplu și natural. Un obiect de serviciu este ceea ce primim atunci când mergem cu fluxul și transformăm verbul într-un obiect.
Desigur, având în vedere definiția de mai sus, putem transforma orice acțiune / metodă într-un obiect de serviciu dacă dorim acest lucru. Următoarele…
client de clasă createPurchase (comanda) # do stuff end end
... ar putea fi transformate în:
clasă CreateCustomerPurchase def initialize (client, comanda) end def execute # do stuff end end
Am putea scrie mai multe postări despre obiectele de serviciu pe care le-ar putea avea asupra designului sistemului dvs., despre diferitele compromisuri pe care le veți realiza etc. Pentru moment, să le conștientizăm ca un concept și să le considerăm doar un alt instrument avem în arsenalul nostru.
Pe măsură ce aplicațiile Rails cresc, modelele noastre tind să devină destul de mari și astfel căutăm căi de a împinge unele funcționalități din ele în obiecte "ajutoare". Dar acest lucru este adesea mai ușor de zis decât de făcut. Rails nu are un concept, în stratul model, care este mai granular decât un model. Deci, în final, trebuie să faceți o mulțime de solicitări de judecată:
lib
pliant?Acum trebuie să comunicați ceea ce ați făcut celorlalți dezvoltatori din echipa dvs. și oricărui nou utilizator care se alătură ulterior. Și, desigur, confruntându-se cu o situație similară, alți dezvoltatori ar putea face apeluri diferite la judecată, ceea ce ar duce la inconsecvențe.
Obiectele de serviciu ne dau un concept mai granular decât un model. Putem avea o locație consecventă pentru toate serviciile noastre și numai tu poți muta o metodă într-un serviciu. Denumiți această clasă după acțiunea / metoda pe care o va reprezenta. Putem extrage funcționalitatea în obiecte mai granulare fără prea multe apeluri de judecată, care păstrează întreaga echipă pe aceeași pagină, permițându-ne să continuăm cu afacerea de a construi o aplicație excelentă.
Utilizarea obiectelor de serviciu reduce cuplajul dintre modelele Rails și serviciile rezultate sunt foarte reutilizabile datorită dimensiunilor reduse / amprentei ușoare.
Obiectele de serviciu sunt, de asemenea, extrem de testabile, deoarece de obicei nu vor necesita atât boilerplate de testare ca și obiecte mai grele, și vă faceți griji numai cu privire la testarea uneia dintre metodele pe care le conține obiectul.
Ambele obiecte de serviciu și testele lor sunt ușor de citit / înțeles deoarece sunt foarte coezive (de asemenea, un efect secundar al mărimii lor mici). De asemenea, puteți să eliminați și să rescrieți atât obiectele de serviciu, cât și testele lor aproape la nevoie, deoarece costul de a face acest lucru este relativ scăzut și este foarte ușor să vă mențineți interfața.
Obiectele de serviciu au cu siguranta multe de facut pentru ei, mai ales cand le introduceti in aplicatiile Rails.
Având în vedere că obiectele de serviciu sunt atât de simple, de ce avem nevoie chiar de o bijuterie? De ce nu creați doar PORO-uri, și atunci nu trebuie să vă faceți griji cu privire la o altă dependență?
S-ar putea să o faceți cu siguranță și, de fapt, am făcut acest lucru pentru o perioadă lungă de timp în Tuts +, dar prin utilizarea amplă am ajuns să dezvoltăm câteva modele pentru servicii care ne-au făcut viața doar puțin mai ușoară, și asta este exact ceea ce am împins în Aldous. Aceste modele sunt ușoare și nu implică multă magie. Ele ne fac viața un pic mai ușoară, dar păstrăm tot controlul dacă avem nevoie de ea.
Mai întâi, mai întâi, unde ar trebui să trăiască serviciile tale? Tindem să le punem app / servicii
, astfel încât aveți nevoie de următoarele în dvs. app / config / application.rb
:
config.autoload_paths + =% W (# config.root / app / services) config.eager_load_paths + =% W (# config.root / app /
Așa cum am menționat mai sus, avem tendința de a numi obiecte de serviciu după acțiuni / verbe (de ex. Creaza utilizator
, RefundPurchase
), dar avem tendința de a adăuga "serviciu" la toate numele de clasă (de ex. CreateUserService
, RefundPurchaseService
). În acest fel, indiferent de contextul în care vă aflați (căutați fișierele din sistemul de fișiere, uitați-vă la o clasă de servicii oriunde în codul de bază) știți întotdeauna că aveți de-a face cu un obiect de serviciu.
Acest lucru nu este impus de bijuterie în nici un fel, dar merită luat în considerare ca o lecție învățată.
Când spunem imutabil, înțelegem că, după ce obiectul este inițializat, starea sa internă nu se va mai schimba. Acest lucru este cu adevărat minunat, deoarece face mult mai simplu să se explice starea fiecărui obiect, precum și sistemul în ansamblu.
Pentru ca cele de mai sus să fie adevărate, metoda obiectului de serviciu nu poate schimba starea obiectului, deci orice date trebuie returnate ca ieșire a metodei. Acest lucru este dificil de aplicat direct, deoarece un obiect va avea întotdeauna acces la propria sa stare internă. Cu Aldous încercăm să o aplicăm prin convenție și educație, iar următoarele două secțiuni vă vor arăta cum.
Un obiect de serviciu Aldous trebuie întotdeauna să returneze unul din cele două tipuri de obiecte:
Aldous :: Serviciul :: Rezultat :: Succes
Aldous :: Serviciul :: Rezultat :: Failure
Iată un exemplu:
clasa CreateUserService < Aldous::Service def perform user = User.new(user_data_hash) if user.save Result::Success.new else Result::Failure.new end end end
Pentru că moștenim Aldous :: Serviciul
, putem construi obiectele de retur ca Rezultat :: Succes
. Utilizarea acestor obiecte ca valori de întoarcere ne permite să facem lucruri precum:
hash = rezultat = CreateUserService.perform (hash) dacă result.success? # face lucruri de succes altceva # result.failure? # face lucruri de eșec final
Am putea, teoretic, să ne întoarcem doar la adevărat sau la fals și să obținem același comportament ca și noi, dar dacă am face acest lucru, nu am putea să transmităm date suplimentare cu valoarea noastră de returnare și de multe ori dorim să transmităm datele.
Succesul sau eșecul unei operațiuni / serviciu este doar o parte din poveste. Deseori vom fi creat un obiect pe care dorim să-l întoarcem sau am creat unele erori despre care dorim să notigem codul de apel. De aceea, returnarea obiectelor, așa cum am arătat mai sus, este utilă. Aceste obiecte nu sunt doar folosite pentru a indica succesul sau eșecul, ci și obiectele de transfer de date.
Aldous vă permite să înlocuiți o metodă pe clasa de servicii de bază, pentru a specifica un set de valori implicite pe care obiectele returnate de serviciu ar conține, de exemplu:
clasa 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
Cheile hash conținute în default_result_data
vor deveni automat metode pe Rezultat :: Succes
și Rezultat :: Nerespectarea
obiecte returnate de serviciu. Și dacă furnizați o valoare diferită pentru una dintre tastele din acea metodă, aceasta va suprascrie valoarea implicită. Deci, în cazul clasei de mai sus:
hash = rezultat = CreateUserService.perform (hash) dacă result.success? result.user # va fi o instanță a utilizatorului result.blah # ar ridica o eroare else # result.failure? result.user # va fi zero result.blah # ar ridica un sfârșit de eroare
De fapt, tastele hash în default_result_data
sunt un contract pentru utilizatorii obiectului de serviciu. Vă garantăm că veți putea apela orice cheie din acel hash ca metodă pe orice obiect rezultat care iese din serviciu.
Când vorbim despre API fără erori, ne referim la metode care nu ridică niciodată erori, dar întotdeauna întorc o valoare pentru a indica succesul sau eșecul. Am scris anterior despre API fără erori. Serviciile Aldous sunt fără erori, în funcție de modul în care le numiți. În exemplul de mai sus:
rezultat = CreateUserService.perform (hash)
Acest lucru nu va ridica niciodată o eroare. Pe plan intern, Aldous îți împachetează metoda de performanță într-o salvare
blocați și dacă codul dvs. ridică o eroare, acesta va reveni Rezultat :: Nerespectarea
cu default_result_data
ca date.
Acest lucru este destul de eliberator, pentru că nu mai trebuie să vă gândiți la ceea ce poate merge prost cu codul pe care l-ați scris. Sunteți interesat doar de succesul sau eșecul serviciului dvs., iar orice eroare va duce la un eșec.
Acest lucru este minunat pentru majoritatea situațiilor. Dar, uneori, doriți o eroare generată. Cel mai bun exemplu este atunci când utilizați un obiect serviciu într-un lucrător de fundal și o eroare ar determina încercarea din nou a lucrătorului de fundal. Acesta este motivul pentru care un serviciu Aldous, de asemenea, devine magic a executa!
și vă permite să înlocuiți o altă metodă din clasa de bază. Iată exemplul nostru din nou:
clasa 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
După cum puteți vedea, am depășit acum raisable_error
metodă. Vrem uneori să se producă o eroare, dar nu vrem să fie nici un tip de eroare. În caz contrar, codul nostru de apel ar trebui să fie conștient de toate erorile posibile pe care le poate produce serviciul sau să fie forțat să prindă unul dintre tipurile de eroare de bază. Acesta este motivul pentru care atunci când utilizați a executa!
, Aldous va prinde toate erorile pentru tine, dar va re-ridica raisable_error
ați specificat și ați setat cauza eronată inițială. Ai putea avea acum acest lucru:
hash = începe serviciul = CreateUserService.build (hash) result = service.perform! serviciul de salvare.raisable_error => e # eroare de sfârșit de lucru
Este posibil să fi observat utilizarea metodei din fabrică:
CreateUserService.build (hash) CreateUserService.perform (hash)
Ar trebui să utilizați întotdeauna aceste lucruri și să nu construiți niciodată obiecte de serviciu direct. Metodele din fabrică sunt cele care ne permit să cârcăm curat caracteristicile cum ar fi salvarea automată și adăugarea default_result_data
.
Cu toate acestea, atunci când vine vorba de teste, nu doriți să vă faceți griji cu privire la modul în care Aldous mărește funcționalitatea obiectelor de serviciu. Deci, atunci când testați, construiți obiectele direct folosind constructorul și apoi testați-vă funcționalitatea. Veți obține specificații pentru logica pe care ați scris-o și credeți că Aldous va face ceea ce ar trebui să facă (Aldous are propriile teste pentru acest lucru) când vine vorba despre producție.
Sperăm că acest lucru v-a dat o idee despre modul în care obiectele de serviciu (și mai ales obiectele de serviciu Aldous) pot fi un instrument frumos în arsenalul dvs. atunci când lucrați cu Ruby / Rails. Dă-i lui Aldous o încercare și spune-ne ce crezi. De asemenea, nu ezitați să aruncați o privire la codul Aldous. Nu am scris doar pentru a fi utile, ci și pentru a fi ușor de citit și ușor de înțeles / modificat.