O clasă pe rails controler de acțiune cu Aldous

Controlorii sunt de multe ori ochii unei aplicații Rails. Acțiunile controlerului sunt umflate, în ciuda încercărilor noastre de a le păstra subțiri, și chiar și atunci când arată slabe, este adesea o iluzie. Deplasăm complexitatea la diverse before_actions, fără a reduce această complexitate. De fapt, adesea necesită săpare semnificativă și compilație mentală pentru a obține o senzație pentru fluxul de control al unei acțiuni particulare. 

După ce am folosit obiecte de serviciu pentru o vreme în echipa Tuts + dev, a devenit evident că putem aplica aceleași principii acțiunilor controlerului. În cele din urmă am venit cu un model care a funcționat bine și l-am împins în Aldous. Astăzi mă voi uita la acțiunile controlerului Aldous și la beneficiile pe care acestea le pot aduce aplicației Rails.

Cazul pentru eliminarea fiecărui controler de acțiune într-o clasă

Eliminarea fiecărei acțiuni într-o clasă separată a fost primul lucru pe care l-am gândit. Unele cadre mai noi, cum ar fi Lotus, fac acest lucru din cutie și, cu puțină muncă, Rails ar putea profita și de acest lucru.

Controler acțiuni care sunt un singur dacă ... altfel declarația este un om de paie. Chiar și aplicațiile cu dimensiuni modeste au mult mai multe lucruri decât acestea, intră în domeniul controlerului. Există autentificare, autorizare și diverse reguli de afaceri la nivel de controlor (de exemplu, dacă o persoană merge aici și nu este conectată, duceți-o la pagina de conectare). Unele acțiuni ale controlorilor pot deveni destul de complexe, iar toată complexitatea este ferm în domeniul stratului de controler.

Având în vedere cât de mult poate fi responsabilă o acțiune a controlorului, se pare că este natural să încapsulem toate acestea într-o clasă. Putem apoi testa logica mult mai ușor, deoarece sperăm că vom avea mai mult control asupra ciclului de viață al acelei clase. De asemenea, ne-ar permite să facem aceste clase de acțiune ale controlorilor mult mai coerente (controlerii complexi RESTful cu o completă completă de acțiuni tind să piardă destul de rapid coeziunea). 

Există și alte probleme cu controlorii Rails, cum ar fi proliferarea stării pe obiectele controlerului prin variabilele de instanță, tendința de formare a ierarhiilor complexe de moștenire etc. Acționarea controlerului în propriile clase ne poate ajuta să abordăm și unele dintre ele.

Ce trebuie să faceți cu controlerul efectiv de șine

Imagine de Mack Barbat

Fără o mulțime de hacking complexe pe codul Rails, nu putem scăpa cu adevărat de controlorii în forma lor actuală. Ce putem face este să le transformăm în plăci cu o cantitate mică de cod pentru a le delega la clasele de acțiune ale controlorului. În Aldous, controlorii arată astfel:

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

Avem un modul astfel încât să avem acces la controller_actions metodă, și apoi stabilim ce acțiuni ar trebui să aibă controlorul. Pe plan intern, Aldous va mapa aceste acțiuni la clasele corespunzătoare numite în controller_actions / todos_controller pliant. Acest lucru nu poate fi configurat încă, dar se poate face cu ușurință, și este o implicită sensibilă.

O acțiune de bază a controlerului de bază

Primul lucru pe care trebuie să-l facem este să-i spunem lui Rails unde să găsească acțiunea noastră de controlor (după cum am menționat mai sus), așa că modificăm app / config / application.rb ca astfel:

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

Suntem gata să scriem acțiuni ale controlorilor Aldous. O simplă ar putea arăta astfel:

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

După cum puteți vedea, pare oarecum similară cu un obiect de serviciu, care este proiectat. Conceptual, o acțiune este, în principiu, un serviciu, deci este logic să aibă o interfață similară.

Există totuși două lucruri care sunt imediat nevăzute:

  • Unde BaseAction vine de la și ce este în el
  • ce build_view este

Vom acoperi BaseAction pe scurt. Dar această acțiune utilizează, de asemenea, obiecte de vizualizare Aldous, unde se află build_view vine de la. Nu acoperim aici obiecte Aldous și nu trebuie să le folosiți (deși ar trebui să o consideri serios). Acțiunea dvs. poate arăta cu ușurință în felul acesta:

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

Acest lucru este mai familiar și vom respecta acest lucru de acum încolo, pentru a nu tulbură apele cu lucruri legate de vedere. Dar de unde vine variabila controlerului?

Ce arata Constructorul pentru o actiune

Să vorbim despre BaseAction pe care am văzut-o mai sus. Este echivalentul lui Aldous ApplicationController, așa că este recomandat să aveți unul. O oase goale BaseAction este:

clasa BaseAction < ::Aldous::ControllerAction end

Moșteneste de la :: Aldous :: ControllerAction și unul dintre lucrurile pe care le moștenește este un constructor. Toate acțiunile controlerului Aldous au aceeași semnătură de constructor:

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

Ce date sunt disponibile direct din instanța controlerului

Fiind ceea ce sunt, am legat strâns acțiunile Aldous unui controlor și astfel pot face exact tot ce poate face un controlor Rails. Evident, aveți acces la instanța controlerului și puteți trage din datele pe care le doriți de acolo. Dar nu doriți să apelați totul la instanța controlerului - ar fi o problemă pentru lucruri comune cum ar fi parami, anteturi etc. Deci, printr-o mică magie Aldous, următoarele acțiuni sunt disponibile direct în acțiune:

  • params
  • antete
  • cerere
  • raspuns
  • fursecuri

Și puteți face mai multe lucruri disponibile în același mod prin intermediul unui inițializator config / initializatori / aldous.rb:

Aldous.configuration do | aldous | aldous.controller_methods_exposed_to_action + = [: curent_user] sfârșit

Mai multe despre viziunile Aldous sau nu

Acțiunile controlerului Aldous sunt proiectate să funcționeze bine cu obiectele de vizualizare Aldous, dar puteți opta să nu utilizați obiectele de vizualizare dacă urmați câteva reguli simple.

Acțiunile controlerului Aldous nu sunt controlori, deci trebuie să oferiți întotdeauna calea completă la o vizualizare. Nu puteți face:

controller.render: index

În schimb, trebuie să faceți:

șablonul controller.render: 'todos / index'

De asemenea, deoarece acțiunile Aldous nu sunt controlori, nu veți putea avea variabile de instanță din aceste acțiuni disponibile în mod automat în șabloanele de vizualizare, deci trebuie să furnizați toate datele ca localnici, de exemplu:

template pentru controler.render: 'todos / index', localnici: todos: Todo.all

Nu permiteți partajarea stărilor prin variabilele de instanță poate îmbunătăți numai codul de vizualizare și randarea mai explicită nu va afecta nici prea mult.

O acțiune mai complexă a controlorului Aldous

Imagine de Howard Lake

Să ne uităm la o acțiune mai complexă a controlorului Aldous și să vorbim despre unele dintre celelalte lucruri pe care ni le oferă Aldous, precum și unele dintre cele mai bune practici pentru scrierea acțiunilor controlorilor Aldous.

clasa TodosController :: Actualizare < 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

Cheia este aici pentru a executa metodă pentru a conține toată sau majoritatea logicii relevante la nivel de controlor. Mai întâi avem câteva linii pentru a face față precondițiilor locale (adică lucruri care trebuie să fie adevărate pentru ca acțiunea să aibă chiar șansa de a reuși). Acestea ar trebui să fie toate cu o linie similară cu ceea ce vedeți mai sus. Singurul lucru inestetic este "și întoarcerea" pe care trebuie să continuăm să le adăugăm. Aceasta nu ar fi o problemă dacă vom folosi vederi ale lui Aldous, dar deocamdată suntem blocați. 

Dacă logica condiționată pentru condiția locală devine prea complexă, ar trebui extrasă într-un alt obiect, pe care eu îl numesc un obiect predicat - astfel, logica complexă poate fi ușor partajată și testată. Obiectele predefinite pot deveni un concept în cadrul lui Aldous la un moment dat.

După ce condițiile preliminare sunt tratate, trebuie să realizăm logica de bază a acțiunii. Există două moduri de a face acest lucru. Dacă logica dvs. este simplă, așa cum este mai sus, executați-o chiar acolo. Dacă este mai complexă, împingeți-l într-un obiect serviciu și apoi executați serviciul. 

De cele mai multe ori acțiunea noastră este a executa metoda ar trebui să fie similară celei de mai sus sau chiar mai puțin complexă în funcție de numărul de precondiții locale pe care le aveți și de posibilitatea eșecului.

Manipularea paramelor puternice

Un alt lucru pe care îl vedeți în clasa de acțiune de mai sus este:

TodosController :: TodoParams.build (params)

Acesta este un alt obiect care moștenește de la o clasă de bază Aldous și acestea sunt aici pentru ca mai multe acțiuni să poată partaja logica paramică puternică. Se pare că:

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

Furnizați logica paramelor într-o singură metodă și un mesaj de eroare în altul. Apoi, instanțiați pur și simplu obiectul și apelați-l pe acesta pentru a obține paramurile permise. Se va întoarce zero în caz de eroare.

Transmiterea datelor către vizualizări

O altă metodă interesantă în clasa de acțiune de mai sus este:

def default_view_data super.merge (todo: todo) sfârșit

Când folosiți obiecte de vizualizare Aldous, există o anumită magie care folosește această metodă, dar nu le folosim, așa că trebuie să le transmitem doar ca pe un hash local pentru orice vizualizare pe care o oferim. De asemenea, acțiunea de bază înlocuiește această metodă:

clasa 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

De aceea trebuie să ne asigurăm că vom folosi super când o vom suprascrie din nou în acțiunile copilului.

Manipularea înaintea acțiunilor prin obiecte precondiționate

Toate lucrurile de mai sus sunt minunate, dar uneori aveți condiții prealabile globale, care trebuie să afecteze toate sau majoritatea acțiunilor din sistem (de exemplu, dorim să facem ceva cu sesiunea înainte de a efectua orice acțiune etc.). Cum facem asta??

Aceasta este o bună parte a motivului pentru care ai BaseAction. Aldous are un concept de obiecte precondiționate - acestea sunt, în esență, acțiuni de controler în orice altceva decât numele. Configurați clasele de acțiuni care trebuie executate înainte de fiecare acțiune dintr-o metodă pe BaseAction, iar Aldous va face automat asta pentru tine. Haideți să aruncăm o privire:

clasa 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

Depășim metoda de precondiții și furnizăm clasa obiectului nostru precondiționat. Acest obiect poate fi:

Class shared :: EnsureUserNotDisabledPreconditionare < 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

Precondiția de mai sus moștenește de la BasePrecondition, care este pur și simplu:

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

Nu ai nevoie de asta decât dacă toate precondițiile tale vor trebui să împărtășești un cod. Noi o creăm pur și simplu pentru că scriem BasePrecondition este mai ușor decât :: Aldous :: Controlor :: Acțiune :: Preconditie.

Condiția prealabilă de mai sus întrerupe executarea acțiunii, deoarece face o vizualizare - Aldous o va face pentru dvs. Dacă condiția dvs. preconditivă nu redă sau redirecționează nimic (de exemplu, setați pur și simplu o variabilă în sesiune), atunci codul de acțiune se va executa după ce toate precondițiile se fac. 

Dacă doriți ca o anumită acțiune să nu fie afectată de o anumită condiție prealabilă, folosim Ruby de bază pentru a realiza acest lucru. Împingeți condiție prealabilă în acțiunea dvs. și respingeți oricare dintre condițiile prealabile:

def precondiții super.reject | klass | klass == Shared :: EnsureUserNotDisabled Preconditionare sfârșit

Nu că nu se deosebește de Rails obișnuite before_actions, dar înfășurat într-o coajă de obiect "plăcută".

Acțiuni fără eroare

Imagine de Duncan Hull

Ultimul lucru pe care trebuie să îl cunoașteți este că acțiunile controlerului sunt fără erori, la fel ca și obiectele de serviciu. Nu trebuie să salvați niciun cod în modul de acțiune al controlerului - Aldous se va ocupa de acest lucru pentru dvs. Dacă apare o eroare, Aldous o va salva și o va folosi default_error_handler pentru a face față situației.

default_error_handler este o metodă pe care o puteți suprascrie pe BaseAction. Când se utilizează obiecte de vizualizare Aldous, aceasta arată astfel:

def default_error_handler (eroare) Valori prestabilite :: ServerErrorView sfârșit

Dar din moment ce nu suntem, puteți face acest lucru în schimb:

def default_error_handler (eroare) controller.render (șablon: "defaults / server_error", status:: internal_server_error, localnici: erori: [eroare] sfârșit

Deci, vă gestionați erorile non-fatale pentru acțiunea dvs. ca precondiții locale și lăsați-l pe Aldous să se îngrijoreze de erorile neașteptate.

Concluzie

Folosind Aldous, puteți înlocui controlerele Rails cu obiecte mai mici, mai coerente, care sunt cu mult mai puțin de o cutie neagră și sunt mult mai ușor de testat. Ca efect secundar puteți reduce cuplarea în întreaga aplicație, îmbunătățiți modul în care lucrați cu vizualizări și promovați reutilizarea logicii în stratul dvs. de controler prin compoziție.

Mai bine, acțiunile controlerului Aldous pot coexista cu controlorii de vanilie Rails fără dublarea codului prea mult, astfel încât să puteți începe să le utilizați în orice aplicație existentă cu care lucrați. De asemenea, puteți utiliza acțiunile controlerului Aldous fără a se angaja să utilizați obiecte sau servicii de vizualizare decât dacă doriți. 

Aldous ne-a permis să ne decuplam viteza de dezvoltare de la dimensiunea aplicației pe care lucrăm, oferindu-ne o bază de cod mai bună și mai organizată pe termen lung. Sperăm că poate face același lucru pentru dvs..

Cod