Sök…


Upprepa inte dig själv (DRY)

För att upprätthålla ren kod följer Rails principen om DRY.

Det innebär att det är möjligt att använda så mycket kod som möjligt snarare än att kopiera liknande kod på flera platser (till exempel med hjälp av partiklar). Detta minskar fel , håller din kod ren och upprätthåller principen om att skriva kod en gång och sedan återanvända den. Det är också lättare och mer effektivt att uppdatera koden på ett ställe än att uppdatera flera delar av samma kod. Således gör din kod mer modulär och robust.

Också Fat Model, Skinny Controller är TOR, eftersom du skriver koden i din modell och i kontrollen bara gör samtalet, som:

# 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

Detta hjälper också till att leda till en API-driven struktur där interna metoder döljs och förändringar uppnås genom att passera parametrar på ett API-sätt.

Konvention över konfiguration

I Rails ser du dig själv titta på styrenheter, vyer och modeller för din databas.

För att minska behovet av tung konfiguration implementerar Rails regler för att underlätta arbetet med applikationen. Du kan definiera dina egna regler men för början (och för senare) är det en bra idé att hålla sig till konventioner som Rails erbjuder.

Dessa konventioner kommer att påskynda utvecklingen, hålla din kod kort och läsbar och gör att du enkelt kan navigera i din applikation.

Konventioner sänker också inträdeshindren för nybörjare. Det finns så många konventioner i Rails att en nybörjare inte ens behöver veta om, utan bara kan dra nytta av i okunnighet. Det är möjligt att skapa fantastiska applikationer utan att veta varför allt är som det är.

Till exempel

Om du har en databastabell som heter orders med det primära nyckel- id , kallas matchningsmodellen order och kontrollenheten som hanterar all logik heter orders_controller . Vyn är uppdelad i olika åtgärder: om kontrollenheten har en new och edit handling finns det också en new och edit .

Till exempel

För att skapa en app kör du helt enkelt rails new app_name . Detta genererar ungefär 70 filer och mappar som innehåller infrastrukturen och grunden för din Rails-app.

Det inkluderar:

  • Mappar för att hålla dina modeller (databaslager), kontroller och vyer
  • Mappar för att hålla enhetstester för din applikation
  • Mappar för att hålla dina webbtillgångar som Javascript och CSS-filer
  • Standardfiler för HTTP 400-svar (dvs. fil hittades inte)
  • Många andra

Fat Model, Skinny Controller

"Fat Model, Skinny Controller" hänvisar till hur M- och C-delarna i MVC idealiskt fungerar tillsammans. Nämligen, alla icke-svar-relaterade logik bör gå i modellen, helst i en trevlig, testbar metod. Samtidigt är den "magra" styrenheten helt enkelt ett trevligt gränssnitt mellan vyn och modellen.

I praktiken kan detta kräva en rad olika typer av refactoring, men allt kommer till en idé: genom att flytta någon logik som inte handlar om svaret på modellen (istället för styrenheten), inte bara har du främjat återanvändning där det är möjligt men du har också gjort det möjligt att testa din kod utanför sammanhanget för en begäran.

Låt oss titta på ett enkelt exempel. Säg att du har kod så här:

def index
  @published_posts = Post.where('published_at <= ?', Time.now)
  @unpublished_posts = Post.where('published_at IS NULL OR published_at > ?', Time.now)
end

Du kan ändra det till detta:

def index
  @published_posts = Post.published
  @unpublished_posts = Post.unpublished
end

Sedan kan du flytta logiken till din inläggsmodell, där den kan se ut så här:

scope :published, ->(timestamp = Time.now) { where('published_at <= ?', timestamp) }
scope :unpublished, ->(timestamp = Time.now) { where('published_at IS NULL OR published_at > ?', timestamp) }

Se upp för default_scope

ActiveRecord innehåller default_scope , för att automatiskt omfatta en modell som standard.

class Post
  default_scope ->{ where(published: true).order(created_at: :desc) }
end

Ovanstående kod kommer att tjäna inlägg som redan har publicerats när du utför någon fråga på modellen.

Post.all # will only list published posts 

Det omfånget, även om det är oskadligt utseende, har flera dolda biverkningar som du kanske inte vill ha.

default_scope och order

Eftersom du förklarade en order i default_scope kallar orderPost kommer att läggas till som ytterligare beställningar i stället för tvingande standard.

Post.order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."created_at" DESC, "posts"."updated_at" DESC

Detta är förmodligen inte det beteende du ville ha; Du kan åsidosätta detta genom att först utesluta order från tillämpningsområdet

Post.except(:order).order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."updated_at" DESC

default_scope och modellinitiering

Som med alla andra ActiveRecord::Relation , kommer default_scope att ändra standardtillståndet för modeller som initieras från det.

I exemplet ovan har Post ställt in where(published: true) som standard, och så kommer nya modeller från Post också att ha det.

Post.new # => <Post published: true>

unscoped

default_scope kan nominellt rensas genom att ringa unscoped först, men detta har också biverkningar. Ta till exempel en STI-modell:

class Post < Document
  default_scope ->{ where(published: true).order(created_at: :desc) }
end

Som standard ska frågor mot Post skopas för att type kolumner som innehåller 'Post' . Men unscoped kommer att rensa detta tillsammans med ditt eget default_scope , så om du använder unscoped måste du komma ihåg att redogöra för detta också.

Post.unscoped.where(type: 'Post').order(updated_at: :desc)

unscoped och modellföreningar

Tänk på ett förhållande mellan Post och User

class Post < ApplicationRecord
  belongs_to :user
  default_scope ->{ where(published: true).order(created_at: :desc) }
end

class User < ApplicationRecord
  has_many :posts
end

Genom att få en individuell User , kan du se inlägg i samband med det:

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]]

Men du vill rensa default_scope från posts relation, så att du använder unscoped

user.posts.unscoped
SELECT "posts".* FROM "posts"

Detta user_id villkor såväl som default_scope .

Ett exempel på användningsfall för default_scope

Trots allt finns det situationer där användning av default_scope är motiverat.

Överväg ett system med flera hyresgäster där flera underdomäner serveras från samma applikation men med isolerade data. Ett sätt att uppnå denna isolering är genom default_scope . Nackdelarna blir i andra fall stora sidor här.

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

Allt du behöver göra är att ställa Tenant.current_id till något tidigt i begäran, och alla tabeller som innehåller tenant_id kommer automatiskt att bli scoped utan någon ytterligare kod. Instantiating records kommer automatiskt att ärva hyresgästen ID som de skapades under.

Det viktiga med detta användningsfall är att räckvidden ställs in en gång per begäran, och det ändras inte. De enda fallen du behöver unscoped här är specialfall som bakgrundsarbetare som kör utanför en begäran.

Du kommer inte att behöva det (YAGNI)

Om du kan säga "YAGNI" (du behöver inte behöva det) om en funktion, bör du inte implementera den. Det kan sparat mycket utvecklingstid genom att fokusera på enkelhet. Implementering av sådana funktioner ändå kan leda till problem:

problem

Overengineering

Om en produkt är mer komplicerad än den måste vara, är den överkonstruerad. Vanligtvis kommer dessa "ännu inte använda" funktioner aldrig att användas på det avsedda sättet de skrivits och måste refactored om de någonsin används. Tidigare optimeringar, särskilt prestationsoptimeringar, leder ofta till designbeslut som kommer att bevisas fel i framtiden.

Code Blat

Code Bloat betyder onödigt komplicerad kod. Detta kan till exempel ske genom abstraktion, redundans eller felaktig tillämpning av designmönster. Kodbasen blir svår att förstå, förvirrande och dyr att underhålla.

Feature Creep

Feature Creep hänvisar till att lägga till nya funktioner som går utöver produktens kärnfunktionalitet och leder till onödigt hög komplexitet hos produkten.

Lång utvecklingstid

Den tid som kan användas för att utveckla nödvändiga funktioner läggs på för att utveckla onödiga funktioner. Produkten tar längre tid att leverera.

lösningar

KISS - Håll det enkelt, dumt

Enligt KISS fungerar de flesta system bäst om de är utformade enkla. Enkelhet bör vara ett primärt designmål för att minska komplexiteten. Det kan uppnås genom att till exempel följa ”Enkeltansvarsprincipen”.

YAGNI - Du kommer inte behöva det

Mindre är mer. Tänk på alla funktioner, är det verkligen nödvändigt? Om du kan tänka på något sätt att det är YAGNI, låt det vara borta. Det är bättre att utveckla det när det behövs.

Kontinuerlig refactoring

Produkten förbättras stadigt. Med refactoring kan vi se till att produkten görs enligt bästa praxis och inte degenererar till ett lapparbete.

Domänobjekt (inga fler feta modeller)

"Fat Model, Skinny Controller" är ett mycket bra första steg, men det skalas inte bra när din codebase börjar växa.

Låt oss tänka på modellernas enda ansvar . Vilket är modellernas enda ansvar? Är det att hålla affärslogik? Är det att ha en icke-svarsrelaterad logik?

Nej. Det är ansvaret för att hantera uthållighetsskiktet och dess abstraktion.

Affärslogik, liksom all icke-svarsrelaterad logik och icke-persistensrelaterad logik, bör gå i domänobjekt.

Domänobjekt är klasser som är utformade för att endast ha ett ansvar inom problemområdet. Låt dina klasser " Skrik deras arkitektur " för de problem de löser.

I praktiken bör du sträva mot magra modeller, magra vyer och magra styrenheter. Arkitekturen för din lösning bör inte påverkas av det ramverk du väljer.

Till exempel

Låt oss säga att du är en marknadsplats som tar ut en fast 15% provision till dina kunder via Stripe. Om du tar ut en fast 15% provision, betyder det att din provision ändras beroende på beställningens belopp eftersom Stripe debiterar 2,9% + 30 ¢.

Det belopp du tar ut som provision ska vara: amount*0.15 - (amount*0.029 + 0.30) .

Skriv inte den här logiken i modellen:

# 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

Så snart du integrerar med en ny betalningsmetod kommer du inte att kunna skala denna funktion i den här modellen.

Dessutom så fort du börjar att integrera mer affärslogik, din Order kommer objektet att börja förlora sammanhållningen .

Föredra domänobjekt, med beräkningen av kommissionen fullständigt abstrakt från ansvaret för bestående order:

# 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

Att använda domänobjekt har följande arkitektoniska fördelar:

  • Det är extremt lätt att enhetstest, eftersom inga inventarier eller fabriker krävs för att instansera föremålen med logiken.
  • arbetar med allt som accepterar meddelandet amount .
  • håller varje domänobjekt litet, med klart definierade ansvar och med högre sammanhållning.
  • skalar enkelt med nya betalningsmetoder genom tillägg, inte modifiering .
  • stoppar tendensen att ha ett ständigt växande User objekt i varje Ruby on Rails-applikation.

Jag gillar personligen att sätta domänobjekt i lib . Om du gör det, kom ihåg att lägga till det på autoload_paths :

# config/application.rb
config.autoload_paths << Rails.root.join('lib')

Du kanske också föredrar att skapa domänobjekt mer handlingsorienterade efter kommandot / frågeställningsmönstret. I sådant fall kan det att placera dessa objekt i app/commands vara en bättre plats eftersom alla app underkataloger automatiskt läggs till på autoload-banan.



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow