Ruby on Rails
Rota Best Practice
Ricerca…
Non ripeti te stesso (ASCIUTTO)
Per aiutare a mantenere il codice pulito, Rails segue il principio di DRY.
Coinvolge quando è possibile, riutilizzando il maggior numero possibile di codice piuttosto che duplicare codice simile in più punti (ad esempio, utilizzando partial). Questo riduce gli errori , mantiene il tuo codice pulito e applica il principio della scrittura del codice una volta e poi riusandolo. Inoltre, è più facile e più efficiente aggiornare il codice in un'unica posizione piuttosto che aggiornare più parti dello stesso codice. Così rendendo il tuo codice più modulare e robusto.
Anche Fat Model, Skinny Controller è DRY, perché scrivi il codice nel tuo modello e nel controller fai solo la chiamata, come:
# Post model
scope :unpublished, ->(timestamp = Time.now) { where('published_at IS NULL OR published_at > ?', timestamp) }
# Any controller
def index
....
@unpublished_posts = Post.unpublished
....
end
def others
...
@unpublished_posts = Post.unpublished
...
end
Ciò aiuta anche a condurre una struttura basata su API in cui i metodi interni sono nascosti e le modifiche vengono raggiunte attraverso i parametri di passaggio in modo API.
Convenzione sulla configurazione
In Rails, ti trovi a guardare controller, viste e modelli per il tuo database.
Per ridurre la necessità di una configurazione pesante, Rails implementa le regole per semplificare il lavoro con l'applicazione. Puoi definire le tue regole ma per l'inizio (e per dopo) è una buona idea attenersi alle convenzioni offerte da Rails.
Queste convenzioni accelerano lo sviluppo, mantengono il tuo codice conciso e leggibile e ti consentono una facile navigazione all'interno dell'applicazione.
Le convenzioni abbassano anche le barriere all'ingresso per i principianti. Ci sono così tante convenzioni in Rails che un principiante non ha nemmeno bisogno di sapere, ma può trarre beneficio dall'ignoranza. È possibile creare grandi applicazioni senza sapere perché tutto sia così com'è.
Per esempio
Se si ha una tabella di database chiamata orders
con l' id
chiave primaria, il modello corrispondente viene chiamato order
e il controller che gestisce tutta la logica è denominato orders_controller
. La vista è divisa in diverse azioni: se il controllore ha un'azione new
e edit
, c'è anche una vista new
e edit
.
Per esempio
Per creare un'app, esegui semplicemente rails new app_name
. Ciò genererà circa 70 file e cartelle che comprendono l'infrastruttura e le fondamenta della tua app Rails.
Include:
- Cartelle per contenere i modelli (livello database), i controller e le viste
- Cartelle per tenere unit test per la tua applicazione
- Cartelle per conservare le tue risorse web come i file Javascript e CSS
- File predefiniti per risposte 400 HTTP (ad esempio file non trovato)
- Molti altri
Fat Model, Skinny Controller
"Fat Model, Skinny Controller" si riferisce a come le parti M e C di MVC lavorano idealmente insieme. Vale a dire, qualsiasi logica non correlata alla risposta dovrebbe essere inserita nel modello, idealmente in un buon metodo testabile. Nel frattempo, il controller "magro" è semplicemente una bella interfaccia tra la vista e il modello.
In pratica, questo può richiedere una gamma di diversi tipi di refactoring, ma tutto si riduce a un'idea: spostando qualsiasi logica che non riguarda la risposta al modello (invece del controller), non solo hai promosso il riutilizzo dove possibile ma hai anche reso possibile testare il tuo codice al di fuori del contesto di una richiesta.
Diamo un'occhiata ad un semplice esempio. Di 'che hai un codice come questo:
def index
@published_posts = Post.where('published_at <= ?', Time.now)
@unpublished_posts = Post.where('published_at IS NULL OR published_at > ?', Time.now)
end
Puoi cambiarlo con questo:
def index
@published_posts = Post.published
@unpublished_posts = Post.unpublished
end
Quindi, puoi spostare la logica sul tuo modello di post, dove potrebbe apparire come questa:
scope :published, ->(timestamp = Time.now) { where('published_at <= ?', timestamp) }
scope :unpublished, ->(timestamp = Time.now) { where('published_at IS NULL OR published_at > ?', timestamp) }
Attenzione a default_scope
ActiveRecord include default_scope
, per default_scope
automaticamente un modello per impostazione predefinita.
class Post
default_scope ->{ where(published: true).order(created_at: :desc) }
end
Il codice precedente servirà post che sono già pubblicati quando si esegue una query sul modello.
Post.all # will only list published posts
Quel mirino, sebbene dall'aspetto innocuo, ha molteplici effetti collaterali nascosti che potresti non volere.
default_scope
e order
Dato che hai dichiarato un order
in default_scope
, l' order
chiamata su Post
verrà aggiunto come ordine aggiuntivo anziché sostituire il valore predefinito.
Post.order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."created_at" DESC, "posts"."updated_at" DESC
Questo probabilmente non è il comportamento che volevi; puoi sovrascriverlo escludendo prima l' order
dall'ambito
Post.except(:order).order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."updated_at" DESC
default_scope
e inizializzazione del modello
Come con qualsiasi altro ActiveRecord::Relation
, default_scope
modificherà lo stato predefinito dei modelli inizializzati da esso.
Nell'esempio precedente, Post
ha where(published: true)
impostato per impostazione predefinita, quindi anche i nuovi modelli di Post
verranno impostati.
Post.new # => <Post published: true>
unscoped
default_scope
può nominalmente essere ovviato chiamando unscoped
prima, ma questo ha anche effetti collaterali. Prendi, per esempio, un modello STI:
class Post < Document
default_scope ->{ where(published: true).order(created_at: :desc) }
end
Per impostazione predefinita, le query su Post
verranno esaminate per type
colonne contenenti 'Post'
. Ma non unscoped
cancellerà questo insieme al tuo default_scope
, quindi se usi unscoped
devi ricordarti di tener conto anche di questo.
Post.unscoped.where(type: 'Post').order(updated_at: :desc)
unscoped
modello e modello
Considera una relazione tra Post
e User
class Post < ApplicationRecord
belongs_to :user
default_scope ->{ where(published: true).order(created_at: :desc) }
end
class User < ApplicationRecord
has_many :posts
end
Ottenendo un singolo User
, puoi vedere i post relativi ad esso:
user = User.find(1)
user.posts
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' AND "posts"."user_id" = ? ORDER BY "posts"."created_at" DESC [["user_id", 1]]
Ma tu vuoi cancellare il default_scope
dalla relazione dei posts
, quindi usi unscoped
user.posts.unscoped
SELECT "posts".* FROM "posts"
Questo cancella la condizione user_id
e il default_scope
.
Un esempio di caso d'uso per default_scope
Nonostante tutto, ci sono situazioni in cui l'uso di default_scope
è giustificabile.
Si consideri un sistema multi-tenant in cui vengono forniti più sottodomini dalla stessa applicazione ma con dati isolati. Un modo per ottenere questo isolamento è tramite default_scope
. I lati negativi in altri casi diventano dei lati positivi qui.
class ApplicationRecord < ActiveRecord::Base
def self.inherited(subclass)
super
return unless subclass.superclass == self
return unless subclass.column_names.include? 'tenant_id'
subclass.class_eval do
default_scope ->{ where(tenant_id: Tenant.current_id) }
end
end
end
Tutto ciò che devi fare è impostare Tenant.current_id
su qualcosa nella fase iniziale della richiesta, e qualsiasi tabella che contenga tenant_id
verrà automaticamente tenant_id
senza alcun codice aggiuntivo. I record di istanziazione erediteranno automaticamente l'ID tenant in cui sono stati creati.
La cosa importante di questo caso d'uso è che l'ambito viene impostato una volta per richiesta e non cambia. Gli unici casi in cui è necessario unscoped
qui sono casi speciali come gli operatori in background che vengono eseguiti al di fuori dell'ambito di una richiesta.
Non ne hai bisogno (YAGNI)
Se riesci a dire "YAGNI" (non ne avrai bisogno) su una funzione, è meglio non implementarla. È possibile risparmiare molto tempo di sviluppo concentrandosi sulla semplicità. L'implementazione di tali funzionalità può comunque causare problemi:
I problemi
Overengineering
Se un prodotto è più complicato di quanto deve essere, è sovradimensionato. Di solito queste funzionalità "non ancora utilizzate" non verranno mai utilizzate nel modo previsto in cui sono state scritte e devono essere nuovamente refactate se mai vengono utilizzate. Le ottimizzazioni premature, in particolare le ottimizzazioni delle prestazioni, portano spesso a decisioni di progettazione che si riveleranno errate in futuro.
Codice Bloat
Code Bloat significa codice complicato non necessario. Ciò può verificarsi ad esempio per astrazione, ridondanza o applicazione non corretta dei modelli di progettazione. Il codice base diventa difficile da capire, confuso e costoso da mantenere.
Feature Creep
Feature Creep fa riferimento all'aggiunta di nuove funzionalità che vanno oltre la funzionalità principale del prodotto e portano a una complessità inutilmente elevata del prodotto.
Lungo tempo di sviluppo
Il tempo che potrebbe essere utilizzato per sviluppare le caratteristiche necessarie è speso per sviluppare funzionalità non necessarie. Il prodotto richiede più tempo per la consegna.
soluzioni
BACIO - Resta semplice, stupido
Secondo KISS, la maggior parte dei sistemi funziona meglio se sono progettati in modo semplice. La semplicità dovrebbe essere un obiettivo primario di progettazione per ridurre la complessità. Si può ottenere seguendo il "principio della singola responsabilità", ad esempio.
YAGNI - Non ne avrai bisogno
Meno è meglio. Pensa a ogni funzione, è davvero necessaria? Se riesci a pensare ad un modo in cui è YAGNI, lascia perdere. È meglio svilupparlo quando è necessario.
Refactoring continuo
Il prodotto viene costantemente migliorato. Con il refactoring, possiamo fare in modo che il prodotto venga eseguito secondo le migliori pratiche e non degeneri in un lavoro di patch.
Oggetti di dominio (non più modelli di grasso)
"Fat Model, Skinny Controller" è un ottimo primo passo, ma non si adatta bene quando la base di codice inizia a crescere.
Pensiamo alla singola responsabilità dei modelli. Qual è la singola responsabilità dei modelli? È per mantenere la logica di business? È in grado di mantenere una logica non correlata alla risposta?
No. La sua responsabilità è di gestire lo strato di persistenza e la sua astrazione.
La logica aziendale, così come qualsiasi logica non correlata alla risposta e logica non correlata alla persistenza, dovrebbe andare negli oggetti dominio.
Gli oggetti di dominio sono classi progettate per avere una sola responsabilità nel dominio del problema. Lascia che le tue classi " urlino la loro architettura " per i problemi che risolvono.
In pratica, dovresti cercare modelli magri, punti di vista magri e controller magri. L'architettura della tua soluzione non dovrebbe essere influenzata dal framework che stai scegliendo.
Per esempio
Supponiamo che tu sia un marketplace che addebita una commissione fissa del 15% ai tuoi clienti tramite Stripe. Se si addebita una commissione fissa del 15%, ciò significa che la commissione cambia in base all'ammontare dell'ordine poiché Stripe addebita il 2,9% + 30 ¢.
L'importo da addebitare come commissione deve essere: amount*0.15 - (amount*0.029 + 0.30)
.
Non scrivere questa logica nel modello:
# app/models/order.rb
class Order < ActiveRecord::Base
SERVICE_COMMISSION = 0.15
STRIPE_PERCENTAGE_COMMISSION = 0.029
STRIPE_FIXED_COMMISSION = 0.30
...
def commission
amount*SERVICE_COMMISSION - stripe_commission
end
private
def stripe_commission
amount*STRIPE_PERCENTAGE_COMMISSION + STRIPE_FIXED_COMMISSION
end
end
Non appena ti integrerai con un nuovo metodo di pagamento, non potrai scalare questa funzionalità all'interno di questo modello.
Inoltre, non appena inizi a integrare più logiche di business, il tuo oggetto Order
inizierà a perdere coesione .
Preferisci oggetti di dominio, con il calcolo della commissione completamente sottratto alla responsabilità degli ordini persistenti:
# app/models/order.rb
class Order < ActiveRecord::Base
...
# No reference to commission calculation
end
# lib/commission.rb
class Commission
SERVICE_COMMISSION = 0.15
def self.calculate(payment_method, model)
model.amount*SERVICE_COMMISSION - payment_commission(payment_method, model)
end
private
def self.payment_commission(payment_method, model)
# There are better ways to implement a static registry,
# this is only for illustration purposes.
Object.const_get("#{payment_method}Commission").calculate(model)
end
end
# lib/stripe_commission.rb
class StripeCommission
STRIPE_PERCENTAGE_COMMISSION = 0.029
STRIPE_FIXED_COMMISSION = 0.30
def self.calculate(model)
model.amount*STRIPE_PERCENTAGE_COMMISSION
+ STRIPE_PERCENTAGE_COMMISSION
end
end
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def create
@order = Order.new(order_params)
@order.commission = Commission.calculate("Stripe", @order)
...
end
end
L'utilizzo di oggetti di dominio presenta i seguenti vantaggi architettonici:
- è estremamente facile da testare l'unità, in quanto non sono necessarie fixture o fabbriche per istanziare gli oggetti con la logica.
- funziona con tutto ciò che accetta l'
amount
del messaggio. - mantiene ogni oggetto del dominio piccolo, con responsabilità chiaramente definite e con maggiore coesione.
- facilmente scalabile con nuovi metodi di pagamento per aggiunta, non modifica .
- blocca la tendenza ad avere un oggetto
User
sempre crescente in ogni applicazione Ruby on Rails.
Personalmente mi piace mettere oggetti di dominio in lib
. In tal caso, ricordati di aggiungerlo a autoload_paths
:
# config/application.rb
config.autoload_paths << Rails.root.join('lib')
Potresti anche preferire creare oggetti di dominio più orientati all'azione, seguendo il modello Command / Query. In tal caso, inserire questi oggetti in app/commands
potrebbe essere un posto migliore in quanto tutte le sottodirectory app
vengono automaticamente aggiunte al percorso di caricamento automatico.