Zoeken…


Don't Repeat Yourself (DRY)

Rails volgt het DRY-principe om schone code te behouden.

Het omvat waar mogelijk het hergebruiken van zoveel mogelijk code in plaats van het dupliceren van vergelijkbare code op meerdere plaatsen (bijvoorbeeld het gebruik van gedeeltelijke). Dit vermindert fouten , houdt uw code schoon en handhaaft het principe van code eenmaal schrijven en vervolgens hergebruiken. Het is ook eenvoudiger en efficiënter om code op één plaats bij te werken dan om meerdere delen van dezelfde code bij te werken. Daardoor wordt uw code modulair en robuuster.

Ook Fat Model, Skinny Controller is DRY, omdat je de code in je model schrijft en in de controller alleen de aanroep doet, zoals:

# 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

Dit helpt ook bij het leiden tot een API-gestuurde structuur waarbij interne methoden verborgen zijn en wijzigingen worden bereikt door parameters op een API-manier door te geven.

Conventie over configuratie

In Rails merk je dat je kijkt naar controllers, views en modellen voor je database.

Om de noodzaak van zware configuratie te verminderen, implementeert Rails regels om het werken met de applicatie te vergemakkelijken. U kunt uw eigen regels definiëren, maar voor het begin (en voor later) is het een goed idee om vast te houden aan conventies die Rails biedt.

Deze conventies versnellen de ontwikkeling, houden uw code beknopt en leesbaar en bieden u een eenvoudige navigatie in uw toepassing.

Conventies verlagen ook de toetredingsdrempels voor beginners. Er zijn zoveel conventies in Rails dat een beginner niet eens iets hoeft te weten, maar er gewoon van kan profiteren in onwetendheid. Het is mogelijk om geweldige applicaties te maken zonder te weten waarom alles is zoals het is.

Bijvoorbeeld

Als u een databasetabel met de naam orders met de primaire sleutel- id , wordt het overeenkomende model order en wordt de controller die alle logica verwerkt, orders_controller genoemd. De weergave is opgesplitst in verschillende acties: als de controller een new en edit heeft, is er ook een new en edit .

Bijvoorbeeld

Om een app te maken, voer je eenvoudig rails new app_name . Dit genereert ongeveer 70 bestanden en mappen die de infrastructuur en basis vormen voor uw Rails-app.

Het bevat:

  • Mappen voor uw modellen (databaselaag), controllers en weergaven
  • Mappen om eenheidstests voor uw toepassing te houden
  • Mappen om uw webactiva zoals Javascript en CSS-bestanden te bewaren
  • Standaardbestanden voor HTTP 400-antwoorden (bestand niet gevonden)
  • Vele anderen

Vet model, magere controller

"Fat Model, Skinny Controller" verwijst naar hoe de M- en C-delen van MVC idealiter samenwerken. Elke niet-responsgerelateerde logica moet namelijk in het model worden opgenomen, idealiter in een mooie, testbare methode. Ondertussen is de 'skinny' controller gewoon een mooie interface tussen het beeld en het model.

In de praktijk kan dit een aantal verschillende soorten refactoring vereisen, maar het komt allemaal neer op één idee: door elke logica te verplaatsen die niet gaat over de reactie op het model (in plaats van de controller), heb je niet alleen hergebruik gepromoot waar mogelijk, maar u hebt het ook mogelijk gemaakt om uw code buiten de context van een verzoek te testen.

Laten we eens kijken naar een eenvoudig voorbeeld. Stel dat u deze code hebt:

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

U kunt dit als volgt wijzigen:

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

Vervolgens kunt u de logica naar uw postmodel verplaatsen, waar deze er mogelijk als volgt uitziet:

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

Pas op voor default_scope

ActiveRecord bevat default_scope , om een model standaard standaard te reiken.

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

De bovenstaande code dient berichten die al zijn gepubliceerd wanneer u een query op het model uitvoert.

Post.all # will only list published posts 

Dat bereik, hoewel onschadelijk ogend, heeft meerdere verborgen neveneffecten die je misschien niet wilt.

default_scope en order

Aangezien u een verklaarde order in de default_scope , roepen order op Post zal worden toegevoegd als extra bestellingen in plaats van overschrijven van de standaard.

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

Dit is waarschijnlijk niet het gedrag dat je wilde; u kunt dit overschrijven door de order uit te sluiten van het bereik

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

default_scope en modelinitialisatie

Zoals bij elke andere ActiveRecord::Relation , zal default_scope de standaardstatus wijzigen van de geïnitialiseerde modellen.

In het bovenstaande voorbeeld heeft Post where(published: true) standaard ingesteld, en dus hebben nieuwe modellen van Post het ook ingesteld.

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

unscoped

default_scope kan nominaal worden gewist door eerst unscoped aan te roepen, maar dit heeft ook bijwerkingen. Neem bijvoorbeeld een soa-model:

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

Standaard queries tegen Post zal worden binnen het bereik van type kolommen met 'Post' . Maar unscoped zal dit samen met je eigen default_scope wissen, dus als je unscoped gebruikt, unscoped je dit ook onthouden.

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

unscoped en Model Associaties

Overweeg een relatie tussen Post en User

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

class User < ApplicationRecord
  has_many :posts
end

Door een individuele User , kunt u de bijbehorende berichten zien:

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

Maar je wilt het wissen default_scope uit de posts relatie, zodat u gebruik unscoped

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

Hiermee wordt zowel de user_id voorwaarde als de default_scope .

Een voorbeeld use-case voor default_scope

Ondanks dit alles zijn er situaties waarin het gebruik van default_scope gerechtvaardigd is.

Overweeg een multi-tenant systeem waarbij meerdere subdomeinen worden bediend vanuit dezelfde applicatie maar met geïsoleerde gegevens. Een manier om dit isolement te bereiken is via default_scope . De nadelen worden in andere gevallen hier kanten.

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

Het enige dat u hoeft te doen, is Tenant.current_id ergens in het begin van het verzoek instellen en elke tabel met tenant_id wordt automatisch zonder extra code bereikt. Het instantiëren van records zal automatisch de tenant-ID erven waaronder ze zijn gemaakt.

Het belangrijkste van deze use-case is dat de scope eenmaal per aanvraag wordt ingesteld en niet verandert. De enige gevallen die u hier niet nodig unscoped , zijn speciale gevallen zoals achtergrondwerkers die buiten een aanvraagbereik vallen.

Je hebt het niet nodig (YAGNI)

Als je 'YAGNI' (je hebt het niet nodig) kunt zeggen over een functie, kun je deze beter niet implementeren. Er kan veel ontwikkeltijd worden bespaard door te focussen op eenvoud. Het implementeren van dergelijke functies kan sowieso tot problemen leiden:

Problemen

Overengineering

Als een product gecompliceerder is dan het moet zijn, is het meer dan ontwikkeld. Gewoonlijk zullen deze "nog niet gebruikte" functies nooit worden gebruikt op de beoogde manier waarop ze zijn geschreven en moeten ze opnieuw worden bewerkt als ze ooit worden gebruikt. Voortijdige optimalisaties, met name prestatie-optimalisaties, leiden vaak tot ontwerpbeslissingen die in de toekomst onjuist blijken te zijn.

Code bloat

Code Bloat betekent onnodig gecompliceerde code. Dit kan bijvoorbeeld gebeuren door abstractie, redundantie of onjuiste toepassing van ontwerppatronen. De codebasis wordt moeilijk te begrijpen, verwarrend en duur om te onderhouden.

Feature Creep

Feep Creep verwijst naar het toevoegen van nieuwe functies die verder gaan dan de kernfunctionaliteit van het product en leiden tot een onnodig hoge complexiteit van het product.

Lange ontwikkeltijd

De tijd die kan worden gebruikt om de benodigde functies te ontwikkelen, wordt besteed aan het ontwikkelen van onnodige functies. Het leveren van het product duurt langer.

