Ruby on Rails
Rails Best Practices
Suche…
Wiederholen Sie sich nicht (TROCKEN)
Um den Code sauber zu halten, folgt Rails dem DRY-Prinzip.
Es geht darum, wann immer möglich, so viel Code wie möglich wiederzuverwenden, anstatt ähnlichen Code an mehreren Stellen zu duplizieren (z. B. unter Verwendung von Partials). Dies reduziert Fehler , hält Ihren Code sauber und setzt das Prinzip ein , Code einmal zu schreiben und anschließend wiederzuverwenden. Das Aktualisieren von Code an einer Stelle ist einfacher und effizienter als das Aktualisieren mehrerer Teile desselben Codes. Dadurch wird Ihr Code modularer und robuster.
Auch Fat Model, Skinny Controller ist DRY, weil Sie den Code in Ihr Modell schreiben und im Controller nur den Aufruf ausführen, z.
# 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
Dies führt auch zu einer API-gesteuerten Struktur, bei der interne Methoden verborgen werden und Änderungen durch das Übergeben von Parametern auf eine API-Art erzielt werden.
Konvention über Konfiguration
In Rails finden Sie Controller, Ansichten und Modelle für Ihre Datenbank.
Um den Bedarf an umfangreichen Konfigurationen zu reduzieren, implementiert Rails Regeln, um die Arbeit mit der Anwendung zu erleichtern. Sie können Ihre eigenen Regeln definieren, aber für den Anfang (und für später) ist es eine gute Idee, sich an die von Rails angebotenen Konventionen zu halten.
Diese Konventionen beschleunigen die Entwicklung, halten Ihren Code übersichtlich und lesbar und ermöglichen Ihnen eine einfache Navigation in Ihrer Anwendung.
Konventionen senken auch die Eintrittsbarrieren für Anfänger. Es gibt so viele Konventionen in Rails, von denen ein Anfänger nicht einmal etwas wissen muss, sondern nur aus Unwissenheit Nutzen ziehen kann. Es ist möglich, großartige Anwendungen zu erstellen, ohne zu wissen, warum alles so ist, wie es ist.
Zum Beispiel
Wenn Sie über eine Datenbanktabelle mit der Primärschlüssel- id
orders
verfügen, wird das übereinstimmende Modell als order
und der Controller, der die gesamte Logik verarbeitet, heißt orders_controller
. Die Ansicht ist in verschiedene Aktionen aufgeteilt: Wenn der Controller eine new
und eine edit
hat, gibt es auch eine new
und eine edit
.
Zum Beispiel
Um eine App zu erstellen, führen Sie einfach rails new app_name
. Auf diese Weise werden ungefähr 70 Dateien und Ordner generiert, die die Infrastruktur und Grundlage für Ihre Rails-App bilden.
Es enthält:
- Ordner für Ihre Modelle (Datenbankebene), Controller und Ansichten
- Ordner zum Halten von Komponententests für Ihre Anwendung
- Ordner zum Speichern von Web-Assets wie Javascript- und CSS-Dateien
- Standarddateien für HTTP 400-Antworten (dh Datei wurde nicht gefunden)
- Viele andere
Fettes Modell, dünner Controller
„Fat Model, Skinny Controller“ bezieht sich darauf, wie die M- und C-Teile von MVC ideal zusammenarbeiten. Jede nicht-antwortbezogene Logik sollte in das Modell aufgenommen werden, idealerweise in einer schönen, überprüfbaren Methode. Inzwischen ist der "Skinny" Controller einfach eine schöne Schnittstelle zwischen Ansicht und Modell.
In der Praxis kann dies eine Reihe verschiedener Arten von Refactoring erfordern, aber es kommt auf eine Idee: Durch das Verschieben einer beliebigen Logik, die nicht die Reaktion auf das Modell (anstelle des Controllers) ist, haben Sie nicht nur die Wiederverwendung befördert wo möglich, aber Sie haben es auch ermöglicht, Ihren Code außerhalb des Kontexts einer Anfrage zu testen.
Schauen wir uns ein einfaches Beispiel an. Angenommen, Sie haben folgenden Code:
def index
@published_posts = Post.where('published_at <= ?', Time.now)
@unpublished_posts = Post.where('published_at IS NULL OR published_at > ?', Time.now)
end
Sie können es so ändern:
def index
@published_posts = Post.published
@unpublished_posts = Post.unpublished
end
Dann können Sie die Logik in Ihr Post-Modell verschieben, wo es wie folgt aussehen könnte:
scope :published, ->(timestamp = Time.now) { where('published_at <= ?', timestamp) }
scope :unpublished, ->(timestamp = Time.now) { where('published_at IS NULL OR published_at > ?', timestamp) }
Hüten Sie sich vor default_scope
default_scope
enthält default_scope
, um ein Modell standardmäßig automatisch default_scope
.
class Post
default_scope ->{ where(published: true).order(created_at: :desc) }
end
Der obige Code wird Beiträge bereitstellen, die bereits veröffentlicht werden, wenn Sie eine Abfrage für das Modell ausführen.
Post.all # will only list published posts
Dieser Spielraum wirkt zwar harmlos, hat aber mehrere versteckte Nebeneffekte, die Sie möglicherweise nicht möchten.
default_scope
und order
Da Sie im default_scope
eine order
default_scope
, wird die aufrufende order
bei Post
als zusätzliche Bestellung hinzugefügt, anstatt die Standardeinstellung zu überschreiben.
Post.order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."created_at" DESC, "posts"."updated_at" DESC
Dies ist wahrscheinlich nicht das gewünschte Verhalten. Sie können dies überschreiben, indem Sie die order
aus dem Geltungsbereich ausschließen
Post.except(:order).order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."updated_at" DESC
default_scope
und Modellinitialisierung
Wie bei jedem anderen ActiveRecord::Relation
default_scope
den Standardzustand von Modellen, die von ihm initialisiert werden.
In dem obigen Beispiel hat Post
standardmäßig where(published: true)
festgelegt, sodass neue Modelle von Post
ebenfalls festgelegt werden.
Post.new # => <Post published: true>
unscoped
default_scope
kann nominell gelöscht werden, unscoped
zuerst unscoped
wird. Dies hat jedoch auch Nebenwirkungen. Nehmen Sie zum Beispiel ein STI-Modell:
class Post < Document
default_scope ->{ where(published: true).order(created_at: :desc) }
end
Standardmäßig Abfragen für Post
werden scoped werden , um type
- Spalten mit 'Post'
. unscoped
wird dies jedoch zusammen mit Ihrem eigenen default_scope
. Wenn Sie also unscoped
verwenden, unscoped
Sie dies ebenfalls berücksichtigen.
Post.unscoped.where(type: 'Post').order(updated_at: :desc)
unscoped
und Model Associations
Betrachten Sie eine Beziehung zwischen Post
und User
class Post < ApplicationRecord
belongs_to :user
default_scope ->{ where(published: true).order(created_at: :desc) }
end
class User < ApplicationRecord
has_many :posts
end
Indem Sie einen einzelnen User
, können Sie die dazugehörigen Beiträge sehen:
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]]
Sie möchten jedoch den default_scope
aus der posts
Relation unscoped
, sodass Sie nicht mit einem Bereich versehene unscoped
user.posts.unscoped
SELECT "posts".* FROM "posts"
Dadurch werden die user_id
Bedingung sowie der default_scope
.
Ein Anwendungsbeispiel für default_scope
Trotzdem gibt es Situationen, in denen die Verwendung von default_scope
vertretbar ist.
Stellen Sie sich ein Mehrmandanten-System vor, bei dem mehrere Subdomains von derselben Anwendung aus mit isolierten Daten bedient werden. Eine Möglichkeit, diese Isolation zu erreichen, ist default_scope
. Die Nachteile in anderen Fällen werden hier zu einem Nachteil.
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
Alles, was Sie tun müssen, ist, Tenant.current_id
zu einem frühen Tenant.current_id
in der Anforderung Tenant.current_id
, und jede Tabelle, die tenant_id
enthält, wird automatisch ohne zusätzlichen Code festgelegt. Durch das Instanziieren von Datensätzen wird automatisch die Mandanten-ID übernommen, unter der sie erstellt wurden.
Das Wichtigste an diesem Anwendungsfall ist, dass der Gültigkeitsbereich einmal pro Anforderung festgelegt wird und sich nicht ändert. Die einzigen Fälle , die Sie benötigen unscoped
hier sind Spezialfälle wie Hintergrund Arbeitnehmer , die außerhalb eines Ersuchens Umfang ausgeführt werden .
Du wirst es nicht brauchen (YAGNI)
Wenn Sie über ein Feature „YAGNI“ (Sie werden es nicht brauchen) sagen können, sollten Sie es besser nicht implementieren. Durch die Fokussierung auf Einfachheit kann viel Entwicklungszeit eingespart werden. Die ohnehin vorhandene Implementierung solcher Funktionen kann zu Problemen führen:
Probleme
Übertechnik
Wenn ein Produkt komplizierter ist als es sein muss, ist es überentwickelt. Normalerweise werden diese "noch nicht verwendeten" Funktionen niemals in der vorgesehenen Art und Weise verwendet, in der sie geschrieben wurden, und müssen umgestaltet werden, wenn sie jemals verwendet werden. Vorzeitige Optimierungen, insbesondere Leistungsoptimierungen, führen häufig zu Designentscheidungen, die sich in der Zukunft als falsch herausstellen.
Code aufgebläht
Code Bloat bedeutet unnötig komplizierten Code. Dies kann beispielsweise durch Abstraktion, Redundanz oder fehlerhafte Anwendung von Entwurfsmustern geschehen. Die Codebasis wird schwer verständlich, verwirrend und teuer in der Wartung.
Feature Kriechen
Feature Creep bezieht sich auf das Hinzufügen neuer Features, die über die Kernfunktionalität des Produkts hinausgehen und zu einer unnötig hohen Komplexität des Produkts führen.
Lange Entwicklungszeit
Die Zeit, die zum Entwickeln notwendiger Merkmale verwendet werden könnte, wird aufgewendet, um nicht benötigte Merkmale zu entwickeln. Die Lieferung dauert länger.
Lösungen
KISS - Mach es einfach, dumm
KISS zufolge funktionieren die meisten Systeme am besten, wenn sie einfach gestaltet sind. Einfachheit sollte ein vorrangiges Designziel sein, um die Komplexität zu reduzieren. Dies kann erreicht werden, indem zum Beispiel das Prinzip der Einzelverantwortung befolgt wird.
YAGNI - Du wirst es nicht brauchen
Weniger ist mehr. Denken Sie über jedes Feature nach, ist es wirklich nötig? Wenn Sie sich vorstellen können, dass es sich um YAGNI handelt, lassen Sie es weg. Es ist besser, es zu entwickeln, wenn es benötigt wird.
Kontinuierliches Refactoring
Das Produkt wird ständig verbessert. Mit Refactoring können wir sicherstellen, dass das Produkt nach bester Vorgehensweise erstellt wird und nicht zu einer Patch-Arbeit degeneriert.
Domänenobjekte (keine fetten Modelle)
"Fat Model, Skinny Controller" ist ein sehr guter erster Schritt, skaliert jedoch nicht, sobald Ihre Codebase zu wachsen beginnt.
Denken wir über die Einzelverantwortung von Modellen nach. Was ist die alleinige Verantwortung von Modellen? Soll es Geschäftslogik geben? Soll es eine nicht antwortbezogene Logik geben?
Nein. Ihre Verantwortung besteht darin, mit der Persistenzschicht und ihrer Abstraktion umzugehen.
Geschäftslogik sowie jede nicht antwortbezogene Logik und nicht persistente Logik sollte in Domänenobjekte verankert sein.
Domänenobjekte sind Klassen, die nur eine Verantwortung in der Domäne des Problems haben. Lassen Sie Ihre Klassen für die von ihnen gelösten Probleme " Scream Their Architecture ".
In der Praxis sollten Sie nach dünnen Modellen, dünnen Ansichten und dünnen Controllern streben. Die Architektur Ihrer Lösung sollte nicht durch das von Ihnen gewählte Framework beeinflusst werden.
Zum Beispiel
Angenommen, Sie sind ein Marktplatz, der Ihren Kunden eine feste Provision von 15% über Stripe berechnet. Wenn Sie eine feste Provision von 15% berechnen, ändert sich Ihre Provision abhängig von der Höhe der Bestellung, da Stripe 2,9% + 30 ¢ berechnet.
Der Betrag, den Sie als Provision berechnen, sollte amount*0.15 - (amount*0.029 + 0.30)
: amount*0.15 - (amount*0.029 + 0.30)
.
Schreiben Sie diese Logik nicht in das Modell:
# 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
Sobald Sie eine neue Zahlungsmethode integriert haben, können Sie diese Funktionalität innerhalb dieses Modells nicht skalieren.
Sobald Sie beginnen, mehr Geschäftslogik zu integrieren, verliert Ihr Order
Objekt den Zusammenhalt .
Bevorzugen Sie Domain-Objekte, wobei die Berechnung der Provision vollständig von der Verantwortung für persistierende Aufträge abhängt:
# 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
Die Verwendung von Domänenobjekten hat die folgenden architektonischen Vorteile:
- Der Komponententest ist extrem einfach, da keine Fixtures oder Factorys erforderlich sind, um die Objekte mit der Logik zu instanziieren.
- arbeitet mit allem, was die Meldung akzeptiert
amount
. - hält jedes Domänenobjekt klein, mit klar definierten Verantwortlichkeiten und höherer Kohäsion.
- Skaliert leicht mit neuen Zahlungsmethoden durch Hinzufügen, nicht Modifizieren .
- stoppt die Tendenz, in jeder Ruby on Rails-Anwendung ein ständig wachsendes
User
zu haben.
Ich persönlich mag Domänenobjekte in lib
. Wenn Sie dies tun, denken Sie daran, es zu autoload_paths
hinzuzufügen:
# config/application.rb
config.autoload_paths << Rails.root.join('lib')
Sie können auch bevorzugen, Domänenobjekte nach dem Befehls- / Abfragemuster aktionsorientierter zu erstellen. In einem solchen Fall empfiehlt es sich, diese Objekte in app/commands
zu platzieren, da alle app
Unterverzeichnisse automatisch zum Autoload-Pfad hinzugefügt werden.