Ruby on Rails
Najlepsze praktyki w Railsach
Szukaj…
Don't Repeat Yourself (DRY)
Aby pomóc w utrzymaniu czystego kodu, Rails przestrzega zasady DRY.
W miarę możliwości wiąże się to z ponownym użyciem jak największej liczby kodów, a nie powielaniem podobnego kodu w wielu miejscach (na przykład przy użyciu częściowych). Zmniejsza to liczbę błędów , utrzymuje kod w czystości i egzekwuje zasadę jednokrotnego pisania kodu, a następnie jego ponownego użycia. Aktualizowanie kodu w jednym miejscu jest również łatwiejsze i wydajniejsze niż aktualizowanie wielu części tego samego kodu. Dzięki temu Twój kod jest bardziej modułowy i niezawodny.
Również Fat Model, Skinny Controller jest SUCHY, ponieważ piszesz kod w swoim modelu, a w kontrolerze wykonujesz tylko wywołanie, takie jak:
# 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
Pomaga to również prowadzić do struktury opartej na API, w której metody wewnętrzne są ukryte, a zmiany są osiągane poprzez przekazywanie parametrów w sposób API.
Konwencja o konfiguracji
W Railsach przeglądasz kontrolery, widoki i modele swojej bazy danych.
Aby zmniejszyć potrzebę ciężkiej konfiguracji, Railsy implementują reguły ułatwiające pracę z aplikacją. Możesz zdefiniować własne reguły, ale na początek (i później) dobrym pomysłem jest trzymanie się konwencji, które oferuje Rails.
Konwencje te przyspieszą rozwój, utrzymają zwięzły i czytelny kod oraz umożliwią łatwą nawigację wewnątrz aplikacji.
Konwencje również obniżają bariery wejścia dla początkujących. W Railsach jest tak wiele konwencji, że początkujący nie musi nawet wiedzieć, ale może po prostu skorzystać z niewiedzy. Możliwe jest tworzenie świetnych aplikacji bez wiedzy, dlaczego wszystko jest tak, jak jest.
Na przykład
Jeśli masz tabelę bazy danych o nazwie orders
z id
klucza podstawowego, pasujący model nazywa się order
a kontroler, który obsługuje całą logikę, nosi nazwę orders_controller
. Widok jest podzielony na różne akcje: jeśli kontroler ma new
akcję edit
i edit
, istnieje również new
widok edit
.
Na przykład
Aby stworzyć aplikację, po prostu uruchom rails new app_name
. Spowoduje to wygenerowanie około 70 plików i folderów, które stanowią infrastrukturę i fundament aplikacji Rails.
Obejmuje:
- Foldery do przechowywania modeli (warstwa bazy danych), kontrolerów i widoków
- Foldery do przechowywania testów jednostkowych dla Twojej aplikacji
- Foldery do przechowywania zasobów internetowych, takich jak Javascript i pliki CSS
- Domyślne pliki dla odpowiedzi HTTP 400 (tzn. Nie znaleziono pliku)
- Wiele innych
Model tłuszczu, chudy kontroler
„Gruby model, chudy kontroler” odnosi się do idealnej współpracy części M i C MVC. Mianowicie, każda logika niezwiązana z odpowiedzią powinna iść w modelu, najlepiej w ładnej, sprawdzalnej metodzie. Tymczasem „chudy” kontroler to po prostu ładny interfejs między widokiem a modelem.
W praktyce może to wymagać szeregu różnych rodzajów refaktoryzacji, ale wszystko sprowadza się do jednego pomysłu: przesuwając dowolną logikę, która nie dotyczy odpowiedzi na model (zamiast kontrolera), nie tylko promujesz ponowne użycie tam gdzie to możliwe, ale umożliwiłeś także przetestowanie kodu poza kontekstem żądania.
Spójrzmy na prosty przykład. Powiedz, że masz taki kod:
def index
@published_posts = Post.where('published_at <= ?', Time.now)
@unpublished_posts = Post.where('published_at IS NULL OR published_at > ?', Time.now)
end
Możesz to zmienić na to:
def index
@published_posts = Post.published
@unpublished_posts = Post.unpublished
end
Następnie możesz przenieść logikę do swojego modelu postu, gdzie może to wyglądać tak:
scope :published, ->(timestamp = Time.now) { where('published_at <= ?', timestamp) }
scope :unpublished, ->(timestamp = Time.now) { where('published_at IS NULL OR published_at > ?', timestamp) }
Uważaj na default_scope
ActiveRecord zawiera domyślny zakres, aby automatycznie domyślnie default_scope
model.
class Post
default_scope ->{ where(published: true).order(created_at: :desc) }
end
Powyższy kod będzie obsługiwał posty, które zostały już opublikowane podczas wykonywania dowolnego zapytania w modelu.
Post.all # will only list published posts
Ten zakres, choć nieszkodliwy, ma wiele ukrytych skutków ubocznych, których możesz nie chcieć.
default_scope
i order
Ponieważ zadeklarowałeś order
w zakresie default_scope
, wywoływanie order
na Post
zostanie dodane jako dodatkowe zamówienia zamiast zastępowania domyślnego.
Post.order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."created_at" DESC, "posts"."updated_at" DESC
Prawdopodobnie nie jest to pożądane zachowanie; możesz to zmienić, wykluczając najpierw order
z zakresu
Post.except(:order).order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."updated_at" DESC
default_scope
i inicjalizacja modelu
Jak w przypadku każdego innego ActiveRecord::Relation
, default_scope
zmieni domyślny stan zainicjowanych z niego modeli.
W powyższym przykładzie Post
ustawił where(published: true)
, a więc nowe modele Post
również to ustawią.
Post.new # => <Post published: true>
unscoped
default_scope
można nominalnie wyczyścić, wywołując najpierw unscoped
, ale ma to również skutki uboczne. Weźmy na przykład model STI:
class Post < Document
default_scope ->{ where(published: true).order(created_at: :desc) }
end
Domyślnie zapytania skierowane do Post
będą miały zakres, aby type
kolumny zawierające 'Post'
. Ale unscoped
wyczyści to wraz z twoim default_scope
, więc jeśli używasz unscoped
, musisz pamiętać, aby to uwzględnić.
Post.unscoped.where(type: 'Post').order(updated_at: :desc)
unscoped
i modelowe
Rozważ związek między Post
a User
class Post < ApplicationRecord
belongs_to :user
default_scope ->{ where(published: true).order(created_at: :desc) }
end
class User < ApplicationRecord
has_many :posts
end
Pozyskając konkretnego User
, możesz zobaczyć powiązane z nim posty:
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]]
Ale chcesz wyczyścić default_scope
z relacji posts
, więc używasz unscoped
user.posts.unscoped
SELECT "posts".* FROM "posts"
user_id
warunek user_id
, a także default_scope
zakres.
Przykładowy przypadek użycia dla default_scope
Mimo to istnieją sytuacje, w których użycie parametru default_scope
jest uzasadnione.
Rozważ system z wieloma dzierżawcami, w którym wiele subdomen obsługiwanych jest z tej samej aplikacji, ale z izolowanymi danymi. Jednym ze sposobów osiągnięcia tej izolacji jest default_scope
. Wady w innych przypadkach stają się tutaj zaletami.
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
Wszystko, co musisz zrobić, to ustawić Tenant.current_id
na coś na wczesnym etapie żądania, a każda tabela zawierająca tenant_id
zostanie automatycznie tenant_id
bez żadnego dodatkowego kodu. Rekordy tworzenia instancji automatycznie dziedziczą identyfikator najemcy, pod którym zostały utworzone.
Ważną rzeczą w tym przypadku użycia jest to, że zakres jest ustawiany raz na żądanie i nie zmienia się. Jedynymi przypadkami, których będziesz potrzebować w unscoped
miejscu, są specjalne przypadki, takie jak pracownicy działający w tle, którzy działają poza zakresem żądania.
Nie potrzebujesz go (YAGNI)
Jeśli możesz powiedzieć „YAGNI” (nie będziesz go potrzebować) o funkcji, lepiej jej nie implementuj. Dzięki skupieniu się na prostocie można zaoszczędzić dużo czasu na programowanie. Wdrożenie takich funkcji może jednak prowadzić do problemów:
Problemy
Nadmierna inżynieria
Jeśli produkt jest bardziej skomplikowany, niż musi być, jest przerobiony. Zazwyczaj te „jeszcze nieużywane” funkcje nigdy nie będą używane w zamierzony sposób i muszą zostać ponownie przetworzone, jeśli kiedykolwiek zostaną wykorzystane. Przedwczesne optymalizacje, zwłaszcza optymalizacje wydajności, często prowadzą do decyzji projektowych, które w przyszłości okażą się błędne.
Kod wzdęcia
Code Bloat oznacza niepotrzebny skomplikowany kod. Może to nastąpić na przykład poprzez abstrakcję, redundancję lub nieprawidłowe zastosowanie wzorców projektowych. Baza kodu staje się trudna do zrozumienia, myląca i kosztowna w utrzymaniu.
Pełzanie funkcji
Pełzanie funkcji odnosi się do dodawania nowych funkcji, które wykraczają poza podstawową funkcjonalność produktu i prowadzą do niepotrzebnie wysokiej złożoności produktu.
Długi czas rozwoju
Czas, który można wykorzystać na opracowanie niezbędnych funkcji, poświęca się na opracowanie niepotrzebnych funkcji. Dostarczenie produktu trwa dłużej.
Rozwiązania
KISS - Niech to będzie proste, głupie
Według KISS większość systemów działa najlepiej, jeśli są zaprojektowane w prosty sposób. Prostota powinna być głównym celem projektowania w celu zmniejszenia złożoności. Można to osiągnąć na przykład poprzez przestrzeganie „zasady pojedynczej odpowiedzialności”.
YAGNI - Nie będziesz go potrzebował
Mniej znaczy więcej. Pomyśl o każdej funkcji, czy jest ona naprawdę potrzebna? Jeśli możesz wymyślić jakiś sposób, że to YAGNI, zostaw to. Lepiej jest go rozwijać, gdy jest potrzebny.
Ciągłe refaktoryzacja
Produkt jest stale ulepszany. Dzięki refaktoryzacji możemy upewnić się, że produkt jest wytwarzany zgodnie z najlepszymi praktykami i nie ulega degeneracji do łatki.
Obiekty domeny (koniec z modelami tłustymi)
„Gruby model, chudy kontroler” to bardzo dobry pierwszy krok, ale nie skaluje się dobrze, gdy baza kodów zaczyna rosnąć.
Zastanówmy się nad jednolitą odpowiedzialnością modeli. Jaka jest jedyna odpowiedzialność modeli? Czy to ma podtrzymywać logikę biznesową? Czy ma logikę niezwiązaną z odpowiedzią?
Nie. Jego zadaniem jest obsługa warstwy trwałości i jej abstrakcji.
Logika biznesowa, jak również logika niezwiązana z odpowiedzią i logika niezwiązana z trwałością, powinna przejść do obiektów domeny.
Obiekty domeny to klasy zaprojektowane tak, aby ponosić tylko jedną odpowiedzialność w dziedzinie problemu. Pozwól swoim klasom „ krzyczeć na ich architekturę ” w poszukiwaniu problemów, które rozwiązują.
W praktyce powinieneś dążyć do chudych modeli, chudych widoków i chudych kontrolerów. Wybrana struktura nie powinna mieć wpływu na architekturę rozwiązania.
Na przykład
Załóżmy, że jesteś rynkiem, który pobiera od klientów stałą prowizję w wysokości 15% za pośrednictwem Stripe. Jeśli naliczasz stałą 15% prowizję, oznacza to, że prowizja zmienia się w zależności od kwoty zamówienia, ponieważ Stripe pobiera 2,9% + 30 centów.
Kwota pobierana jako prowizja powinna wynosić: amount*0.15 - (amount*0.029 + 0.30)
.
Nie pisz tej logiki w modelu:
# 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
Po zintegrowaniu z nową metodą płatności nie będzie można skalować tej funkcji w tym modelu.
Ponadto, gdy tylko zaczniesz integrować więcej logiki biznesowej, obiekt Order
zacznie tracić spójność .
Preferuj obiekty domenowe, przy obliczaniu prowizji całkowicie oddzielonej od odpowiedzialności za utrzymywanie zamówień:
# 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
Korzystanie z obiektów domeny ma następujące zalety architektoniczne:
- testowanie jednostkowe jest niezwykle łatwe, ponieważ nie są wymagane żadne urządzenia ani fabryki do tworzenia instancji obiektów za pomocą logiki.
- działa ze wszystkim, co akceptuje
amount
wiadomości. - utrzymuje każdy obiekt w domenie mały, z jasno określonymi obowiązkami i większą spójnością.
- łatwo skalować za pomocą nowych metod płatności przez dodanie, a nie modyfikację .
- powstrzymuje tendencję do posiadania stale rosnącego obiektu
User
w każdej aplikacji Ruby on Rails.
Osobiście lubię umieszczać obiekty domeny w lib
. Jeśli to zrobisz, pamiętaj, aby dodać go do autoload_paths
:
# config/application.rb
config.autoload_paths << Rails.root.join('lib')
Możesz także chcieć tworzyć obiekty domeny bardziej zorientowane na działanie, zgodnie z wzorcem Polecenie / Zapytanie. W takim przypadku umieszczenie tych obiektów w app/commands
może być lepszym miejscem, ponieważ wszystkie podkatalogi app
są automatycznie dodawane do ścieżki automatycznego ładowania.