Un problema comune quando si lavora con Rails è decidere dove collocare la logica delle nostre funzioni.
La logica è spesso collocata nei controllori, nei modelli o, se siamo fortunati, in un oggetto servizio. Quindi, se abbiamo gli oggetti di servizio, perché abbiamo bisogno dei casi d'uso?
Seguitemi in questo articolo per scoprire i vantaggi di questo modello.
Caso d'uso
Definizione
Un caso d'uso è un elenco di azioni o fasi di eventi che definiscono tipicamente le interazioni tra un ruolo e un sistema per raggiungere un obiettivo.
Vale la pena ricordare che questo schema viene applicato in molti modi diversi e ha nomi alternativi. Possiamo trovarlo come Interattori, Operatori o Comandi, ma nel Rubino comunità a cui ci atteniamo Caso d'uso. Ogni implementazione è diversa, ma con lo stesso scopo: servire il caso d'uso del sistema da parte dell'utente.
Anche se nel nostro progetto non stiamo definendo i requisiti utilizzando Caso d'usoe UML, questo pattern è ancora utile per strutturare la logica aziendale in modo pratico.
Regole
Il nostro Casi d'uso deve essere:
Agnostico al framework
Agnostico rispetto al database
Responsabile di una sola cosa (definire i passi per raggiungere l'obiettivo dell'utente)
Vantaggi
Leggibilità: Facile da leggere e da capire, poiché i passaggi sono chiaramente definiti.
Disaccoppiamento: Spostare la logica dai controllori e dai modelli e creare un nuovo livello di astrazione.
Visibilità: La base di codice rivela le funzionalità disponibili nel sistema.
Nella pratica
Prendiamo l'esempio di un utente che vuole acquistare qualcosa nel nostro sistema.
modulo Casi d'uso
modulo Acquirente
classe Acquisto
def initialize(buyer:, cart:)
@acquirente = acquirente
@carrello = carrello
fine
def call
return unless check_stock
return a meno che create_purchase
notificare fine
privato
attr_reader :buyer, :cart
def check_stock
Services::CheckStock.call(cart: cart)
fine
def crea_acquisto
Servizi::CreaAcquisto.call(acquirente: acquirente, carrello: carrello).call
fine
def notifica
Servizi::NotificaAcquirente.call(acquirente: acquirente)
fine
fine
fine
fine
Come si può vedere in questo codice esempio, abbiamo creato un nuovo Caso d'uso chiamato Acquisto. Abbiamo definito un solo metodo pubblico chiamata. All'interno del metodo call, troviamo i passi fondamentali per effettuare un acquisto e tutti i passi sono definiti come metodi privati. Ogni passo è una chiamata a un oggetto servizio, in questo modo il nostro Caso d'uso sta definendo solo i passaggi per effettuare un acquisto e non la logica stessa. Questo ci dà un quadro chiaro di ciò che può essere fatto nel nostro sistema (effettuare un acquisto) e dei passaggi per ottenerlo.
Ora siamo pronti a chiamare il nostro primo Caso d'uso da un controllore.
classe Controllore
def acquisto
UseCases::Buyer::Purchase.new(
acquirente: purchase_params[:buyer],
carrello: purchase_params[:cart]
).call
...
fine
...
fine
Da questo punto di vista, il Caso d'uso assomiglia molto a un Oggetto Servizio, ma lo scopo è diverso. Un oggetto servizio esegue un compito di basso livello e interagisce con diverse parti del sistema, come il database, mentre l'oggetto Il caso d'uso crea una nuova astrazione di alto livello e definisce i passaggi logici.
Miglioramenti
Il nostro primo Caso d'uso funziona ma potrebbe essere migliore. Come possiamo migliorarlo? Utilizziamo il asciutto gemme. In questo caso utilizzeremo transazione a secco.
Per prima cosa definiamo la nostra classe base.
classe UseCase
includere Dry::Transaction
classe << self
def call(**args)
new.call(**args)
fine
fine
fine
Questo ci aiuterà a passare gli attributi alla transazione UseCase e a utilizzarli. Siamo quindi pronti a ridefinire il nostro Caso d'uso Acquisto.
modulo Casi d'uso
modulo Acquirente
classe Acquisto
def initialize(buyer:, cart:)
@acquirente = acquirente
@carrello = carrello
fine
def call
return unless check_stock
ritorno a meno che non si crei un acquisto
notificare
fine
privato
attr_reader :buyer, :cart
def check_stock
Services::CheckStock.call(cart: cart)
fine
def crea_acquisto
Servizi::CreaAcquisto.call(acquirente: acquirente, carrello: carrello).call
fine
def notificare
Servizi::NotificaAcquirente.call(acquirente: acquirente)
fine
fine
fine
fine
Con le nuove modifiche, possiamo vedere in modo chiaro come vengono definiti i nostri passi e possiamo gestire il risultato di ogni passo con Success() e Failure().
Siamo pronti a richiamare il nostro nuovo caso d'uso nel controllore e a preparare la nostra risposta in base al risultato finale.
classe Controllore
def acquisto
UseCases::Buyer::Purchase.new.call(
acquirente: purchase_params[:buyer],
carrello: purchase_params[:cart]
) do |risultato|
risultato.successo do
...
fine
risultato.fallimento fare
...
fine
fine
...
fine
...
fine
Questo esempio potrebbe essere migliorato ancora di più con le convalide, ma è sufficiente per mostrare la potenza di questo modello.
Conclusioni
Siamo onesti: il Modello di caso d'uso è piuttosto semplice e assomiglia molto a un oggetto servizio, ma questo livello di astrazione può cambiare notevolmente l'applicazione.
Immaginate che un nuovo sviluppatore entri nel progetto e apra la cartella use_cases: come prima impressione avrà un elenco di tutte le funzionalità disponibili nel sistema e, dopo aver aperto un Use Case, vedrà tutti i passaggi necessari per quella funzionalità senza addentrarsi nella logica. Questo senso di ordine e controllo è il principale vantaggio di questo modello.
Portatelo nella vostra cassetta degli attrezzi e forse in futuro ne farete buon uso.