One Class per Rails Controller Action med Aldous

Controllers er ofte øye på en Rails applikasjon. Controller handlinger oppblåst til tross for forsøkene på å holde dem tynne, og selv når de ser tynne ut, er det ofte en illusjon. Vi beveger kompleksiteten til forskjellige before_actions, uten å redusere nevnte kompleksitet. Faktisk krever det ofte betydelig graving rundt og mental kompilering for å få en følelse av kontrollflyten av en bestemt handling. 

Etter å ha brukt tjenesteobjekter for en stund i Tuts + dev-teamet, ble det klart at vi kanskje kan bruke noen av de samme prinsippene til kontrollerhandlinger. Vi kom til slutt med et mønster som fungerte bra og presset det inn i Aldous. I dag vil jeg se på Aldous controller-handlinger og fordelene de kan bringe til din Rails-applikasjon.

Saken for å bryte ut hver kontrollør Handling i en klasse

Å bryte ut hver handling i en egen klasse var det første vi tenkte på. Noen av de nyere rammene som Lotus gjør dette ut av esken, og med litt arbeid kan Rails også dra nytte av dette.

Controller handlinger som er en enkelt hvis ... annet uttalelse er en halm mann. Selv beskjeden størrelse apps har mye flere ting enn det, creeping inn i kontrolleren domenet. Det er autentisering, autorisasjon og ulike forretningsregler for kontrollernivå (for eksempel hvis en person går her og de ikke er logget inn, ta dem til påloggingssiden). Noen kontrollerhandlinger kan bli ganske komplekse, og all kompleksitet er fast i kontrollerlaget.

Gitt hvor mye en kontrollerhandling kan være ansvarlig for, virker det bare naturlig at vi innkapsler alt dette inn i en klasse. Vi kan da teste logikken mye lettere, da vi forhåpentligvis vil ha mer kontroll over livets livssyklus. Det vil også gi oss mulighet til å gjøre disse kontrollerens handlingsklasser mye mer sammenhengende (komplekse RESTful-controllere med et komplett tiltak av handlinger har en tendens til å miste sammenholdet ganske raskt). 

Det er andre problemer med Rails-kontroller, som for eksempel spredning av tilstand på kontrollerobjekter via forekomstvariabler, tendensen for komplekse arvshierarkier, for å danne osv. Å skyve kontrollerhandlinger inn i sine egne klasser kan hjelpe oss med å adressere noen av dem også.

Hva å gjøre med den faktiske Rails Controller

Bilde av Mack Male

Uten mye komplisert hacking på Rails-koden, kan vi ikke bli kvitt kontrollører i sin nåværende form. Hva vi kan gjøre er å slå dem inn i boilerplate med en liten mengde kode for å delegere til kontrollerens handlingsklasser. I Aldous ser regulatorer ut slik:

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

Vi inkluderer en modul slik at vi har tilgang til controller_actions metode, og vi angir deretter hvilke handlinger regulatoren skal ha. Internt vil Aldous kartlegge disse handlingene til tilsvarende navngitte klasser i controller_actions / todos_controller mappe. Dette er ikke konfigurert ennå, men kan enkelt gjøres slik, og det er en fornuftig standard.

En grunnleggende Aldous Controller Action

Det første vi må gjøre er å fortelle Rails hvor du finner vår kontrollerhandling (som jeg har nevnt ovenfor), så vi endrer vår app / config / application.rb som så:

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

Vi er nå klar til å skrive Aldous Controller-handlinger. En enkel kan se slik ut:

klasse TodosController :: Indeks < BaseAction def perform build_view(Todos::IndexView) end end

Som du ser, ser det litt ut som et serviceobjekt, som er av design. Konseptuelt en handling er i utgangspunktet en tjeneste, så det er fornuftig for dem å ha et lignende grensesnitt.

Det er imidlertid to ting som umiddelbart ikke er åpenbare:

  • hvor BaseAction kommer fra og hva som er i det
  • hva build_view er

Vi vil dekke BaseAction om kort tid. Men denne handlingen bruker også Aldous vise objekter, som er hvor build_view kommer fra. Vi dekker ikke Aldous se objekter her, og du trenger ikke å bruke dem (selv om du seriøst bør vurdere det). Handlingen din kan lett se slik ut i stedet:

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

Dette er mer kjent, og vi holder oss til dette fra nå av, slik at vi ikke gjør dem til vannet med visningsrelaterte ting. Men hvor kommer kontrollervariabelen fra?

Hva konstruksjonen for en handling ser ut som

La oss snakke om BaseAction som vi så over. Det er Aldous-ekvivalenten av ApplicationController, så det anbefales sterkt at du har en. En bare-bein BaseAction er:

klasse BaseAction < ::Aldous::ControllerAction end

Det arver fra :: Aldous :: ControllerAction og en av de tingene det arver er en konstruktør. Alle Aldous controller-handlinger har samme konstruktør signatur:

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

Hvilke data er direkte tilgjengelig fra kontrolløren

Å være hva de er, vi har tett koblet Aldous handlinger til en kontroller og så de kan gjøre omtrent alt en Rails controller kan gjøre. Åpenbart har du tilgang til kontrolleren forekomsten og kan trekke uansett data du ønsker derfra. Men du ønsker ikke å ringe alt på kontroller-forekomsten - det ville være en dra for vanlige ting som parameter, overskrifter, osv. Så, via en liten Aldous magi, er følgende ting tilgjengelige på handlingen direkte:

  • params
  • overskrifter
  • be om
  • respons
  • kjeks

Og du kan også gjøre flere ting tilgjengelige på samme måte via en initialiserer konfig / initializers / aldous.rb:

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

Mer om Aldous Views eller Ikke

Aldous Controller-handlinger er utformet for å fungere godt med Aldous vise objekter, men du kan velge å ikke bruke visningsobjektene hvis du følger noen enkle regler.

Aldous Controller-handlinger er ikke kontroller, så du må alltid gi hele banen til en visning. Du kan ikke gjøre:

controller.render: index

I stedet må du gjøre:

controller.render template: 'todos / index'

Siden Aldous-handlinger ikke er kontrollører, vil du ikke kunne få instansvariabler fra disse handlingene automatisk til å være tilgjengelige i visningsmaler, så du må oppgi alle dataene som lokalbefolkningen, for eksempel:

controller.render template: 'todos / index', lokalbefolkningen: todos: Todo.all

Ikke deling av tilstand via instansvariabler kan bare forbedre visningskoden din, og heller ikke eksplisitt gjengivelse kommer heller ikke til skade.

En mer kompleks Aldous Controller Action

Bilde av Howard Lake

La oss se på en mer komplisert Aldous-kontrollerhandling og snakke om noen av de andre tingene Aldous gir oss, samt noen av de beste metodene for å skrive Aldous Controller-handlinger.

klasse TodosController :: Oppdater < 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

Nøkkelen her er for utføre Metode for å inneholde alt eller det meste av den relevante kontrollenivålogikken. Først har vi noen få linjer for å håndtere de lokale forutsetningene (det vil si ting som må være sanne for at handlingen skal ha en sjanse til å lykkes). Disse bør alle være en-liners som ligner på hva du ser over. Den eneste stygge ting er "og tilbake" som vi må fortsette å legge til. Dette ville ikke være et problem hvis vi skulle bruke Aldous visninger, men for nå står vi fast med den. 

Hvis den betingede logikken for den lokale forutsetningen blir for kompleks, skal den hentes ut i et annet objekt, som jeg kaller et predikatobjekt på. Slik kan den komplekse logikken enkelt deles og testes. Predikatobjekter kan bli et konsept innen Aldous på et tidspunkt.

Etter at de lokale forutsetningene er håndtert, må vi utføre kjernelogikken til handlingen. Det er to måter å gå om dette. Hvis logikken din er enkel, som den er over, bare utfør den der. Hvis det er mer komplekst, skyv det inn i et serviceobjekt og utfør tjenesten. 

Mesteparten av tiden vår handling er utføre Metoden skal være lik den ovenfor, eller enda mindre kompleks, avhengig av hvor mange lokale forutsetninger du har og muligheten for feil.

Håndtering av sterke parametere

En annen ting du ser i handlingen ovenfor er:

TodosController :: TodoParams.build (params)

Dette er et annet objekt som arver fra en Aldous-baseklasse, og disse er her for at flere handlinger skal kunne dele sterk params logikk. Det ser ut slik:

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

Du leverer params logikken din i en metode og en feilmelding i en annen. Du kan så bare ordne objektet og ringe hente på det for å få de tillatte parametrene. Det kommer tilbake nil i tilfelle feil.

Passerer data til visninger

En annen interessant metode i handlingen klassen ovenfor er:

def default_view_data super.merge (todo: todo) slutten

Når du bruker Aldous å se objekter, er det noe magi som bruker denne metoden, men vi bruker dem ikke, så vi må bare sende den som en lokal ish til enhver visning som vi gir. Basishandlingen overstyrer også denne metoden:

klasse 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

Det er derfor vi må sørge for å bruke super når vi overstyrer det igjen i barnehandlinger.

Håndtering Før Handlinger via Forutsetning Objekter

Alle de ovennevnte ting er bra, men noen ganger har du globale forutsetninger, som må påvirke alle eller de fleste handlinger i systemet (for eksempel vil vi gjøre noe med økten før du utfører handlinger etc.). Hvordan håndterer vi det?

Dette er en god del av grunnen til å ha a BaseAction. Aldous har et konsept for forutsetninger - disse er i utgangspunktet kontrollerhandlinger i alt annet enn navn. Du konfigurerer hvilke handlingsklasser som skal utføres før hver handling i en metode på BaseAction, og Aldous vil automatisk gjøre dette for deg. La oss se:

klasse 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

Vi overstyrer forutsetningsmetoden og leverer klassen av vår forutsetningsobjekt. Dette objektet kan være:

klasse Delt :: SikreUserNotDisabledPrecondition < 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

Ovennevnte forutsetning arver fra BasePrecondition, som er rett og slett:

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

Du trenger ikke virkelig dette, med mindre alle dine forutsetninger må dele kode. Vi lager det bare fordi du skriver BasePrecondition er lettere enn :: Aldous :: Controller :: Handling :: Forutsetning.

Ovennevnte forutsigelse avslutter utførelsen av handlingen siden den gir en visning-Aldous vil gjøre dette for deg. Hvis forutsetningen din ikke gjengir eller omdirigerer noe (for eksempel du bare angir en variabel i økten), vil handlingskoden utføres når alle forutsetningene er ferdige. 

Hvis du vil at en bestemt handling skal være upåvirket av en bestemt forutsetning, bruker vi grunnleggende Ruby for å oppnå dette. Overstyr forutsetning metode i din handling og avvise hvilke forutsetninger du liker:

def precisjoner super.reject | klass | klasse == Delt :: SikreUserNotDisabledPrecondition avslutte

Ikke så forskjellig fra vanlige Rails before_actions, men innpakket i et fint "objektivt" skall.

Feilfrie handlinger

Bilde av Duncan Hull

Den siste tingen å være klar over er at kontrollerhandlinger er feilfrie, akkurat som tjenesteobjekter. Du trenger aldri å redde noen kode i kontrolleren handling utføre metode-Aldous vil håndtere dette for deg. Hvis det oppstår en feil, vil Aldous redde den og bruke default_error_handler å håndtere situasjonen.

De default_error_handler er en metode du kan overstyre på BaseAction. Når du bruker Aldous vise objekter, ser det slik ut:

def default_error_handler (feil) Standard :: ServerErrorView end

Men siden vi ikke er, kan du gjøre dette i stedet:

def default_error_handler (feil) controller.render (mal: 'standard / server_error', status:: internal_server_error, lokalbefolkningen: feil: [feil]) slutt

Så du håndterer de ikke-fatale feilene for handlingen din som lokale forutsetninger, og lar Aldous bekymre seg for de uventede feilene.

Konklusjon

Ved å bruke Aldous kan du erstatte Rails-kontrollerne med mindre, mer sammenhengende objekter som er mye mindre av en svart boks og er mye lettere å teste. Som en bivirkning kan du redusere koblingen i hele applikasjonen din, forbedre hvordan du arbeider med visninger, og fremme gjenbruk av logikk i kontrollerlaget ditt via komposisjon.

Enda bedre, Aldous Controller-handlinger kan eksistere sammen med Vanilla Rails-kontroller uten for mye kodeplikering, så du kan begynne å bruke dem i en eksisterende app du jobber med. Du kan også bruke Aldous Controller-handlinger uten å forplikte seg til å bruke enten vise objekter eller tjenester, med mindre du vil. 

Aldous har gitt oss mulighet til å avkoble vår utviklingshastighet fra størrelsen på søknaden vi jobber med, samtidig som vi gir oss en bedre og mer organisert kodebase i det lange løp. Forhåpentligvis kan det gjøre det samme for deg.