Oplossingen

KUS - Houd het simpel, dom

Volgens KISS werken de meeste systemen het beste als ze eenvoudig zijn ontworpen. Eenvoud moet een primair ontwerpdoel zijn om de complexiteit te verminderen. Dit kan bijvoorbeeld worden bereikt door het 'Single Responsibility Principle' te volgen.

YAGNI - Je hebt het niet nodig

Minder is meer. Denk aan elke functie, is het echt nodig? Als je iets kunt bedenken dat het YAGNI is, laat het dan weg. Het is beter om het te ontwikkelen wanneer het nodig is.

Voortdurende refactoring

Het product wordt gestaag verbeterd. Met refactoring kunnen we ervoor zorgen dat het product volgens de beste praktijk wordt uitgevoerd en niet degenereert tot een patch-werk.

Domeinobjecten (geen vetmodellen meer)

"Fat Model, Skinny Controller" is een zeer goede eerste stap, maar het schaalt niet goed zodra je codebase begint te groeien.

Laten we nadenken over de enkele verantwoordelijkheid van modellen. Wat is de enige verantwoordelijkheid van modellen? Is het om bedrijfslogica vast te houden? Is het om niet-respons-gerelateerde logica vast te houden?

Nee. Het is zijn verantwoordelijkheid om de persistentielaag en zijn abstractie te hanteren.

Bedrijfslogica, evenals alle niet-respons-gerelateerde logica en niet-persistentie-gerelateerde logica, moeten in domeinobjecten worden opgenomen.

Domeinobjecten zijn klassen die zijn ontworpen om slechts één verantwoordelijkheid in het domein van het probleem te hebben. Laat je klassen " hun architectuur schreeuwen " voor de problemen die ze oplossen.

In de praktijk moet u streven naar magere modellen, magere weergaven en magere controllers. De architectuur van uw oplossing mag niet worden beïnvloed door het framework dat u kiest.

Bijvoorbeeld

Stel dat u een marktplaats bent die een vaste commissie van 15% aan uw klanten in rekening brengt via Stripe. Als u een vaste commissie van 15% in rekening brengt, betekent dit dat uw commissie verandert afhankelijk van het bedrag van de bestelling, omdat Stripe 2,9% + 30 ¢ in rekening brengt.

Het bedrag dat u als commissie in rekening brengt, moet zijn: amount*0.15 - (amount*0.029 + 0.30) .

Schrijf deze logica niet in het model:

# 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

Zodra u integreert met een nieuwe betaalmethode, kunt u deze functionaliteit niet in dit model schalen.

Ook, zodra je begint te meer business logica te integreren, uw Order zal object beginnen te verliezen cohesie .

Geef de voorkeur aan domeinobjecten, waarbij de berekening van de commissie volledig is geabstraheerd van de verantwoordelijkheid voor aanhoudende bestellingen:

# 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

Het gebruik van domeinobjecten heeft de volgende architecturale voordelen:

  • het is buitengewoon eenvoudig om eenheden te testen, omdat er geen armaturen of fabrieken nodig zijn om de objecten met de logica te instantiëren.
  • werkt met alles dat het bericht accepteert amount .
  • houdt elk domeinobject klein, met duidelijk omschreven verantwoordelijkheden en met een grotere samenhang.
  • gemakkelijk schaalbaar met nieuwe betaalmethoden door toevoeging, geen aanpassing .
  • stopt de neiging om een steeds groter wordend User in elke Ruby on Rails-toepassing te hebben.

Persoonlijk plaats ik graag domeinobjecten in lib . Als u dit doet, vergeet dan niet om het toe te voegen aan autoload_paths :

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

U kunt er ook de voorkeur aan geven domeinobjecten actiegerichter te maken, volgens het Command / Query-patroon. In dergelijke gevallen kan het beter zijn om deze objecten in app/commands te plaatsen, omdat alle app submappen automatisch worden toegevoegd aan het autoload-pad.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow