Recherche…


Ne te répète pas (DRY)

Pour aider à maintenir un code propre, Rails suit le principe de DRY.

Cela implique, autant que possible, de réutiliser autant de code que possible plutôt que de dupliquer un code similaire à plusieurs endroits (par exemple, en utilisant des partiels). Cela réduit les erreurs , maintient votre code propre et applique le principe d' écriture du code une fois , puis de le réutiliser. Il est également plus facile et plus efficace de mettre à jour le code au même endroit que de mettre à jour plusieurs parties du même code. Ainsi, votre code est plus modulaire et robuste.

Aussi Fat Model, Skinny Controller est DRY, car vous écrivez le code dans votre modèle et dans le contrôleur ne faites que l'appel, comme:

# 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

Cela permet également de conduire à une structure pilotée par l'API où les méthodes internes sont masquées et les modifications obtenues grâce à la transmission de paramètres en mode API.

Convention sur la configuration

Dans Rails, vous découvrez les contrôleurs, les vues et les modèles de votre base de données.

Pour réduire le besoin d'une configuration lourde, Rails implémente des règles pour faciliter l'utilisation de l'application. Vous pouvez définir vos propres règles, mais pour le début (et pour plus tard), il est conseillé de respecter les conventions proposées par Rails.

Ces conventions vont accélérer le développement, garder votre code concis et lisible et vous permettre une navigation facile dans votre application.

Les conventions abaissent également les barrières à l'entrée pour les débutants. Il y a tellement de conventions dans Rails qu'un débutant n'a même pas besoin de connaître, mais peut simplement profiter de l'ignorance. Il est possible de créer de superbes applications sans savoir pourquoi tout est comme ça.

Par exemple

Si vous avez une table de base de données appelée orders avec l' id clé primaire, le modèle correspondant est appelé order et le contrôleur qui gère toute la logique s'appelle orders_controller . La vue est divisée en différentes actions: si le contrôleur a une new action et une action d' edit , il y a également une new vue d' edit .

Par exemple

Pour créer une application, il vous suffit d'exécuter rails new app_name . Cela générera environ 70 fichiers et dossiers comprenant l'infrastructure et les bases de votre application Rails.

Il comprend:

  • Dossiers contenant vos modèles (couche de base de données), contrôleurs et vues
  • Dossiers contenant des tests unitaires pour votre application
  • Dossiers pour contenir vos ressources Web comme les fichiers Javascript et CSS
  • Fichiers par défaut pour les réponses HTTP 400 (fichier introuvable)
  • Beaucoup d'autres

Fat Model, Skinny Controller

«Fat Model, Skinny Controller» fait référence à la manière dont les parties M et C de MVC fonctionnent parfaitement ensemble. À savoir, toute logique liée à la non-réponse devrait aller dans le modèle, idéalement dans une belle méthode testable. Pendant ce temps, le contrôleur «skinny» est simplement une interface agréable entre la vue et le modèle.

En pratique, cela peut nécessiter différents types de refactorisation, mais tout se résume à une idée: en déplaçant une logique qui ne concerne pas la réponse au modèle (au lieu du contrôleur), non seulement vous avez favorisé la réutilisation. dans la mesure du possible, mais vous avez également permis de tester votre code en dehors du contexte d'une demande.

Regardons un exemple simple. Disons que vous avez un code comme celui-ci:

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

Vous pouvez le changer en ceci:

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

Ensuite, vous pouvez déplacer la logique vers votre post-modèle, où il pourrait ressembler à ceci:

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

Attention à default_scope

ActiveRecord inclut default_scope pour default_scope automatiquement un modèle par défaut.

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

Le code ci-dessus servira les publications déjà publiées lorsque vous effectuez une requête sur le modèle.

Post.all # will only list published posts 

Cette portée, bien qu'inoffensive, a de multiples effets secondaires cachés que vous ne voudrez peut-être pas.

default_scope et order

Puisque vous avez déclaré une order dans default_scope , l' order appel sur Post sera ajouté en tant que commandes supplémentaires au lieu de remplacer la valeur par défaut.

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

Ce n'est probablement pas le comportement que vous vouliez; vous pouvez remplacer cela en excluant la order du périmètre en premier

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

default_scope et initialisation du modèle

Comme pour toute autre ActiveRecord::Relation , default_scope modifiera l'état par défaut des modèles initialisés à partir de celui-ci.

Dans l'exemple ci-dessus, Post a défini where(published: true) la valeur par défaut, de sorte que les nouveaux modèles de Post définiront également.

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

unscoped

default_scope peut être nominalement effacé en appelant non- unscoped premier, mais cela a aussi des effets secondaires. Prenez, par exemple, un modèle STI:

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

Par défaut, les requêtes sur Post seront étendues pour type colonnes contenant 'Post' . Mais non unscoped ceci avec votre propre default_scope , donc si vous utilisez unscoped vous devez vous rappeler de le prendre en compte.

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

unscoped et modèles

Envisager une relation entre Post et User

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

class User < ApplicationRecord
  has_many :posts
end

En obtenant un User individuel, vous pouvez voir les messages associés:

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

Mais vous voulez effacer le default_scope de la relation des posts , de sorte que vous unscoped

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

Cela efface la condition user_id ainsi que le default_scope .

Un exemple d'utilisation pour default_scope

Malgré tout cela, il existe des situations où l'utilisation de default_scope est justifiable.

Considérons un système multi-locataire où plusieurs sous-domaines sont servis à partir de la même application mais avec des données isolées. L'un des moyens de parvenir à cette isolation consiste à default_scope . Les inconvénients dans d'autres cas deviennent des avantages ici.

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

Tout ce que vous avez à faire est de définir Tenant.current_id sur quelque chose au début de la requête, et toute table qui contient tenant_id deviendra automatiquement portée sans code supplémentaire. L'instanciation d'enregistrements héritera automatiquement l'identifiant du locataire sous lequel ils ont été créés.

L'important à propos de ce cas d'utilisation est que la portée est définie une fois par requête et qu'elle ne change pas. Les seuls cas dont vous aurez besoin de les unscoped sont des cas particuliers tels que les travailleurs en arrière-plan qui s'exécutent en dehors d'une étendue de requête.

Vous n'en aurez pas besoin (YAGNI)

Si vous pouvez dire "YAGNI" (vous n'en aurez pas besoin) à propos d'une fonctionnalité, vous feriez mieux de ne pas l'implémenter. Il est possible de gagner beaucoup de temps de développement en se concentrant sur la simplicité. L'implémentation de telles fonctionnalités peut de toute façon entraîner des problèmes:

Problèmes

Ingénierie

Si un produit est plus compliqué que cela doit être, il est sur-conçu. Habituellement, ces fonctionnalités «non encore utilisées» ne seront jamais utilisées de la manière prévue, mais elles devront être remises à neuf si elles sont utilisées. Les optimisations prématurées, en particulier les optimisations de performances, conduisent souvent à des décisions de conception qui se révéleront erronées dans le futur.

Ballonnement de code

Code Bloat signifie un code compliqué inutile. Cela peut se produire, par exemple, par abstraction, redondance ou application incorrecte des modèles de conception. La base de code devient difficile à comprendre, déroutante et coûteuse à maintenir.

Fonction de fluage

La fonction de fluage des fonctionnalités fait référence à l'ajout de nouvelles fonctionnalités qui vont au-delà des fonctionnalités de base du produit et entraînent une complexité inutilement élevée du produit.

Long temps de développement

Le temps nécessaire pour développer les fonctionnalités nécessaires est utilisé pour développer des fonctionnalités inutiles. Le produit prend plus de temps à livrer.

Solutions

KISS - Reste simple, stupide

Selon KISS, la plupart des systèmes fonctionnent le mieux s'ils sont conçus simplement. La simplicité devrait être l’un des principaux objectifs de conception pour réduire la complexité. Cela peut être réalisé en suivant le «principe de la responsabilité unique», par exemple.

YAGNI - Vous n'en aurez pas besoin

Moins est plus. Pensez à toutes les fonctionnalités, est-ce vraiment nécessaire? Si vous pensez à YAGNI, laissez-le de côté. Il est préférable de le développer quand il le faut.

Refactoring en continu

Le produit est amélioré régulièrement. Avec le refactoring, nous pouvons nous assurer que le produit est conforme aux meilleures pratiques et ne dégénère pas en patch.

Objets de domaine (No More Fat Models)

"Fat Model, Skinny Controller" est une très bonne première étape, mais elle ne s’améliore pas une fois que votre base de code commence à se développer.

Pensons à la responsabilité unique des modèles. Quelle est la responsabilité unique des modèles? Est-ce que c'est la logique métier? S'agit-il d'une logique liée à la non-réponse?

Non, sa responsabilité est de gérer la couche de persistance et son abstraction.

La logique métier, de même que toute logique associée à la non-réponse et toute logique liée à la non-persistance, doit figurer dans les objets de domaine.

Les objets de domaine sont des classes conçues pour n'avoir qu'une seule responsabilité dans le domaine du problème. Laissez vos cours " Crier leur architecture " pour les problèmes qu'ils résolvent.

En pratique, vous devez vous efforcer d’utiliser des modèles réduits, des vues maigres et des manettes minces. L'architecture de votre solution ne doit pas être influencée par le cadre que vous choisissez.

Par exemple

Disons que vous êtes un marché qui facture une commission fixe de 15% à vos clients via Stripe. Si vous facturez une commission fixe de 15%, cela signifie que votre commission change en fonction du montant de la commande, car Stripe facture 2,9% + 30 ¢.

Le montant que vous facturez en tant que commission doit être: amount*0.15 - (amount*0.029 + 0.30) .

N'écrivez pas cette logique dans le modèle:

# 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

Dès que vous intégrez un nouveau mode de paiement, vous ne pourrez plus adapter cette fonctionnalité à ce modèle.

De plus, dès que vous commencez à intégrer davantage de logique métier, votre objet Order commence à perdre sa cohésion .

Préférer les objets du domaine, avec le calcul de la commission complètement abstrait de la responsabilité des ordres persistants:

# 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

L'utilisation d'objets de domaine présente les avantages architecturaux suivants:

  • Il est extrêmement facile de tester les unités, car aucune installation ou usine n'est requise pour instancier les objets avec la logique.
  • fonctionne avec tout ce qui accepte le amount du message.
  • maintient chaque objet de domaine petit, avec des responsabilités clairement définies et une plus grande cohésion.
  • évolue facilement avec les nouveaux modes de paiement par ajout, et non par modification .
  • arrête la tendance à avoir un objet User toujours croissant dans chaque application Ruby on Rails.

Personnellement, j'aime mettre des objets de domaine dans lib . Si vous le faites, n'oubliez pas de l'ajouter à autoload_paths :

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

Vous pouvez également préférer créer des objets de domaine plus orientés vers l'action, en suivant le modèle Command / Query. Dans un tel cas, placer ces objets dans les app/commands pourrait être un meilleur endroit car tous les sous-répertoires d' app sont automatiquement ajoutés au chemin de chargement automatique.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow