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.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow