Azione di una classe per controller di rotaie con Aldous

I controller sono spesso il pugno nell'occhio di un'applicazione Rails. Le azioni del controller sono gonfie nonostante i nostri tentativi di tenerle magre, e anche quando sembrano magre, è spesso un'illusione. Spostiamo la complessità in vari before_actions, senza ridurre detta complessità. Infatti, spesso richiede un significativo scavo e una compilazione mentale per avere un'idea del flusso di controllo di una determinata azione. 

Dopo aver utilizzato gli oggetti di servizio per un po 'nel team di sviluppo di Tuts +, è diventato evidente che potremmo essere in grado di applicare alcuni degli stessi principi alle azioni del controller. Alla fine arrivammo a un modello che funzionava bene e lo spinse su Aldous. Oggi esaminerò le azioni del controller Aldous e i vantaggi che possono apportare all'applicazione Rails.

Il caso di interrompere ogni azione del controller in una classe

Spezzare ogni azione in una classe separata è stata la prima cosa che abbiamo pensato. Alcuni dei nuovi framework come Lotus lo fanno subito, e con un po 'di lavoro Rails potrebbe anche approfittare di questo.

Azioni del controllore che sono un singolo se altro la dichiarazione è un uomo di paglia. Anche le app di modeste dimensioni hanno molto più roba del genere, insinuandosi nel dominio del controller. Esiste un'autenticazione, un'autorizzazione e varie regole aziendali a livello di controller (ad esempio se una persona va qui e non ha effettuato l'accesso, portali alla pagina di accesso). Alcune azioni del controller possono diventare piuttosto complesse e tutta la complessità è saldamente nel regno del livello controller.

Data la quantità di azione di un controllore può essere responsabile, sembra naturale che incapsuliamo tutto ciò in una classe. Possiamo quindi testare la logica molto più facilmente, poiché speriamo di avere un maggiore controllo del ciclo di vita di quella classe. Ci permetterebbe anche di rendere queste classi di azione dei controller molto più coese (i controller RESTful complessi con un completo complemento di azioni tendono a perdere la coesione abbastanza rapidamente). 

Ci sono altri problemi con i controller Rails, come la proliferazione di stati sugli oggetti controller tramite variabili di istanza, la tendenza a creare gerarchie di ereditarietà complesse, ecc. Spingere le azioni del controllore nelle proprie classi può aiutarci ad affrontare anche alcune di esse.

Cosa fare con il controller delle guide reali

Immagine di Mack Male

Senza un sacco di intrusioni complesse sul codice Rails, non possiamo davvero sbarazzarci dei controllori nella loro forma attuale. Quello che possiamo fare è trasformarli in un fornello con una piccola quantità di codice da delegare alle classi d'azione del controllore. In Aldous, i controller si presentano così:

classe TodosController < ApplicationController include Aldous::Controller controller_actions :index, :new, :create, :edit, :update, :destroy end

Includiamo un modulo in modo che possiamo accedere a controller_actions metodo, e quindi stabiliamo quali azioni il controller dovrebbe avere. Internamente, Aldous mapperà queste azioni a classi con nomi corrispondenti nel controller_actions / todos_controller cartella. Questo non è ancora configurabile, ma può essere facilmente realizzato in questo modo, ed è un default sensato.

Un'Azione di controller Aldous di base

La prima cosa che dobbiamo fare è dire a Rails dove trovare l'azione del nostro controller (come ho menzionato sopra), quindi modifichiamo il nostro app / config / application.rb così:

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

Ora siamo pronti per scrivere azioni di controller Aldous. Un semplice potrebbe assomigliare a questo:

class TodosController :: Index < BaseAction def perform build_view(Todos::IndexView) end end

Come puoi vedere, sembra in qualche modo simile a un oggetto di servizio, che è di progettazione. Concettualmente un'azione è fondamentalmente un servizio, quindi ha senso per loro avere un'interfaccia simile.

Vi sono, tuttavia, due cose che sono immediatamente non ovvi:

  • dove BaseAction viene da e cosa c'è dentro
  • che cosa build_view è

Copriremo BaseAction in breve. Ma questa azione utilizza anche oggetti vista Aldous, che è dove build_view viene da. Non stiamo coprendo gli oggetti vista Aldous qui e non devi usarli (anche se dovresti considerarlo seriamente). La tua azione può facilmente apparire come questa:

class TodosController :: Index < BaseAction def perform controller.render template: 'todos/index', locals:  end end

Questo è più familiare e ci atteniamo a questo d'ora in poi, in modo da non confondere le acque con le cose relative alla vista. Ma da dove viene la variabile del controller?

Che aspetto ha il Costruttore per un'azione

Parliamo del BaseAction che abbiamo visto sopra. È l'equivalente Aldous di ApplicationController, quindi è fortemente raccomandato che tu ne abbia uno. Un nudo BaseAction è:

classe BaseAction < ::Aldous::ControllerAction end

Da esso eredita :: :: Aldous ControllerAction e una delle cose che eredita è un costruttore. Tutte le azioni del controller Aldous hanno la stessa firma del costruttore:

attr_reader: controller def initialize (controller) @controller = controller end

Quali dati sono disponibili direttamente dall'istanza del controller

Essendo quello che sono, abbiamo stretto strettamente le azioni di Aldous a un controller e quindi possono fare praticamente tutto ciò che un controller Rails può fare. Ovviamente hai accesso all'istanza del controller e puoi prelevare tutti i dati che desideri da lì. Ma non vuoi chiamare tutto sull'istanza del controller, sarebbe un trascinamento per cose comuni come parametri, intestazioni, ecc. Quindi, tramite un po 'di magia Aldous, le seguenti cose sono disponibili direttamente sull'azione:

  • params
  • intestazioni
  • richiesta
  • risposta
  • biscotti

E puoi anche rendere disponibili più cose allo stesso modo tramite un inizializzatore config / inizializzatori / aldous.rb:

Aldous.configuration do | aldous | aldous.controller_methods_exposed_to_action + = [: current_user] end

Altro su Aldous Views o Not

Le azioni di controller Aldous sono progettate per funzionare correttamente con gli oggetti di visualizzazione Aldous, ma puoi scegliere di non utilizzare gli oggetti di visualizzazione se segui alcune semplici regole.

Le azioni del controller Aldous non sono controller, quindi è necessario fornire sempre il percorso completo per una vista. Non puoi fare:

controller.render: index

Invece devi fare:

modello controller.render: 'todos / index'

Inoltre, poiché le azioni Aldous non sono controller, non sarai in grado di avere automaticamente variabili di istanza da queste azioni nei modelli di visualizzazione, quindi devi fornire tutti i dati come locali, ad esempio:

modello controller.render: 'todos / index', gente del posto: todos: Todo.all

Non condividere lo stato tramite le variabili di istanza può solo migliorare il tuo codice di visualizzazione, e un rendering più esplicito non guasta troppo.

Un'azione di controller Aldous più complesso

Immagine di Howard Lake

Diamo un'occhiata ad un'azione di controller Aldous più complessa e parliamo di alcune delle altre cose che ci offre Aldous, oltre ad alcune delle migliori pratiche per scrivere azioni del controller Aldous.

classe TodosController :: Aggiornamento < BaseAction def default_view_data super.merge(todo: todo) end def perform controller.render(template: 'home/show', locals: default_view_data) and return unless current user controller.render(template: 'defaults/bad_request', locals: errors: [todo_params.error_message]) and return unless todo_params.fetch controller.render(template: 'todos/not_found', locals: default_view_data.merge(todo_id: params[:id])) and return unless todo controller.render(template: 'default/forbidden', locals: default_view_data) and return unless current_ability.can?(:update, todo) if todo.update_attributes(todo_params.fetch) controller.redirect_to controller.todos_path else controller.render(template: 'todos/edit', locals: default_view_data) end end private def todo @todo ||= Todo.where(id: params[:id]).first end def todo_params TodosController::TodoParams.build(params) end end

La chiave qui è per il eseguire metodo per contenere tutto o la maggior parte della logica di livello controller pertinente. Per prima cosa abbiamo poche righe per gestire le precondizioni locali (cioè cose che devono essere vere perché l'azione abbia anche una possibilità di successo). Questi dovrebbero essere tutti one-liner simile a quello che vedi sopra. L'unica cosa sgradevole è il 'e il ritorno' che dobbiamo continuare ad aggiungere. Questo non sarebbe un problema se dovessimo utilizzare le visualizzazioni di Aldous, ma per ora ci siamo bloccati. 

Se la logica condizionale per la precondizione locale diventa troppo complessa, dovrebbe essere estratta in un altro oggetto, che io chiamo un oggetto predicato, in questo modo la logica complessa può essere facilmente condivisa e testata. Gli oggetti predicati possono diventare un concetto all'interno di Aldous ad un certo punto.

Dopo aver gestito le precondizioni locali, è necessario eseguire la logica di base dell'azione. Ci sono due modi per farlo. Se la tua logica è semplice, come sopra, eseguila proprio lì. Se è più complesso, inserirlo in un oggetto servizio e quindi eseguire il servizio. 

Il più delle volte è la nostra azione eseguire il metodo dovrebbe essere simile a quello sopra, o anche meno complesso a seconda di quante precondizioni locali si hanno e la possibilità di fallimento.

Gestione di Params forti

Un'altra cosa che vedi nella suddetta classe d'azione è:

TodosController :: TodoParams.build (params)

Questo è un altro oggetto che eredita da una classe di base Aldous, e questi sono qui in modo che più azioni siano in grado di condividere la logica dei parametri forti. Sembra così:

class TodosController :: TodoParams < Aldous::Params def permitted_params params.require(:todo).permit(:description, :user_id) end def error_message 'Missing param :todo' end end

Fornisci la logica params in un metodo e un messaggio di errore in un altro. È quindi sufficiente creare un'istanza dell'oggetto e richiamare il recupero su di esso per ottenere i parametri consentiti. Ritornerà zero in caso di errore.

Passaggio dei dati alle viste

Un altro metodo interessante nella classe di azione sopra è:

def default_view_data super.merge (todo: todo) end

Quando usi gli oggetti di visualizzazione Aldous, c'è un po 'di magia che usa questo metodo, ma non li stiamo usando, quindi dobbiamo semplicemente passarlo come un hash locale a qualsiasi vista che rendiamo. Anche l'azione di base sovrascrive questo metodo:

classe BaseAction < ::Aldous::ControllerAction def default_view_data  current_user: current_user, current_ability: current_ability,  end def current_user @current_user ||= FindCurrentUserService.perform(session).user end def current_ability @current_ability ||= Ability.new(current_user) end end

Questo è il motivo per cui dobbiamo assicurarci di usare super quando lo sovrascriviamo nelle azioni secondarie.

Gestione prima delle azioni tramite oggetti di precondizione

Tutte le cose di cui sopra sono grandiose, ma a volte hai delle precondizioni globali, che devono influenzare tutte o la maggior parte delle azioni nel sistema (ad es. Vogliamo fare qualcosa con la sessione prima di eseguire qualsiasi azione, ecc.). Come lo gestiamo?

Questa è una buona parte della ragione per avere un BaseAction. Aldous ha un concetto di oggetti di precondizione - queste sono fondamentalmente azioni del controllore in tutto tranne che nel nome. Configurare quali classi di azioni devono essere eseguite prima di ogni azione in un metodo su BaseAction, e Aldous lo farà automaticamente per te. Diamo un'occhiata:

classe BaseAction < ::Aldous::ControllerAction def preconditions [Shared::EnsureUserNotDisabledPrecondition] end def current_user @current_user ||= FindCurrentUserService.perform(session).user end def current_ability @current_ability ||= Ability.new(current_user) end end

Sostituiamo il metodo delle precondizioni e forniamo la classe del nostro oggetto di precondizione. Questo oggetto potrebbe essere:

class Shared :: GuaranteeUserNotDisabledPrecondition < BasePrecondition delegate :current_user, :current_ability, to: :action def perform if current_user && current_user.disabled && !current_ability.can?(:manage, :all) controller.render template: 'default/forbidden', status: :forbidden, locals: errors: ['Your account has been disabled'] end end end

La precondizione di cui sopra eredita da BasePrecondition, che è semplicemente:

classe BasePrecondition < ::Aldous::Controller::Action::Precondition end

Non hai davvero bisogno di questo a meno che tutte le tue precondizioni non debbano condividere del codice. Lo creiamo semplicemente perché scriviamo BasePrecondition è più facile di :: Azione Aldous :: :: :: controller Presupposto.

Il precondizionamento di cui sopra termina l'esecuzione dell'azione poiché rende una vista-Aldous lo farà per te. Se la condizione preliminare non esegue il rendering o il reindirizzamento di alcun elemento (ad es. È sufficiente impostare una variabile nella sessione), il codice azione verrà eseguito dopo che tutte le condizioni preliminari sono state completate. 

Se vuoi che un'azione particolare non sia influenzata da una precondizione particolare, usiamo Ruby di base per ottenere questo risultato. Ignora il condizione indispensabile metodo nella tua azione e rifiuta qualsiasi condizione preliminare che ti piace:

precondizioni def super.reject | klass | klass == Shared :: EnsureUserNotDisabledPrecondition end

Non è molto diverso dal normale Rails before_actions, ma avvolto in una bella conchiglia "obiettiva".

Azioni senza errori

Immagine di Duncan Hull

L'ultima cosa da tenere presente è che le azioni del controllore sono prive di errori, proprio come gli oggetti di servizio. Non è mai necessario salvare alcun codice nell'azione del controller per eseguire il metodo. Aldous gestirà ciò per te. Se si verifica un errore, Aldous lo salverà e utilizzerà il default_error_handler per gestire la situazione.

Il default_error_handler è un metodo che puoi sovrascrivere su BaseAction. Quando si usano oggetti vista Aldous, appare come questo:

def default_error_handler (errore) Defaults :: Fine ServerErrorView

Ma dal momento che non lo siamo, puoi farlo invece:

def default_error_handler (error) controller.render (template: 'defaults / server_error', stato:: internal_server_error, locals: errors: [error]) end

Quindi gestisci gli errori non fatali per la tua azione come precondizioni locali e lascia che Aldous si preoccupi degli errori imprevisti.

Conclusione

Usando Aldous puoi sostituire i tuoi controller Rails con oggetti più piccoli, più compatti, che sono molto meno di una scatola nera e sono molto più facili da testare. Come effetto collaterale è possibile ridurre l'accoppiamento nell'intera applicazione, migliorare il modo in cui si lavora con le viste e promuovere il riutilizzo della logica nel livello controller tramite composizione.

Ancora meglio, le azioni del controller Aldous possono coesistere con i controller di vanilla Rails senza troppa duplicazione del codice, quindi è possibile iniziare a utilizzarle in qualsiasi app esistente con cui si sta lavorando. È inoltre possibile utilizzare le azioni del controller Aldous senza impegnarsi a utilizzare gli oggetti o i servizi di visualizzazione, a meno che non lo si voglia. 

Aldous ci ha permesso di disaccoppiare la nostra velocità di sviluppo dalla dimensione dell'applicazione a cui stiamo lavorando, dandoci al contempo una base di codice migliore e più organizzata a lungo termine. Spero che possa fare lo stesso per te.