Construiți componente elegante șine cu obiecte simple de rubin vechi
Publicat: 2022-03-11Site-ul dvs. web câștigă teren și creșteți rapid. Ruby/Rails este limbajul de programare preferat. Echipa ta este mai mare și ai renunțat la „modele grase, controlere slabe” ca stil de design pentru aplicațiile tale Rails. Cu toate acestea, tot nu doriți să renunțați la utilizarea Rails.
Nici o problemă. Astăzi, vom discuta despre cum să folosiți cele mai bune practici OOP pentru a vă face codul mai curat, mai izolat și mai decuplat.
Merită refactorizată aplicația dvs.?
Să începem prin a vedea cum ar trebui să decideți dacă aplicația dvs. este un candidat bun pentru refactorizare.
Iată o listă de valori și întrebări pe care mi le pun de obicei pentru a determina dacă codul meu are nevoie sau nu de refactorizare.
- Teste unitare lente. Testele unitare PORO rulează de obicei rapid cu cod bine izolat, așa că testele care rulează lentă pot fi adesea un indicator al unui design prost și al responsabilităților prea cuplate.
- Modele sau controlere FAT. Un model sau controler cu mai mult de 200 de linii de cod (LOC) este, în general, un bun candidat pentru refactorizare.
- Baza de cod excesiv de mare. Dacă aveți ERB/HTML/HAML cu mai mult de 30.000 LOC sau cod sursă Ruby (fără GEM-uri) cu mai mult de 50.000 LOC, există șanse mari să refactorizați.
Încercați să utilizați așa ceva pentru a afla câte linii de cod sursă Ruby aveți:
find app -iname "*.rb" -type f -exec cat {} \;| wc -l
Această comandă va căuta prin toate fișierele cu extensia .rb (fișiere ruby) din folderul /app și va imprima numărul de linii. Vă rugăm să rețineți că acest număr este doar aproximativ, deoarece rândurile de comentarii vor fi incluse în aceste totaluri.
O altă opțiune mai precisă și mai informativă este să utilizați stats
sarcinii Rails Rake care scoate un rezumat rapid al liniilor de cod, numărului de clase, numărului de metode, raportului dintre metode și clase și raportului liniilor de cod pe metodă:
bundle exec rake stats +----------------------+-------+-----+-------+---------+-----+-------+ | Name | Lines | LOC | Class | Methods | M/C | LOC/M | +----------------------+-------+-----+-------+---------+-----+-------+ | Controllers | 195 | 153 | 6 | 18 | 3 | 6 | | Helpers | 14 | 13 | 0 | 2 | 0 | 4 | | Models | 120 | 84 | 5 | 12 | 2 | 5 | | Mailers | 0 | 0 | 0 | 0 | 0 | 0 | | Javascripts | 45 | 12 | 0 | 3 | 0 | 2 | | Libraries | 0 | 0 | 0 | 0 | 0 | 0 | | Controller specs | 106 | 75 | 0 | 0 | 0 | 0 | | Helper specs | 15 | 4 | 0 | 0 | 0 | 0 | | Model specs | 238 | 182 | 0 | 0 | 0 | 0 | | Request specs | 699 | 489 | 0 | 14 | 0 | 32 | | Routing specs | 35 | 26 | 0 | 0 | 0 | 0 | | View specs | 5 | 4 | 0 | 0 | 0 | 0 | +----------------------+-------+-----+-------+---------+-----+-------+ | Total | 1472 |1042 | 11 | 49 | 4 | 19 | +----------------------+-------+-----+-------+---------+-----+-------+ Code LOC: 262 Test LOC: 780 Code to Test Ratio: 1:3.0
- Pot extrage modele recurente din baza mea de cod?
Decuplare în acțiune
Să începem cu un exemplu din lumea reală.
Prefaceți-vă că vrem să scriem o aplicație care urmărește timpul pentru joggeri. În pagina principală, utilizatorul poate vedea orele pe care le-a introdus.
Fiecare intrare de timp are o dată, distanță, durată și informații suplimentare relevante despre „stare” (de exemplu, vremea, tipul de teren etc.) și o viteză medie care poate fi calculată atunci când este necesar.
Avem nevoie de o pagină de raport care să afișeze viteza medie și distanța pe săptămână.
Dacă viteza medie pentru intrare este mai mare decât viteza medie generală, vom anunța utilizatorul printr-un SMS (pentru acest exemplu vom folosi API-ul Nexmo RESTful pentru a trimite SMS-ul).
Pagina de pornire vă va permite să selectați distanța, data și timpul petrecut la jogging pentru a crea o intrare similară cu aceasta:
Avem, de asemenea, o pagină de statistics
care este practic un raport săptămânal care include viteza medie și distanța parcursă pe săptămână.
- Puteți consulta eșantionul online aici.
Codul
Structura directorului app
arată cam așa:
⇒ tree . ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── helpers │ ├── application_helper.rb │ ├── entries_helper.rb │ └── statistics_helper.rb ├── mailers ├── models │ ├── entry.rb │ └── user.rb └── views ├── devise │ └── ... ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
Nu voi discuta despre modelul User
, deoarece nu este nimic special, deoarece îl folosim cu Devise pentru a implementa autentificarea.
În ceea ce privește modelul Entry
, acesta conține logica de business pentru aplicația noastră.
Fiecare Entry
aparține unui User
.
Validăm prezența atributelor distance
, time_period
, date_time
și status
pentru fiecare intrare.
De fiecare dată când creăm o intrare, comparăm viteza medie a utilizatorului cu media tuturor celorlalți utilizatori din sistem și informăm utilizatorul prin SMS folosind Nexmo (nu vom discuta despre cum este utilizată biblioteca Nexmo, deși am vrut pentru a demonstra un caz în care folosim o bibliotecă externă).
- Eșantion principal
Observați că modelul Entry
conține mai mult decât doar logica de afaceri. De asemenea, gestionează unele validări și apeluri inverse.
entries_controller.rb
are principalele acțiuni CRUD (totuși fără actualizare). EntriesController#index
primește intrările pentru utilizatorul curent și ordonează înregistrările după data creării, în timp ce EntriesController#create
creează o nouă intrare. Nu este nevoie să discutăm despre evidentele și responsabilitățile EntriesController#destroy
:
- Eșantion principal
În timp ce statistics_controller.rb
este responsabil pentru calcularea raportului săptămânal, StatisticsController#index
primește intrările pentru utilizatorul conectat și le grupează pe săptămână, utilizând metoda #group_by
conținută în clasa Enumerable din Rails. Apoi încearcă să decoreze rezultatele folosind unele metode private.
- Eșantion principal
Nu discutăm prea mult despre punctele de vedere aici, deoarece codul sursă se explică de la sine.
Mai jos este vizualizarea pentru listarea intrărilor pentru utilizatorul conectat ( index.html.erb
). Acesta este șablonul care va fi folosit pentru a afișa rezultatele acțiunii (metodei) indexului în controlerul de intrări:
- Eșantion principal
Rețineți că folosim partial render @entries
, pentru a extrage codul partajat într-un șablon parțial _entry.html.erb
, astfel încât să ne putem păstra codul USCAT și reutilizabil:
- Eșantion principal
Același lucru este valabil și pentru _form
parțial. În loc să folosim același cod cu acțiuni (nou și editare), creăm o formă parțială reutilizabilă:
- Eșantion principal
În ceea ce privește vizualizarea paginii de raport săptămânal, statistics/index.html.erb
arată unele statistici și raportează performanța săptămânală a utilizatorului prin gruparea unor intrări:
- Eșantion principal
Și, în cele din urmă, ajutorul pentru intrări, entries_helper.rb
, include doi helper readable_time_period
și readable_speed
care ar trebui să facă atributele mai lizibile uman:
- Eșantion principal
Nimic de lux până acum.
Cei mai mulți dintre voi veți argumenta că refactorizarea este împotriva principiului KISS și va face sistemul mai complicat.
Deci, această aplicație chiar are nevoie de refactorizare?
Absolut nu , dar îl vom lua în considerare doar în scopuri demonstrative.
La urma urmei, dacă consultați secțiunea anterioară și caracteristicile care indică că o aplicație are nevoie de refactorizare, devine evident că aplicația din exemplul nostru nu este un candidat valid pentru refactorizare.
Ciclu de viață
Deci, să începem prin a explica structura modelului Rails MVC.
De obicei, începe prin a face o solicitare de către browser, cum ar fi https://www.toptal.com/jogging/show/1
.
Serverul web primește cererea și folosește routes
pentru a afla ce controller
să folosească.
Controlorii fac munca de a analiza cererile utilizatorilor, trimiterile de date, cookie-urile, sesiunile etc., apoi cer model
să obțină datele.
models
sunt clase Ruby care vorbesc cu baza de date, stochează și validează date, efectuează logica de afaceri și, altfel, fac sarcini grele. Vizualizările sunt ceea ce vede utilizatorul: HTML, CSS, XML, Javascript, JSON.
Dacă vrem să arătăm secvența unui ciclu de viață al cererii Rails, ar arăta cam așa:
Ceea ce vreau să obțin este să adaug mai multă abstracție folosind obiecte rubin vechi (PORO) și să fac modelul așa cum urmează pentru acțiunile de create/update
:
Și ceva de genul următor pentru acțiunile list/show
:
Adăugând abstracții PORO, vom asigura separarea completă între responsabilități SRP, lucru la care Rails nu este foarte bun.
Instrucțiuni
Pentru a realiza noul design, voi folosi liniile directoare enumerate mai jos, dar vă rugăm să rețineți că acestea nu sunt reguli pe care trebuie să le urmați la T. Gândiți-vă la ele ca pe niște ghiduri flexibile care facilitează refactorizarea.
- Modelele ActiveRecord pot conține asocieri și constante, dar nimic altceva. Deci, nu înseamnă apeluri inverse (utilizați obiecte de serviciu și adăugați apelurile înapoi acolo) și nicio validări (utilizați obiecte Form pentru a include denumirea și validările pentru model).
- Păstrați controlerele ca straturi subțiri și apelați întotdeauna obiectele Service. Unii dintre voi v-ar întreba de ce să folosiți controlere, deoarece vrem să continuăm să apelăm obiecte de serviciu pentru a conține logica? Ei bine, controlerele sunt un loc bun pentru a avea rutarea HTTP, analizarea parametrilor, autentificarea, negocierea conținutului, apelarea serviciului sau obiectul editor potrivit, capturarea excepțiilor, formatarea răspunsului și returnarea codului de stare HTTP corect.
- Serviciile ar trebui să apeleze obiecte Query și nu ar trebui să stocheze starea. Folosiți metode de instanță, nu metode de clasă. Ar trebui să existe foarte puține metode publice în conformitate cu SRP.
- Interogările ar trebui făcute în obiecte de interogare. Metodele obiectului de interogare ar trebui să returneze un obiect, un hash sau o matrice, nu o asociere ActiveRecord.
- Evitați să folosiți Helpers și folosiți în schimb decoratori. De ce? O capcană comună cu ajutoarele Rails este că se pot transforma într-o grămadă mare de funcții non-OO, toate partajând un spațiu de nume și călcând unul pe celălalt. Dar mult mai rău este că nu există o modalitate grozavă de a folosi orice fel de polimorfism cu ajutorul Rails - oferind implementări diferite pentru diferite contexte sau tipuri, ajutoare de supraechipare sau subclasare. Cred că clasele de ajutor Rails ar trebui folosite în general pentru metode utilitare, nu pentru cazuri de utilizare specifice, cum ar fi formatarea atributelor modelului pentru orice fel de logică de prezentare. Păstrați-le ușoare și aerisite.
- Evitați utilizarea preocupărilor și folosiți în schimb Decoratori/Delegatori. De ce? La urma urmei, preocupările par să fie o parte esențială a Rails și pot USCA codul atunci când este partajat între mai multe modele. Cu toate acestea, principala problemă este că preocupările nu fac obiectul model mai coeziv. Codul este mai bine organizat. Cu alte cuvinte, nu există o schimbare reală a API-ului modelului.
- Încercați să extrageți obiecte de valoare din modele pentru a vă păstra codul mai curat și pentru a grupa atributele asociate.
- Treceți întotdeauna o variabilă de instanță per vizualizare.
Refactorizarea
Înainte de a începe, vreau să discutăm încă un lucru. Când începeți refactorizarea, de obicei ajungeți să vă întrebați: „Este chiar bună refactorizarea?”
Dacă simțiți că faceți mai multă separare sau izolare între responsabilități (chiar dacă asta înseamnă să adăugați mai mult cod și fișiere noi), atunci acesta este de obicei un lucru bun. La urma urmei, decuplarea unei aplicații este o practică foarte bună și ne face mai ușor să facem testarea unitară adecvată.
Nu voi discuta lucruri, cum ar fi mutarea logicii de la controlere la modele, deoarece presupun că faci asta deja și că ești confortabil să folosești Rails (de obicei, Skinny Controller și modelul FAT).
De dragul de a păstra acest articol strâns, nu voi discuta despre testare aici, dar asta nu înseamnă că nu ar trebui să testați.
Dimpotrivă, ar trebui să începeți întotdeauna cu un test pentru a vă asigura că lucrurile sunt în regulă înainte de a merge mai departe. Aceasta este o necesitate, mai ales la refactorizare.
Apoi putem implementa modificări și ne putem asigura că toate testele trec pentru părțile relevante ale codului.
Extragerea obiectelor de valoare
În primul rând, ce este un obiect de valoare?
Martin Fowler explică:
Value Object este un obiect mic, cum ar fi un obiect de bani sau interval de date. Proprietatea lor cheie este că urmează semantica valorii mai degrabă decât semantica de referință.
Uneori poți întâlni o situație în care un concept își merită propria abstractizare și a cărui egalitate nu se bazează pe valoare, ci pe identitate. Exemplele ar include data lui Ruby, URI și Pathname. Extragerea la un obiect de valoare (sau model de domeniu) este o mare comoditate.
De ce sa te deranjezi?
Unul dintre cele mai mari avantaje ale unui obiect Value este expresivitatea pe care o ajută să o obțină în codul tău. Codul dvs. va tinde să fie mult mai clar, sau cel puțin poate fi dacă aveți bune practici de denumire. Deoarece obiectul valoare este o abstractizare, duce la un cod mai curat și la mai puține erori.
Un alt mare câștig este imuabilitatea. Imuabilitatea obiectelor este foarte importantă. Când stocăm anumite seturi de date, care ar putea fi folosite într-un obiect de valoare, de obicei nu vreau ca aceste date să fie manipulate.
Când este util acest lucru?
Nu există un răspuns unic, care se potrivește tuturor. Fă ceea ce este mai bine pentru tine și ceea ce are sens în orice situație dată.
Mergând dincolo de asta, totuși, există câteva linii directoare pe care le folosesc pentru a mă ajuta să iau această decizie.
Dacă te gândești că un grup de metode este înrudit, cu obiectele Value acestea sunt mai expresive. Această expresivitate înseamnă că un obiect Value ar trebui să reprezinte un set distinct de date, pe care dezvoltatorul tău obișnuit le poate deduce pur și simplu uitându-se la numele obiectului.
Cum se face asta?
Obiectele de valoare ar trebui să respecte câteva reguli de bază:
- Obiectele de valoare ar trebui să aibă mai multe atribute.
- Atributele ar trebui să fie imuabile pe tot parcursul ciclului de viață al obiectului.
- Egalitatea este determinată de atributele obiectului.
În exemplul nostru, voi crea un obiect de valoare EntryStatus
pentru a abstractiza Entry#status_weather
și Entry#status_landform
în propria lor clasă, care arată cam așa:
- Eșantion principal
Notă: Acesta este doar un Plain Old Ruby Object (PORO) care nu moștenește de la ActiveRecord::Base
. Am definit metode de citire pentru atributele noastre și le atribuim la inițializare. De asemenea, am folosit un mixin comparabil pentru a compara obiecte folosind metoda (<=>).
Putem modifica modelul Entry
pentru a folosi obiectul de valoare creat de noi:
- Eșantion principal
De asemenea, putem modifica metoda EntryController#create
pentru a folosi noul obiect de valoare în consecință:
- Eșantion principal
Extrageți obiecte de serviciu
Deci, ce este un obiect de serviciu?
Sarcina unui obiect de serviciu este de a păstra codul pentru un anumit bit de logică de afaceri. Spre deosebire de stilul „model gras” , în care un număr mic de obiecte conțin multe, multe metode pentru toată logica necesară, utilizarea obiectelor Service are ca rezultat multe clase, fiecare dintre acestea având un singur scop.

De ce? Care sunt beneficiile?
- Decuplare. Obiectele de serviciu vă ajută să obțineți mai multă izolare între obiecte.
- Vizibilitate. Obiectele de serviciu (dacă sunt bine numite) arată ce face o aplicație. Pot doar să arunc o privire peste directorul de servicii pentru a vedea ce capabilități oferă o aplicație.
- Curățați modele și controlere. Controlorii transformă cererea (parametri, sesiune, cookie-uri) în argumente, le transmit serviciului și redirecționează sau redă în funcție de răspunsul serviciului. În timp ce modelele se ocupă doar de asocieri și persistență. Extragerea codului de la controlere/modele la obiecte de service ar suporta SRP și ar face codul mai decuplat. Responsabilitatea modelului ar fi atunci doar să se ocupe de asocieri și de salvarea/ștergerea înregistrărilor, în timp ce obiectul de serviciu ar avea o singură responsabilitate (SRP). Acest lucru duce la un design mai bun și la teste unitare mai bune.
- USCĂ și Îmbrățișează schimbarea. Păstrez obiectele de service cât pot de simple și mici. Compun obiecte de serviciu cu alte obiecte de serviciu și le reutilizam.
- Curățați și accelerați suita de teste. Serviciile sunt ușor și rapid de testat, deoarece sunt mici obiecte Ruby cu un singur punct de intrare (metoda apelului). Serviciile complexe sunt compuse cu alte servicii, astfel încât să vă puteți împărți testele cu ușurință. De asemenea, utilizarea obiectelor de serviciu facilitează baterea/stupirea obiectelor asociate fără a fi nevoie să încărcați întregul mediu șinelor.
- Apelabil de oriunde. Este posibil ca obiectele de serviciu să fie apelate de la controlere, precum și de la alte obiecte de serviciu, joburi DelayedJob / Rescue / Sidekiq, sarcini Rake, consolă etc.
Pe de altă parte, nimic nu este niciodată perfect. Un dezavantaj al obiectelor Service este că pot fi exagerat pentru o acțiune foarte simplă. În astfel de cazuri, s-ar putea foarte bine să ajungeți să vă complicați, mai degrabă decât să simplificați, codul.
Când ar trebui să extrageți obiectele de serviciu?
Nici aici nu există o regulă rigidă.
În mod normal, obiectele Service sunt mai bune pentru sistemele medii sau mari; cei cu o cantitate decentă de logică dincolo de operațiunile standard CRUD.
Deci, ori de câte ori credeți că un fragment de cod ar putea să nu aparțină directorului în care urma să-l adăugați, probabil că este o idee bună să vă reconsiderați și să vedeți dacă ar trebui să meargă la un obiect de serviciu.
Iată câțiva indicatori despre când să utilizați obiectele de serviciu:
- Acțiunea este complexă.
- Acțiunea se întinde pe mai multe modele.
- Acțiunea interacționează cu un serviciu extern.
- Acțiunea nu este o preocupare centrală a modelului de bază.
- Există mai multe moduri de a efectua acțiunea.
Cum ar trebui să proiectați obiecte de serviciu?
Proiectarea clasei pentru un obiect de serviciu este relativ simplă, deoarece nu aveți nevoie de pietre prețioase speciale, nu trebuie să învățați un nou DSL și vă puteți baza mai mult sau mai puțin pe abilitățile de proiectare software pe care le dețineți deja.
De obicei, folosesc următoarele linii directoare și convenții pentru a proiecta obiectul de serviciu:
- Nu stocați starea obiectului.
- Folosiți metode de instanță, nu metode de clasă.
- Ar trebui să existe foarte puține metode publice (de preferință una care să susțină SRP.
- Metodele ar trebui să returneze obiecte cu rezultate bogate și nu boolean.
- Serviciile merg în directorul
app/services
. Vă încurajez să utilizați subdirectoare pentru domenii cu logica de afaceri grele. De exemplu, fișierulapp/services/report/generate_weekly.rb
va definiReport::GenerateWeekly
, în timp ceapp/services/report/publish_monthly.rb
va definiReport::PublishMonthly
. - Serviciile încep cu un verb (și nu se termină cu Service):
ApproveTransaction
,SendTestNewsletter
,ImportUsersFromCsv
. - Serviciile răspund la metoda de apel. Am descoperit că folosirea unui alt verb îl face puțin redundant: ApproveTransaction.approve() nu citește bine. De asemenea, metoda call este metoda de facto pentru obiectele lambda, procs și method.
Dacă te uiți la StatisticsController#index
, vei observa un grup de metode ( weeks_to_date_from
, weeks_to_date_to
, avg_distance
, etc.) cuplate la controler. Asta nu e chiar bine. Luați în considerare ramificațiile dacă doriți să generați raportul săptămânal în afara statistics_controller
.
În cazul nostru, să creăm Report::GenerateWeekly
și să extragem logica raportului din StatisticsController
:
- Eșantion principal
Deci StatisticsController#index
arată acum mai curat:
- Eșantion principal
Prin aplicarea modelului de obiect Service, grupăm codul în jurul unei acțiuni specifice și complexe și promovăm crearea de metode mai mici și mai clare.
Temă: luați în considerare utilizarea obiectului Value pentru WeeklyReport
în loc de Struct
.
Extrageți obiectele de interogare din controlere
Ce este un obiect Query?
Un obiect Query este un PORO care reprezintă o interogare de bază de date. Poate fi reutilizat în diferite locuri din aplicație, ascunzând în același timp logica interogării. De asemenea, oferă o unitate izolată bună de testat.
Ar trebui să extrageți interogări complexe SQL/NoSQL în propria lor clasă.
Fiecare obiect Query este responsabil pentru returnarea unui set de rezultate pe baza criteriilor / regulilor de afaceri.
În acest exemplu, nu avem interogări complexe, așa că utilizarea obiectului Query nu va fi eficientă. Cu toate acestea, în scop demonstrativ, să extragem interogarea în Report::GenerateWeekly#call
și să creăm generate_entries_query.rb
:
- Eșantion principal
Și în Report::GenerateWeekly#call
, să înlocuim:
def call @user.entries.group_by(&:week).map do |week, entries| WeeklyReport.new( ... ) end end
cu:
def call weekly_grouped_entries = GroupEntriesQuery.new(@user).call weekly_grouped_entries.map do |week, entries| WeeklyReport.new( ... ) end end
Modelul obiectului de interogare vă ajută să păstrați logica modelului strict legată de comportamentul unei clase, menținând, de asemenea, controlerele subțiri. Deoarece nu sunt altceva decât clase vechi Ruby, obiectele de interogare nu trebuie să moștenească din ActiveRecord::Base
și nu ar trebui să fie responsabile pentru nimic mai mult decât executarea interogărilor.
Extrageți crearea intrării într-un obiect de serviciu
Acum, să extragem logica creării unei noi intrări la un nou obiect de serviciu. Să folosim convenția și să creăm CreateEntry
:
- Eșantion principal
Și acum, EntriesController#create
este după cum urmează:
def create begin CreateEntry.new(current_user, entry_params).call flash[:notice] = 'Entry was successfully created.' rescue Exception => e flash[:error] = e.message end redirect_to root_path end
Mutați validările într-un obiect formular
Acum, aici lucrurile încep să devină mai interesante.
Amintiți-vă, în ghidurile noastre, am convenit că dorim ca modelele să conțină asocieri și constante, dar nimic altceva (fără validări și fără apeluri inverse). Deci, să începem prin a elimina apelurile inverse și să folosim în schimb un obiect Form.
Un obiect Form este un Plain Old Ruby Object (PORO). Preia controlerul/obiectul de serviciu oriunde trebuie să vorbească cu baza de date.
De ce să folosiți obiecte Form?
Când căutați să refactorizați aplicația dvs., este întotdeauna o idee bună să aveți în vedere principiul responsabilității unice (SRP).
SRP vă ajută să luați decizii de proiectare mai bune cu privire la ceea ce ar trebui să fie responsabilă o clasă.
Modelul tabelului bazei de date (un model ActiveRecord în contextul Rails), de exemplu, reprezintă o singură înregistrare a bazei de date în cod, așa că nu există niciun motiv pentru care să fie preocupat de orice face utilizatorul tău.
Aici intervin obiectele Form.
Un obiect Formular este responsabil pentru reprezentarea unui formular în aplicația dvs. Deci, fiecare câmp de intrare poate fi tratat ca un atribut în clasă. Poate valida că aceste atribute îndeplinesc unele reguli de validare și poate transmite datele „curate” acolo unde trebuie să ajungă (de exemplu, modelele bazei de date sau poate generatorul de interogări de căutare).
Când ar trebui să utilizați un obiect Form?
- Când doriți să extrageți validările din modelele Rails.
- Când mai multe modele pot fi actualizate printr-o singură trimitere a formularului, este posibil să doriți să creați un obiect Formular.
Acest lucru vă permite să puneți toată logica formularului (convenții de denumire, validări și așa mai departe) într-un singur loc.
Cum creezi un obiect Form?
- Creați o clasă Ruby simplă.
- Includeți
ActiveModel::Model
(în Rails 3, trebuie să includeți în schimb denumirea, conversia și validările) - Începeți să utilizați noua clasă de formulare ca și cum ar fi un model ActiveRecord obișnuit, cea mai mare diferență fiind că nu puteți persista datele stocate în acest obiect.
Vă rugăm să rețineți că puteți utiliza bijuteria reformei, dar rămânând cu PORO-urile, vom crea entry_form.rb
care arată astfel:
- Eșantion principal
Și vom modifica CreateEntry
pentru a începe să folosim obiectul Form EntryForm
:
class CreateEntry ...... ...... def call @entry_form = ::EntryForm.new(@params) if @entry_form.valid? .... else .... end end end
Notă: Unii dintre voi ar spune că nu este nevoie să accesați obiectul Form din obiectul Service și că putem apela obiectul Form direct de la controler, ceea ce este un argument valid. Cu toate acestea, aș prefera să am un flux clar și, de aceea, apelez întotdeauna obiectul Form din obiectul Service.
Mutați apelurile înapoi în obiectul de serviciu
După cum am convenit mai devreme, nu dorim ca modelele noastre să conțină validări și apeluri inverse. Am extras validările folosind obiecte Form. Dar încă folosim câteva apeluri inverse ( after_create
în modelul Entry
compare_speed_and_notify_user
).
De ce vrem să eliminăm apelurile înapoi de la modele?
Dezvoltatorii Rails încep de obicei să observe durerea de apel invers în timpul testării. Dacă nu testați modelele dvs. ActiveRecord, veți începe să observați dureri mai târziu, pe măsură ce aplicația dvs. crește și pe măsură ce este nevoie de mai multă logică pentru a apela sau a evita apelul înapoi.
after_*
sunt utilizate în principal în legătură cu salvarea sau menținerea obiectului.
Odată ce obiectul este salvat, scopul (adică responsabilitatea) obiectului a fost îndeplinit. Deci, dacă vedem în continuare apeluri invocate după ce obiectul a fost salvat, ceea ce vedem probabil este apelurile care ajung în afara zonei de responsabilitate a obiectului și atunci ne confruntăm cu probleme.
În cazul nostru, trimitem un SMS utilizatorului după ce salvăm o intrare, care nu are cu adevărat legătură cu domeniul Entry.
O modalitate simplă de a rezolva problema este mutarea apelului înapoi la obiectul de serviciu aferent. La urma urmei, trimiterea unui SMS pentru utilizatorul final este legată de obiectul serviciului CreateEntry
și nu de modelul Entry în sine.
Procedând astfel, nu mai trebuie să eliminăm metoda compare_speed_and_notify_user
în testele noastre. Am făcut o chestiune simplă să creăm o intrare fără a necesita trimiterea unui SMS și urmăm un design bun orientat pe obiect, asigurându-ne că clasele noastre au o singură responsabilitate (SRP).
Așa că acum CreateEntry
arată ceva de genul:
- Eșantion principal
Folosiți decoratori în loc de ajutoare
Deși putem folosi cu ușurință colecția Draper de modele de vedere și decoratori, voi rămâne la PORO de dragul acestui articol, așa cum am făcut până acum.
Ceea ce am nevoie este o clasă care va apela metode pe obiectul decorat.
Pot folosi method_missing
pentru a implementa asta, dar voi folosi biblioteca standard a lui Ruby SimpleDelegator
.
Următorul cod arată cum să folosiți SimpleDelegator
pentru a implementa decoratorul nostru de bază:
% app/decorators/base_decorator.rb require 'delegate' class BaseDecorator < SimpleDelegator def initialize(base, view_context) super(base) @object = base @view_context = view_context end private def self.decorates(name) define_method(name) do @object end end def _h @view_context end end
Deci, de ce metoda _h
?
Această metodă acționează ca un proxy pentru contextul de vizualizare. În mod implicit, contextul de vizualizare este o instanță a unei clase de vizualizare, clasa de vizualizare implicită fiind ActionView::Base
. Puteți accesa ajutoarele de vizualizare după cum urmează:
_h.content_tag :div, 'my-div', class: 'my-class'
Pentru a o face mai convenabilă, adăugăm o metodă de decorate
la ApplicationHelper
:
module ApplicationHelper # ..... def decorate(object, klass = nil) klass ||= "#{object.class}Decorator".constantize decorator = klass.new(object, self) yield decorator if block_given? decorator end # ..... end
Acum, putem muta ajutoarele EntriesHelper
la decoratori:
# app/decorators/entry_decorator.rb class EntryDecorator < BaseDecorator decorates :entry def readable_time_period mins = entry.time_period return Time.at(60 * mins).utc.strftime('%M <small>Mins</small>').html_safe if mins < 60 Time.at(60 * mins).utc.strftime('%H <small>Hour</small> %M <small>Mins</small>').html_safe end def readable_speed "#{sprintf('%0.2f', entry.speed)} <small>Km/H</small>".html_safe end end
Și putem folosi readable_time_period
și readable_speed
astfel:
# app/views/entries/_entry.html.erb - <td><%= readable_speed(entry) %> </td> + <td><%= decorate(entry).readable_speed %> </td>
- <td><%= readable_time_period(entry) %></td> + <td><%= decorate(entry).readable_time_period %></td>
Structura după refactorizare
Am ajuns să avem mai multe fișiere, dar asta nu este neapărat un lucru rău (și amintiți-vă că, de la început, am recunoscut că acest exemplu a fost doar în scopuri demonstrative și nu a fost neapărat un caz de utilizare bun pentru refactorizare):
app ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── decorators │ ├── base_decorator.rb │ └── entry_decorator.rb ├── forms │ └── entry_form.rb ├── helpers │ └── application_helper.rb ├── mailers ├── models │ ├── entry.rb │ ├── entry_status.rb │ └── user.rb ├── queries │ └── group_entries_query.rb ├── services │ ├── create_entry.rb │ └── report │ └── generate_weekly.rb └── views ├── devise │ └── .. ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
Concluzie
Chiar dacă ne-am concentrat pe șine în această postare pe blog, RoR nu este o dependență de obiectele de serviciu descrise și alte PORO. Puteți utiliza această abordare cu orice cadru web, aplicație mobilă sau consolă.
Folosind MVC ca arhitectură a aplicațiilor web, totul rămâne cuplat și te face să mergi mai încet, deoarece majoritatea modificărilor au un impact asupra altor părți ale aplicației. De asemenea, vă obligă să vă gândiți unde să puneți o logică de afaceri - ar trebui să intre în model, controlor sau vizualizare?
Folosind PORO-uri simple, am mutat logica de business la modele sau servicii care nu moștenesc de la ActiveRecord
, ceea ce este deja un mare câștig, ca să nu mai vorbim că avem un cod mai curat, care acceptă SRP și teste unitare mai rapide.
Arhitectura curată își propune să plaseze cazurile de utilizare în centrul/susul structurii dvs., astfel încât să puteți vedea cu ușurință ce face aplicația dvs. De asemenea, facilitează adoptarea modificărilor, deoarece este mult mai modular și izolat.
Sper că am demonstrat cum utilizarea Plain Old Ruby Objects și a mai multor abstracții decuplează preocupările, simplifică testarea și ajută la producerea unui cod curat, care poate fi întreținut.