EN /HU | Belépés

RoR stílusútmutató

Ezek azok a szabályok, amelyeket a Rails-kódunk követ. Két helyről jönnek: a Growing Rails Applications in Practice (makandra) könyvből, és abból a két alkalmazásból, amelyet már rájuk építve üzemeltetünk — a nyilvános oldalból a WarpEngine katalógusmotorral, és a Teletype Orbitból. Semmi itt nem stíluspreferencia. Ha itt Railst írsz, ezeket követed.

Egy mondat, amit érdemes megjegyezni: a nagy alkalmazások nagyok. Ezt nem tudjuk eltüntetni. Amit tehetünk: úgy rendezzük a kódot, hogy a kétszer annyi modell ne jelentsen kétszer annyi problémát.

1. Hova kerül a kód

Minden kódrésznek egy otthona van. Ha nem tudod megnevezni az otthonát, akkor az osztály, amire szükséged van, még nem létezik — hozd létre.

  • Kontroller (app/controllers/) — csak HTTP: paramétereket olvas, form objektumot vagy szolgáltatást hív, rendel vagy átirányít. Üzleti logika nincs benne.
  • Form objektum (app/forms/) — minden olyan űrlap, amely egynél több modellbe ír, vagy amely mögött egyáltalán nincs modell (belépés, pénztár, feltöltés). Sima ActiveModel::Model validációkkal.
  • Szolgáltatás (app/services/) — egy használati eset, egy publikus metódus beszédes névvel (place, issue, revoke, publish). A tranzakciók, döntések és mellékhatások itt élnek. A metódust soha ne nevezd call-nak.
  • Query objektum (app/queries/) — bármi, ami bonyolultabb egy scope-nál: keresés, szűrők, diagramok, riportok. Relációt vagy sorok tömbjét adja vissza.
  • Presenter (app/presenters/) — formázás a nézetnek, hogy a sablonokban ne legyen logika.
  • DTO (app/dtos/) — az az eredmény, amit egy szolgáltatás visszaad. Struct-szerű érték, validációk nélkül.
  • Szerializáló (app/serializers/) — Blueprinter, kizárólag a JSON API-hoz. A HTML-oldalak nézeteket használnak.
  • Modell (app/models/) — asszociációk, validációk, scope-ok.
  • Job (app/jobs/) — aszinkron mellékhatások: levél, aggregálás, webhook-feldolgozás.

Két szabály a rétegek között:

  • Egy szolgáltatás hívhat másik szolgáltatást. Egy modell soha nem hív szolgáltatást.
  • Ha egy modell-callback levelet küldene vagy jogosultságot adna, akkor rossz helyen van. Tedd át egy szolgáltatásba.

2. Kontrollerek

  • Csak a hét szabványos akció létezik: index, show, new, create, edit, update, destroy. Nincs approve, publish, add_to_cart, redeem.
  • Egy új interakció új kontrollert jelent, amelynek az erőforrása maga az interakció:
Ehelyett Ezt írd
CartsController#add_item Store::CartItemsController#create
SubmissionsController#approve Moderation::ApprovalsController#create
ProductsController#publish Studio::PublicationsController#create
ReviewsController#upvote Community::ReviewVotesController#create
SessionsController#logout Accounts::SessionsController#destroy
  • Egy akció három-hat sor. Nulla üzleti logika, és nulla if egy modell állapotára.
  • Minden bemenetet engedélyezőlistázz. A modellattribútumok permit-en mennek át. Azok a szolgáltatásbemenetek, amelyek nem modellattribútumok, form objektumba valók — a „csak két mező" mondattal kezdődik a következő pokoli kontroller.
  • Soha ne hívj constantize-t paraméterre. Előbb ellenőrizd egy engedélyezőlista-konstanshoz (már megvan a Support::Flag::FLAGGABLES és a Community::Follow::FOLLOWABLES).
  • Egyetlen privát metódus az egyetlen ajtó a modellhez. Ahol egy erőforrás felhasználóhoz, kiadóhoz vagy tokenhez van szűkítve, azon a szűkítésen keresztül töltsd be, ne előbb keresd meg és utána ellenőrizd.
  • Az ismétlődő before_action törzsek az alapkontrollerbe valók, lehetőleg kis osztálymakróként. Ugyanannak a „keresd meg a kiadót és engedélyezz" párnak öt másolata öt esély az elcsúszásra.
  • A szolgáltatások saját hibaosztályokat dobnak (Store::CheckoutService::Error, Library::DownloadService::Denied). A kontroller azokat kapja el, soha nem a StandardError-t.
  • A közös válaszformálás — 404, 403, a JSON hibatörzs — egyszer él, az alapkontroller rescue_from blokkjában.

3. Modellek és ActiveRecord

  • Egy invariánst validációban és callbackben mondj ki, ne segédmetódusban. Egy invite.accept! metódust tucatnyi módon meg lehet kerülni (update!, create!, attribútum-beállító). Csak a validációkról és a callbackekről garantált, hogy lefutnak.
  • A kényelmi metódusok jöhetnek. Csak soha ne támaszkodj arra, hogy más kód meghívja őket.
  • Egy callback konzisztensen tartja az adatot. Soha nem csinál üzletet. Slug vagy token generálása, státusz tükrözése, csak olvasható sor őrzése: igen. Levélküldés, jogosultság adása, webhook hívása: nem.
  • Az update_column, az update_columns és az update_all megkerüli a saját szerződésedet. Csak házimunkára használd őket — egy last_used_at érintés, egy számláló, egy kötegelt söprés. Bármi, amit egy ember ténynek nevezne (kié ez, listázva van-e, melyik a fő kép), save/update! hívásokon megy át.
  • Soha ne nyúlj a külvilághoz, mielőtt a sor commitolódott. Egy before_save-ben írt fájl vagy egy before_validation-ben mentett másik rekord árvákat hagy maga után, ha a mentés elbukik. Használj after_commit-et, vagy csináld egy szolgáltatásban, tranzakción belül.
  • Egy értékkészlet a modellen konstansként él, mellette inclusion validációval. A validáció nélküli konstans javaslat, nem szabály — és soha ne másold be az értékeket egy ActiveAdmin-szűrőbe, hivatkozz a konstansra.
  • Az interakcióspecifikus kód nem a mag-modellbe való. Egy validáció, amire csak egy űrlapnak van szüksége, egy virtuális attribútum egyetlen képernyőhöz, egy callback egyetlen használati esethez: az form objektum vagy szolgáltatás.
  • A modulok és concernek nem karcsúsítanak egy modellt. Ezek fájlszervezés; a callbackek és a metódusok továbbra is betöltődnek az osztályba.
  • A default_scope a soft delete szűrőjére való (where(deleted_at: nil)), és semmi másra. Soha ne tegyél order-t default_scope-ba — átszivárog minden asszociációba és aggregátumba, és a végén unscope(:order) hívásokat fogsz írni, hogy kikerülj belőle.
  • Minden modell, amelyhez adminoldal tartozik, explicit ransackable_attributes és ransackable_associations listát deklarál.

4. Nevek és névterek

  • Minden domain egy Ruby-modul, és nincsenek legfelső szintű modellek. Accounts, Store, Library, Studio, Community, Support, Finance, Ops.
  • A modul viszi a tábla-előtagot, így a leképezés automatikus:
RUBY
# app/models/store.rb
module Store
  def self.table_name_prefix = "store_"
end
# Store::Product -> store_products

Ez nem dekoráció: a WarpEngine saját táblái (softwares, releases, images) előtag nélkül ülnek ugyanabban az adatbázisban, tehát az előtag az, ami szerkezetileg megakadályozza az ütközést.

  • A motor szavai foglaltak. A Software, Release, ReleaseAsset, Download, SoftwareImage, ExternalLink, PlatformLink, ApplicationToken, Pipeline a kódunkban soha nem jelent mást. Ahol a bolt fogalma eltér, használj másik szót: az eladható dolog Store::Product, egy fórumszál Community::Topic (a Thread Ruby-osztály), egy tartalombejelentés Support::Flag (a „report" az értékesítési riport).
  • Kerülendő továbbá: Process, Set, Data, Method, File, Dir, Range, Signal, Random, Comparable.
  • Névterezz agresszíven, és a szolgáltatásokat fogalom szerint névterezd, ne utótag szerint: Software::Publish, Rss::BlogFeed, Store::CheckoutService — nem egy lapos mappa tizenhét _service.rb végű fájllal.
  • Használj mindenhol ugyanazt a szerkezetet: a modellek, kontrollerek, nézetek, helperek és specek tükrözik egymást. Egy request spec a spec/requests alatt él, egy model spec a spec/models alatt — még akkor is, ha a fájl máshonnan is futna.
  • A névterezés kétszer térül meg: olvashatóvá teszi a mappát, és nyilvánvaló helyet ad az új osztályoknak — pontosan ez akadályozza meg, hogy a modellek elhízzanak.

5. A WarpEngine határa

  • Minden hoszt-specifikus rész egy adapter alapértékkel, és az alapérték a mai viselkedés. Így marad a motor önmagában is használható, miközben a bolt beteszi a saját szabályait: tárolás, hozzáférési szabályzat, képek, CI, tokenforrás, alanyfeloldás.
  • Bolt-specifikus kód soha nem kerül a motorba. A motor közöl és konfigurál; a hoszt dönt.
  • A motor kifelé ActiveSupport::Notifications eseményekkel beszél, nem a hosztba visszahívó callbackekkel. A hoszt feliratkozik (warp_engine.publish, warp_engine.download).
  • Egy motormodellt a load hookján keresztül bővíts (ActiveSupport.run_load_hooks(:warp_engine_software, self)), soha ne az osztály újranyitásával.
  • A motor URL-jei foglaltak: /api/software*, /api/builds, /api/download, /api/service, /api/auth/*, /api/ci/*, /build/*, /file/*. A saját JSON API-nk verziózott (/api/v1), tehát nem ütközhet. A mount WarpEngine::Engine mindig a routes.rb utolsó sora.
  • Ha szándékosan árnyékolsz egy motorútvonalat, az útvonalhoz komment kell arról, hogy miért, és annak a regressziós specnek a neve, amely védi.
  • Egy motormodellbe úgy írj, ahogy a motor várja: update! hívással, hogy a validációi és callbackjei lefussanak. A motor verziózott függőség — egy oszlopírás, ami ma működik, némán kihagyja azt a callbacket, amit a következő kiadásban kap.
  • Amikor egy seam leváltja a régi kódot, töröld a régi kódot. Miután a tárolóadapter beérkezett, még négy másik hely számolta a fájlok alapútvonalát; kettő közülük már halott volt.

6. Konfiguráció

  • Minden ENV-olvasás egy helyen történik, a config/application.rb fájlban, a config.x alá:
RUBY
config.x.downloads.grant_ttl = 15.minutes
config.x.images.container_path = ENV.fetch("IMAGE_CONTAINER_PATH", "/images")
  • A kód a Rails.configuration.x értékeit olvassa, soha nem közvetlenül az ENV-et. Egyetlen hely mutatja meg, mit vár az alkalmazás a környezetétől, és egy teszt felül tudja írni.
  • Egy dolognak egy neve. Két környezeti változó ugyanarra a wikire, amelyek közül az egyiket a compose-fájl soha nem állítja be, olyan hiba, amely a péntekre vár.
  • A motorbeállítások a motor saját konfigurációs objektumába kerülnek alapértékekkel, a hoszt pedig egyetlen initializerben állítja be őket.
  • Semmi monkey patch initializerből. Az ApplicationController újranyitása egy to_prepare blokkban elrejti a viselkedést az osztályt deklaráló fájl elől — tedd concernbe, és include-old.

7. Nézetek és stílusok

  • Tailwind, és elnevezett osztály mindenre, ami kétszernél többször előfordul. Ha a text-2xl font-semibold 81-szer szerepel, akkor az egy címsorosztály, amely meg akar születni.
  • Szemantikus színtokenek, nem nyers palettanevek. surface, line, muted, accent — így egy témaváltás egy fájl, nem 134 nézet.
  • Egy téma tokencsere. Más blokkok !important-tal való felülírásától válik karbantarthatatlanná egy stíluslap; egy blokk a saját módosítóosztályán keresztül dönti el, hogyan néz ki egy témában.
  • Egy blokk soha nem stílusoz egy másikat. A .sidebar .article {} kizárva; vagy az elem a blokkhoz tartozik (.sidebar__article), vagy a blokk kap egy módosítót (.article.is_summary).
  • Megengedett pragmatikus kivételek: összetett HTML gyerekei (table, dl, ul), :before/:after, pszeudoszelektorok, médiaquery-k, markdown- vagy WYSIWYG-tartalom egyetlen konténeren belül (.wiki-content), és külső könyvtár által generált markup.
  • Nézetben nincs lekérdezés, és includes sem — az adatot a kontroller vagy a szolgáltatás készíti elő. Egy lekérdezés egy partial ciklusában N+1 probléma, extra lépésekkel.
  • Nincs soron belüli style: attribútum ActiveAdmin-oldalakon, ha a projektnek már van adminstíluslapja. Az ismétlődő markup osztályt kap; egy hosszú ikon-case presentert.
  • Az a nézet, amelynek hat példányváltozó kell öt szolgáltatásból, nem show — az egy munkalap, és presentert vagy query objektumot kér.

8. JSON API

  • Blueprinter-szerializálók, camelCase kulcsok, és szerializálók kizárólag az API-hoz — soha nem HTML-oldalakhoz.
  • A nyilvános végpontokat apipie-jal annotáld (api, param, returns, error), felsorolva a válasz tulajdonságait. Az /api/docs és a swagger-oldal ebből épül.
  • Írd le, miért nyilvános egy végpont. A „egy kliensnek nincs kinek belépnie" a végpont leírásába való, nem egy commitüzenetbe.
  • A saját API-nk verziózott. Az a válasz, amelyre egy kliens támaszkodik, verziófejlécet visz, egy szolgáltatásleíró végpont pedig még bármilyen hitelesítő adat előtt megmondja a kliensnek, mit kínál ez a telepítés.
  • Ha a projektben vannak szerializálók, egy kontrollerben kézzel összerakott hash az az út, amelynek a végén ugyanannak a mezőnek két neve lesz egy válaszban.

9. Tesztek

  • Az egységtesztek és a teljes vertikumú tesztek térülnek meg. Ne adj hozzá kontroller- és nézetspecet ahhoz, amit ez a kettő már lefed.
  • A sötét ház szabálya: az alkalmazás egy sötét ház, az integrációs tesztek a háttérvilágítás, az egységtesztek a reflektorok. Nem kell mindenhova sebészi fény — arra van szükség, hogy biztos lehess benne: egy sötét sarokban sem nő semmi.
  • spec/models minden invariánsra: az állapotgép, az utolsó tulajdonos szabálya, a csak olvasható sor, az egyediség, ami számít. Ezek a legolcsóbb és leghosszabb életű tesztek, amiket írni fogsz.
  • A spec/services a súlypont — minden ág, minden hibaút.
  • spec/requests a jogosultságokra, a státuszkódokra és az átirányításokra. A letöltési kapunak kötelező regressziós spece van.
  • Minden képernyőt megérint legalább egy teljes vertikumú teszt. Egy-két út rajta keresztül, nem minden peremeset. Hotwire mellett ez nem opcionális.
  • Egy parancs futtat mindent, külön beállítás nélkül, a tesztadatot pedig maguk a tesztek hozzák létre.
  • Semmi nem kerül commitba bukó vagy függőben lévő teszttel. Amint a tesztkészlet nem zöld, nem tudja többé megmondani, hogy elrontott-e valamit a változtatásod, a léc pedig egyre lejjebb csúszik.
  • A factory-k minimálisak maradnak, traitekkel. Nincs istenfactory, ami az adatbázis felét behúzza.
  • Tartsd unalmasan a teszteket: az okosság a tesztelt kódba való.

10. Gemek és frissítések

  • Rögzíts minden gemet, és írd oda az indoklást minden kockázatos választás mellé. A Gemfile jó hely egy mondatnak, ami megspórol egy délutánt.
  • Nincs git-ágas függőség. Egy branch: master gem azt jelenti, hogy két bundle update között bármi megérkezhet, a lockfile pedig revíziót nevez meg, nem verziót. Ha még nincs kiadás, rögzíts egy ref: értéket, és szándékosan emeld.
  • Nincs :latest konténerimage a CI-ban. Egy build eszközlánca nem változhat commit nélkül.
  • Egy gem hozzáadása azt jelenti, hogy a tiéd lesz: a viselkedése terhelés alatt, a biztonsági frissítései, a frissítése, amikor a Rails továbblép, és a függőségei. Válassz inkább alacsony szintű könyvtárat, mint mini keretrendszert, amely mélyen beleakaszkodik a Rails belsőségeibe.
  • Ne élj a vérző élen. Várd meg, amíg egy főverzió elér néhány javítószintet.
  • Használd ki a meglévő eszközeidet. Egy újabb tábla a Redis előtt, LIKE a keresőszerver előtt, egy cron a sorkezelő előtt — és rejtsd a döntést egy kis API mögé (Article::Search.find), hogy a későbbi csere egyetlen osztályt érintsen.
  • A monkey patchekért frissítéskor fizetsz. Forkolj, javítsd teszttel, és nyiss pull requestet.

11. Hogyan dolgozunk

  • *A terv a repó gyökerében lévő `NOTES_.md` fájlban él** (gitignore-olva), nem commitüzenetek között szétszórva: döntések, alternatívák és a miértek. A kód szakaszra hivatkozva utalhat rá.
  • Repónkénti kommentkonvenció. A teletype-orbit repóban a miértet a nem nyilvánvaló kód mellé írjuk; a teletypegames monorepóban nem használunk magyarázó kommentet, a miért a commitba megy. Mindkettő szándékos — kövesd azt a repót, amelyikben vagy.
  • A kiadói műveleteket táblába auditáld, ne naplófájlba: ki, mit, mikor, melyik rekorddal.
  • A seedek két szinten jönnek: alapadatok, amelyek élesben is lefutnak, és példaadatok fejlesztéshez, amelyek a valódi úton mennek végig (studio, beküldés, jóváhagyás, kosár, fizetési webhook), így a seed a domaint gyakorolja, nem sorokat tol be oldalról.
  • Az új mintákat megítéljük, nem átvesszük. Írj át egy kis részt az új stílusban, hasonlítsd össze a kettőt, és mondd ki hangosan, mi a kompromisszum. A következetesség többet ér egy félúton átvett, marginálisan jobb technikánál.