Ruby on Rails
Organisation de classe
Recherche…
Remarques
Cela semble être une chose simple à faire, mais lorsque vous commencez à grossir, vous serez reconnaissant d'avoir pris le temps de les organiser.
Classe de modèle
class Post < ActiveRecord::Base
belongs_to :user
has_many :comments
validates :user, presence: true
validates :title, presence: true, length: { in: 6..40 }
scope :topic, -> (topic) { joins(:topics).where(topic: topic) }
before_save :update_slug
after_create :send_welcome_email
def publish!
update(published_at: Time.now, published: true)
end
def self.find_by_slug(slug)
find_by(slug: slug)
end
private
def update_slug
self.slug = title.join('-')
end
def send_welcome_email
WelcomeMailer.welcome(self).deliver_now
end
end
Les modèles sont généralement responsables de:
- établir des relations
- validation des données
- fournir un accès aux données via des portées et des méthodes
- Effectuer des actions sur la persistance des données.
Au plus haut niveau, les modèles décrivent les concepts de domaine et gèrent leur persistance.
Classe de service
Le contrôleur est un point d'entrée dans notre application. Cependant, ce n'est pas le seul point d'entrée possible. Je voudrais avoir ma logique accessible depuis:
- Tâches de ratissage
- emplois de fond
- console
- des tests
Si je lance ma logique dans un contrôleur, il ne sera pas accessible de tous ces endroits. Essayons donc l'approche «maigre contrôleur, gros modèle» et déplaçons la logique vers un modèle. Mais lequel? Si une logique donnée implique des modèles d' User
, de Cart
et de Product
, où devrait-elle vivre?
Une classe qui hérite d' ActiveRecord::Base
déjà beaucoup de responsabilités. Il gère l'interface de requête, les associations et les validations. Si vous ajoutez encore plus de code à votre modèle, il deviendra rapidement un gâchis intraitable avec des centaines de méthodes publiques.
Un service est simplement un objet Ruby normal. Sa classe n'a pas à hériter d'une classe spécifique. Son nom est une expression verbale, par exemple CreateUserAccount
plutôt que UserCreation
ou UserCreationService
. Il vit dans le répertoire app / services. Vous devez créer ce répertoire par vous-même, mais Rails va automatiquement charger les classes à l'intérieur.
Un objet de service fait une chose
Un objet de service (objet de méthode aka) effectue une action. Il détient la logique métier pour effectuer cette action. Voici un exemple:
# app/services/accept_invite.rb
class AcceptInvite
def self.call(invite, user)
invite.accept!(user)
UserMailer.invite_accepted(invite).deliver
end
end
Les trois conventions que je suis sont:
Les services vont dans le app/services directory
. Je vous encourage à utiliser des sous-répertoires pour les domaines à forte logique applicative. Par exemple:
- Le fichier
app/services/invite/accept.rb
définiraInvite::Accept
lorsqueapp/services/invite/create.rb
définiraInvite::Create
- Les services commencent par un verbe (et ne se terminent pas par Service):
ApproveTransaction
,SendTestNewsletter
,ImportUsersFromCsv
- Les services répondent à la méthode d'
call
. J'ai trouvé que l'utilisation d'un autre verbe le rend un peu redondant:ApproveTransaction.approve()
ne lit pas bien. De plus, la méthodecall
est la méthode de facto pourlambda
objetslambda
,procs
et method.
Avantages
Les objets de service montrent ce que fait mon application
Je peux simplement parcourir le répertoire des services pour voir ce que mon application fait: ApproveTransaction
, CancelTransaction
, BlockAccount
, SendTransactionApprovalReminder
…
Un regard rapide sur un objet de service et je sais quelle logique métier est impliquée. Je n'ai pas besoin de passer par les contrôleurs, les rappels de modèles ActiveRecord
et les observateurs pour comprendre ce que signifie «approuver une transaction».
Modèles et contrôleurs de nettoyage
Les contrôleurs transforment la requête (params, session, cookies) en arguments, les transmettent au service et les redirigent ou les rendent en fonction de la réponse du service.
class InviteController < ApplicationController
def accept
invite = Invite.find_by_token!(params[:token])
if AcceptInvite.call(invite, current_user)
redirect_to invite.item, notice: "Welcome!"
else
redirect_to '/', alert: "Oopsy!"
end
end
end
Les modèles ne traitent que des associations, des étendues, des validations et de la persistance.
class Invite < ActiveRecord::Base
def accept!(user, time=Time.now)
update_attributes!(
accepted_by_user_id: user.id,
accepted_at: time
)
end
end
Cela rend les modèles et les contrôleurs beaucoup plus faciles à tester et à entretenir!
Quand utiliser la classe de service
Atteindre des objets de service lorsqu'une action répond à un ou plusieurs de ces critères:
- L'action est complexe (par exemple, la fermeture des livres à la fin d'une période comptable)
- L'action s'étend sur plusieurs modèles (par exemple, un achat en ligne via des objets Order, CreditCard et Customer).
- L'action interagit avec un service externe (par exemple, publication sur les réseaux sociaux)
- L'action n'est pas une préoccupation centrale du modèle sous-jacent (par exemple, balayer des données obsolètes après une certaine période).
- Il y a plusieurs façons d'effectuer l'action (par exemple, authentifier avec un jeton d'accès ou un mot de passe).
Sources