Oggetti di servizio con guide che utilizzano Aldous

Uno dei concetti con cui abbiamo avuto un grande successo nel team Tuts + è rappresentato dagli oggetti di servizio. Abbiamo utilizzato oggetti di servizio per ridurre l'accoppiamento nei nostri sistemi, renderli più testabili e rendere la logica di business più importante a tutti gli sviluppatori del team.. 

Quindi, quando abbiamo deciso di codificare alcuni dei concetti che abbiamo usato nel nostro sviluppo Rails in una gemma Ruby (chiamata Aldous), gli oggetti di servizio erano in cima alla lista.

Quello che mi piacerebbe fare oggi è dare una rapida carrellata di oggetti di servizio come li abbiamo implementati in Aldous. Speriamo che questo vi dirà la maggior parte delle cose che dovete sapere per poter utilizzare gli oggetti di servizio Aldous nei vostri progetti.

Anatomia di un oggetto di servizio di base

Foto di Dennis Skley

Un oggetto servizio è fondamentalmente un metodo che è racchiuso in un oggetto. A volte un oggetto servizio può contenere diversi metodi, ma la versione più semplice è solo una classe con un metodo, ad esempio:

class DoSomething def perform # do stuff end end

Siamo abituati a usare nomi per nominare i nostri oggetti, ma a volte può essere difficile trovare un buon nome per rappresentare un concetto, mentre parlarne in termini di un'azione (o verbo) è semplice e naturale. Un oggetto di servizio è ciò che otteniamo quando "andiamo con il flusso" e trasformiamo semplicemente il verbo in un oggetto.

Naturalmente, data la definizione di cui sopra, possiamo trasformare qualsiasi azione / metodo in un oggetto di servizio, se lo desideriamo. Il seguente…

class Customer def createPurchase (order) # do stuff end end

... potrebbe essere trasformato in:

class CreateCustomerPurchase def initialize (customer, order) end def perform # do stuff end end

Potremmo scrivere molti altri post sugli effetti che gli oggetti di servizio potrebbero avere sul design del tuo sistema, i vari trade-off che farai, ecc. Per ora limitiamoci a considerarli un concetto e considerarli solo un altro strumento abbiamo nel nostro arsenale.

Perché utilizzare gli oggetti di servizio in Rails

Con l'aumentare delle dimensioni delle app Rails, i nostri modelli tendono a diventare piuttosto grandi, quindi cerchiamo modi per estrapolarne alcune funzionalità in oggetti "helper". Ma questo è spesso più facile a dirsi che a farsi. Rails non ha un concetto, nel livello del modello, che è più granulare di un modello. Quindi finisci per dover fare molte chiamate di giudizio:

  • Crei un modello PORO o crei un corso nel lib cartella?
  • Quali metodi ti muovi in ​​questa classe?
  • Come nominate sensibilmente questa classe, dati i metodi con cui ci siamo trasferiti? 

Ora devi comunicare ciò che hai fatto agli altri sviluppatori del tuo team ea tutte le nuove persone che si uniranno più tardi. E, naturalmente, di fronte a una situazione simile, altri sviluppatori potrebbero fare diverse chiamate di giudizio, portando a incoerenze che si insinuano.

Gli oggetti di servizio ci danno un concetto che è più granulare di un modello. Possiamo avere una posizione coerente per tutti i nostri servizi e spostare sempre un solo metodo in un servizio. Puoi nominare questa classe dopo l'azione / metodo che rappresenterà. Possiamo estrarre la funzionalità in oggetti più granulari senza troppe chiamate di giudizio, che mantiene l'intero team sulla stessa pagina, permettendoci di andare avanti con il business della creazione di una grande applicazione. 

L'utilizzo di oggetti di servizio riduce l'accoppiamento tra i modelli Rails ei servizi risultanti sono altamente riutilizzabili a causa delle dimensioni ridotte / ingombro ridotto. 

Gli oggetti di servizio sono anche altamente testabili, in quanto di solito non richiedono più codice di prova di altri oggetti pesanti e ti preoccupi solo di testare l'unico metodo che l'oggetto contiene. 

Sia gli oggetti di servizio che i loro test sono di facile lettura / comprensione in quanto altamente coesi (anche un effetto collaterale delle loro piccole dimensioni). Puoi anche scartare e riscrivere sia gli oggetti di servizio che i loro test quasi a piacimento, poiché il costo di ciò è relativamente basso ed è molto facile mantenere la loro interfaccia.

Gli oggetti di servizio hanno sicuramente molto da offrire, specialmente quando li introducete nelle vostre app Rails. 

Servizio oggetti con Aldous

Foto di Trevor Leyenhorst

Dato che gli oggetti di servizio sono così semplici, perché abbiamo persino bisogno di un gioiello? Perché non creare solo PORO e quindi non è necessario preoccuparsi di un'altra dipendenza? 

Potresti sicuramente farlo, e in effetti lo abbiamo fatto per un po 'in Tuts +, ma attraverso un uso esteso abbiamo finito per sviluppare alcuni modelli per servizi che hanno reso le nostre vite un po' più semplici, ed è esattamente quello che abbiamo spinto in Aldous. Questi modelli sono leggeri e non implicano molta magia. Rendono le nostre vite un po 'più semplici, ma manteniamo tutto il controllo se ne abbiamo bisogno.

Dove dovrebbero vivere

Per prima cosa, dove dovrebbero vivere i tuoi servizi? Tendiamo a inserirli app / servizi, quindi hai bisogno del seguente nel tuo app / config / application.rb:

config.autoload_paths + =% W (# config.root / app / services) config.eager_load_paths + =% W (# config.root / app / services)

Come dovrebbero essere chiamati

Come ho menzionato sopra, tendiamo a dare un nome agli oggetti di servizio dopo azioni / verbi (ad es. Creare un utente, RefundPurchase), ma tendiamo anche ad aggiungere "servizio" a tutti i nomi di classe (ad es. CreateUserService, RefundPurchaseService). In questo modo indipendentemente dal contesto in cui ti trovi (guardando i file sul file system, guardando una classe di servizio in qualsiasi punto della base di codici) sai sempre che hai a che fare con un oggetto di servizio.

Questo non è imposto dalla gemma in alcun modo, ma vale la pena prendere in considerazione come una lezione appresa.

Gli oggetti di servizio sono immutabili

Quando diciamo immutabile, intendiamo che dopo che l'oggetto è stato inizializzato, il suo stato interno non cambierà più. Questo è davvero grandioso perché rende molto più semplice ragionare sullo stato di ogni oggetto e sul sistema nel suo insieme.

Affinché quanto sopra sia vero, il metodo dell'oggetto servizio non può modificare lo stato dell'oggetto, quindi tutti i dati devono essere restituiti come output del metodo. Questo è difficile da applicare direttamente, poiché un oggetto avrà sempre accesso al proprio stato interno. Con Aldous cerchiamo di farla rispettare attraverso la convenzione e l'istruzione, e le prossime due sezioni ti mostreranno come.

Rappresentare il successo e il fallimento

Un oggetto di servizio Aldous deve sempre restituire uno dei due tipi di oggetti:

  • Aldous :: Service :: Risultato :: Successo
  • Aldous :: Service :: Risultato :: Fallimento

Ecco un esempio:

classe CreateUserService < Aldous::Service def perform user = User.new(user_data_hash) if user.save Result::Success.new else Result::Failure.new end end end

Perché ereditiamo da Aldous :: servizio, possiamo costruire i nostri oggetti di ritorno come Risultato :: Successo. L'utilizzo di tali oggetti come valori di ritorno ci consente di fare cose come:

hash =  result = CreateUserService.perform (hash) if result.success? # do successo stuff else # result.failure? # finiscono le cose fallimentari

In teoria, potremmo semplicemente restituire vero o falso e ottenere lo stesso comportamento di cui sopra, ma se lo facessimo non potremmo trasportare alcun dato aggiuntivo con il nostro valore di ritorno, e spesso vogliamo trasportare dati.

Utilizzo di DTO

Il successo o il fallimento di un'operazione / servizio è solo una parte della storia. Spesso avremo creato qualche oggetto che vogliamo restituire, o prodotto degli errori di cui vogliamo notificare il codice chiamante. Questo è il motivo per cui restituire oggetti, come abbiamo mostrato sopra, è utile. Questi oggetti non sono solo usati per indicare il successo o il fallimento, ma sono anche oggetti di trasferimento dati.

Aldous consente di sovrascrivere un metodo sulla classe di servizio di base, per specificare un insieme di valori predefiniti che gli oggetti restituiti dal servizio conterrebbero, ad esempio:

classe 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

Le chiavi di hash contenute in default_result_data diventeranno automaticamente metodi sul Risultato :: Successo e Risultato :: Fallimento oggetti restituiti dal servizio. E se fornisci un valore diverso per una delle chiavi in ​​quel metodo, sovrascriverà il valore predefinito. Quindi nel caso della classe di cui sopra:

hash =  result = CreateUserService.perform (hash) if result.success? result.user # sarà un'istanza di User result.blah # potrebbe generare un errore else # result.failure? result.user # sarà nil result.blah # genererebbe un errore

In effetti le chiavi di hash in default_result_data metodo sono un contratto per gli utenti dell'oggetto servizio. Garantiamo che sarà possibile chiamare qualsiasi chiave in tale hash come metodo su qualsiasi oggetto risultato che esce dal servizio.

API senza errori

Immagine di Roberto Zingales

Quando parliamo di API prive di errori intendiamo metodi che non generano mai errori, ma restituiscono sempre un valore per indicare il successo o l'insuccesso. Ho già scritto su API prive di errori. I servizi Aldous sono privi di errori a seconda di come li chiami. Nell'esempio sopra: 

risultato = CreateUserService.perform (hash)

Questo non genererà mai un errore. Internally Aldous racchiude il tuo metodo di esecuzione in a salvare blocco e se il tuo codice solleva un errore restituirà a Risultato :: Fallimento con il default_result_data come dati. 

Questo è abbastanza liberatorio, perché non devi più pensare a cosa può andare storto con il codice che hai scritto. Ti interessa solo il successo o l'insuccesso del tuo servizio, e qualsiasi errore provocherà un errore. 

Questo è ottimo per la maggior parte delle situazioni. Ma a volte, vuoi un errore generato. Il miglior esempio è quando si utilizza un oggetto di servizio in un worker in background e un errore causerebbe un nuovo tentativo da parte dell'operatore in background. Ecco perché anche un servizio Aldous ottiene magicamente un eseguire! metodo e consente di sovrascrivere un altro metodo dalla classe base. Ecco di nuovo il nostro esempio:

classe 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

Come puoi vedere, ora abbiamo scavalcato il raisable_error metodo. A volte vogliamo che venga prodotto un errore, ma non vogliamo che si verifichi alcun tipo di errore. Altrimenti il ​​nostro codice chiamante dovrebbe prendere coscienza di ogni possibile errore che il servizio può produrre, o essere costretto a catturare uno dei tipi di errore di base. Questo è il motivo per cui quando usi il eseguire! metodo, Aldous catturerà comunque tutti gli errori per te, ma poi rileverà il raisable_error hai specificato e imposta l'errore originale come causa. Ora puoi avere questo:

hash =  begin service = CreateUserService.build (hash) result = service.perform! rescue service.raisable_error => e # errore roba fine

Test di oggetti di servizio Aldous

Potresti aver notato l'utilizzo del metodo di fabbrica:

CreateUserService.build (hash) CreateUserService.perform (hash)

Dovresti sempre utilizzarli e non costruire direttamente oggetti di servizio. I metodi di fabbrica sono ciò che ci permette di collegare in modo pulito le caratteristiche gradevoli come il salvataggio automatico e l'aggiunta del default_result_data.

Tuttavia, quando si tratta di test, non ti devi preoccupare di come Aldous aumenti la funzionalità dei tuoi oggetti di servizio. Quindi, quando si esegue il test, è sufficiente costruire gli oggetti direttamente utilizzando il costruttore e quindi testare la funzionalità. Otterrete le specifiche per la logica che avete scritto e credete che Aldous farà ciò che deve fare (Aldous ha i suoi test per questo) quando si tratta di produzione.

Conclusione

Speriamo che questo ti abbia dato un'idea di come gli oggetti di servizio (e in particolare gli oggetti di servizio Aldous) possano essere uno strumento utile nel tuo arsenale quando lavori con Ruby / Rails. Fai una prova a Aldous e facci sapere cosa ne pensi. Sentiti pure libero di dare un'occhiata al codice Aldous. Non l'abbiamo semplicemente scritto per essere utile, ma anche per essere leggibile e facile da capire / modificare.