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.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow