One Class per Rails Controller Action med Aldous

Controllers är ofta ögonen på en Rails applikation. Kontrolleråtgärder är uppblåsta trots våra försök att hålla dem skinniga, och även när de ser mager ut är det ofta en illusion. Vi flyttar komplexiteten till olika before_actions, utan att reducera nämnda komplexitet. Faktum är att det ofta kräver betydande grävning och mental kompilering för att få en känsla för kontrollflödet av en viss åtgärd. 

Efter att ha använt serviceobjekt ett tag i Tuts + dev-teamet blev det uppenbart att vi kanske kan tillämpa några av samma principer för kontrolleråtgärder. Vi kom till slut med ett mönster som fungerade bra och drev det till Aldous. Idag ser jag på Aldous Controller-åtgärderna och de fördelar som de kan få till din Rails-applikation.

Fallet för att bryta ut varje kontrollör Åtgärd i en klass

Att bryta ut varje åtgärd i en separat klass var det första vi tänkte på. Några av de nyare ramarna som Lotus gör det här ur lådan, och med lite arbete kan Rails också dra nytta av detta.

Controller handlingar som är en enda om annat uttalande är en halm man. Även apparater med blygsam storlek har mycket mer grejer än det som kryper in i domänkontrollanten. Det finns autentisering, auktorisering och olika regleringsregler på regleringsnivå (till exempel om en person går här och de inte är inloggade, ta dem till inloggningssidan). Vissa kontrolleråtgärder kan bli ganska komplexa, och all komplexitet är fast i regimens lager.

Med tanke på hur mycket en kontrolleråtgärd kan vara ansvarig för, verkar det bara naturligt att vi inkapslar allt detta till en klass. Vi kan sedan testa logiken mycket lättare, eftersom vi förhoppningsvis skulle ha större kontroll över den klassens livscykel. Det skulle också göra det möjligt för oss att göra dessa kontroller åtgärdsklasser mycket mer sammanhängande (komplexa RESTful controllers med ett komplett komplement av åtgärder tenderar att förlora sammanhållning ganska snabbt). 

Det finns andra problem med Rails-kontroller, såsom statens spridning på kontrollerns objekt via instansvariabler, tendensen för komplexa arvshierarkier att bildas etc. Genom att ställa in kontrolleråtgärder i sina egna klasser kan vi också adressera några av dem också.

Vad ska man göra med den faktiska spårkontrollen

Bild av Mack Male

Utan mycket komplicerat hacking på Rails-koden kan vi inte riktigt bli av med kontroller i sin nuvarande form. Vad vi kan göra är att göra dem till boilerplate med en liten mängd koden för att delegera till kontrolleraktivitetsklasserna. I Aldous ser regulatorer ut så här:

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

Vi inkluderar en modul så att vi har tillgång till controller_actions metod, och vi anger sedan vilka åtgärder regulatorn ska ha. Internt kommer Aldous kartlägga dessa åtgärder till motsvarande namngivna klasser i controller_actions / todos_controller mapp. Det här är inte konfigurerbart ännu, men det går enkelt att göra det, och det är en förnuftig standard.

En grundläggande Aldous Controller Action

Det första vi behöver göra är att berätta Rails om att hitta vår controller-åtgärd (som jag har nämnt ovan), så vi ändrar vår app / config / application.rb såhär:

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

Vi är nu redo att skriva Aldous controller-åtgärder. En enkel kan se ut så här:

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

Som du kan se ser det lite ut som ett serviceobjekt, vilket är av design. Konceptuellt är en handling i princip en tjänst, så det är vettigt att de har ett liknande gränssnitt.

Det finns emellertid två saker som omedelbart inte är uppenbara:

  • var BaseAction kommer ifrån och vad som är i det
  • Vad build_view är

Vi kommer att täcka BaseAction inom kort. Men denna åtgärd använder också Aldous visa objekt, vilket är var build_view kommer från. Vi täcker inte Aldous visa objekt här och du behöver inte använda dem (även om du seriöst bör överväga det). Din åtgärd kan enkelt se ut så här istället:

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

Detta är mer bekant, och vi kommer att hålla fast vid det här från och med nu, för att inte lera vattnet med synrelaterade saker. Men var kommer regulatorvariabeln från?

Vad konstruktionen för en åtgärd ser ut som

Låt oss prata om BaseAction som vi såg ovan. Det är Aldous motsvarigheten till ApplicationController, så det rekommenderas starkt att du har en. En barben BaseAction är:

klass BaseAction < ::Aldous::ControllerAction end

Det ärar från :: Aldous :: ControllerAction och en av de saker som den ärver är en konstruktör. Alla Aldous controller-åtgärder har samma konstruktorsignatur:

attr_reader: controller def initiera (controller) @controller = kontroller slutet

Vilka data är direkt tillgängliga från Controller Instance

Att vara vad de är, vi har tätt kopplat Aldous handlingar till en controller och så kan de göra nästan allt som en Rails controller kan göra. Självklart har du tillgång till kontrollerns förekomst och kan dra vilken data du vill ha därifrån. Men du vill inte ringa allt på kontrollerns instans - det skulle vara ett drag för vanliga saker som parameter, rubriker osv. Så, via en liten Aldous magi finns följande saker tillgängliga direkt på åtgärden:

  • params
  • headers
  • begäran
  • svar
  • småkakor

Och du kan också göra fler saker tillgängliga på samma sätt via en initializer config / initializers / aldous.rb:

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

Mer om Aldous Views eller inte

Aldous controller-åtgärder är utformade för att fungera bra med Aldous visa objekt, men du kan välja att inte använda visningsobjekten om du följer några enkla regler.

Aldous controller-åtgärder är inte controllers, så du måste alltid ge hela vägen till en vy. Du kan inte göra:

controller.render: index

I stället måste du göra:

controller.render mall: 'todos / index'

Eftersom Aldous-åtgärder inte är kontroller kan du inte få instansvariabler från dessa åtgärder automatiskt att vara tillgängliga i visningsmallarna, så du måste tillhandahålla all data som lokalbefolkningen, t.ex.

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

Om du inte delar tillstånd via instansvariabler kan du bara förbättra visningskoden, och det kommer inte heller att skada dig för mycket uttryckligt.

En mer komplex Aldous Controller Action

Bild av Howard Lake

Låt oss titta på en mer komplicerad Aldous-kontrollerad åtgärd och prata om några av de andra saker som Aldous ger oss, liksom några av de bästa metoderna för att skriva Aldous controller-åtgärder.

klass TodosController :: Uppdatering < 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

Nyckeln här är för utföra metod för att innehålla alla eller de flesta av de relevanta logikerna på kontrollernivå. Först har vi några linjer för att hantera de lokala förutsättningarna (dvs saker som måste vara sanna för att åtgärden ska få en chans att lyckas). Dessa borde alla vara linjer som liknar vad du ser ovan. Den enda fula saken är "och tillbaka" som vi måste fortsätta lägga till. Det här skulle inte vara ett problem om vi skulle använda Aldous visningar, men för tillfället har vi fastnat med det. 

Om den villkorliga logiken för den lokala förutsättningen blir för komplex, bör den extraheras till ett annat objekt som jag kallar ett predikatobjekt, så att den komplexa logiken lätt kan delas och testas. Predikatobjekt kan bli ett begrepp inom Aldous vid någon tidpunkt.

När de lokala förutsättningarna hanteras måste vi utföra kärnlogiken i åtgärden. Det finns två sätt att gå om detta. Om din logik är enkel, så som den är ovan, kör den bara där. Om det är mer komplext, tryck det in i ett serviceobjekt och kör sedan tjänsten. 

Merparten av tiden vår åtgärd är utföra Metoden ska likna den ovanstående eller till och med mindre komplex beroende på hur många lokala förutsättningar du har och risken för misslyckande.

Hantering av starka parametrar

En annan sak som du ser i ovanstående actionklass är:

TodosController :: TodoParams.build (params)

Detta är ett annat objekt som ärar från en Aldous-basklass, och dessa är här för att flera åtgärder ska kunna dela starka parametrarlogik. Det ser ut så här:

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

Du levererar din params logik i en metod och ett felmeddelande i en annan. Därefter instanserar du objektet och ringer hämtas på det för att få tillåtna parametrar. Det kommer att återvända noll vid fel.

Överför data till synpunkter

En annan intressant metod i handlingsklassen ovan är:

def default_view_data super.merge (todo: todo) slut

När du använder Aldous visa objekt finns det någon magi som använder den här metoden, men vi använder dem inte, så vi behöver helt enkelt överföra den som en lokal ish till någon vy som vi gör. Basåtgärden åsidosätter också denna metod:

klass 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 är därför vi måste se till att använda super när vi åsidosätter det igen i barnåtgärder.

Hantering före åtgärder via förbehållsobjekt

Alla ovanstående saker är bra, men ibland har du globala förutsättningar som behöver påverka alla eller de flesta åtgärderna i systemet (till exempel vill vi göra något med sessionen innan du utför någon åtgärd etc.). Hur hanterar vi det?

Det här är en bra del av anledningen till att ha en BaseAction. Aldous har ett begrepp av förbehållsobjekt - det här är i grunden kontrollerande åtgärder i allt annat än namn. Du konfigurerar vilka handlingsklasser som ska utföras före varje åtgärd i en metod på BaseAction, och Aldous gör det automatiskt för dig. Låt oss ta en titt:

klass 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 åsidosätter förutsättningsmetoden och levererar klassen av vårt preconditionobjekt. Detta objekt kan vara:

klass delad :: Se till att användaren inte är aktiverad < 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

Ovanstående förutsättning ärverv från BasePrecondition, vilket är helt enkelt:

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

Du behöver verkligen inte detta om inte alla dina förutsättningar måste dela någon kod. Vi skapar det helt enkelt för att skriva BasePrecondition är lättare än :: Aldous :: Controller :: Action :: Förutsättning.

Ovanstående förutsättning avslutar verkställighetens genomförande eftersom det gör en uppfattning-Aldous kommer att göra det här för dig. Om din förutsättning inte ger eller omdirigerar någonting (t ex du bara ställer in en variabel i sessionen) kommer åtgärdskoden att utföras när alla förutsättningar är färdiga. 

Om du vill att en viss åtgärd inte påverkas av en viss förutsättning använder vi grundläggande Ruby för att uppnå detta. Åsidosätta förutsättning metod i din handling och avvisa vilka förutsättningar du gillar:

def precitions super.reject | klass | klass == Delat :: Se till attUserNotDisabledPrecondition slutar

Inte så annorlunda än vanliga Rails before_actions, men inslaget i ett snyggt "objektivt" skal.

Fel-fria åtgärder

Bild av Duncan Hull

Det sista att vara medveten om är att kontrolleråtgärderna är felfri, precis som serviceobjekt. Du behöver aldrig rädda någon kod i kontrollenhetens verkställighetsmetod. Aldous hanterar detta för dig. Om ett fel uppstår, kommer Aldous att rädda det och utnyttja default_error_handler att hantera situationen.

De default_error_handler är en metod som du kan åsidosätta på din BaseAction. När du använder Aldous View-objekt ser det ut så här:

def default_error_handler (error) Standard :: ServerErrorView slutet

Men eftersom vi inte är det kan du göra det istället:

def default_error_handler (error) controller.render (mall: "default / server_error", status:: internal_server_error, lokalbefolkningen: fel: [error]) slutet

Så du hanterar de icke-dödliga felen för din verksamhet som lokala förutsättningar, och låt Aldous oroa sig för de oväntade felen.

Slutsats

Med Aldous kan du ersätta dina Rails-kontroller med mindre, mer sammanhängande föremål som är mycket mindre av en svart låda och är mycket enklare att testa. Som en bieffekt kan du minska kopplingen genom hela din applikation, förbättra hur du arbetar med visningar och främja återanvändning av logik i ditt styrlager via komposition.

Ännu bättre kan Aldous Controller-åtgärder existera med Vanilla Rails-controllers utan att för mycket kod duplicering, så du kan börja använda dem i alla befintliga appar du arbetar med. Du kan också använda Aldous controller-åtgärder utan att begå att använda antingen visa objekt eller tjänster om du inte vill. 

Aldous har gjort det möjligt för oss att koppla bort vår utvecklingshastighet från storleken på den ansökan vi jobbar på, samtidigt som vi ger oss en bättre och mer organiserad kodbas på lång sikt. Förhoppningsvis kan det göra detsamma för dig.