OO Design in Rails: où mettre des trucs


244

J'apprécie vraiment Rails (même si je suis généralement sans repos) et j'aime que Ruby soit très OO. Pourtant, la tendance à créer d'énormes sous-classes ActiveRecord et d'énormes contrôleurs est assez naturelle (même si vous utilisez un contrôleur par ressource). Si vous deviez créer des mondes d'objets plus profonds, où mettriez-vous les classes (et les modules, je suppose)? Je pose des questions sur les vues (dans les Helpers eux-mêmes?), Les contrôleurs et les modèles.

Lib va ​​bien, et j'ai trouvé des solutions pour le recharger dans un environnement de développement , mais j'aimerais savoir s'il existe une meilleure façon de faire ce genre de choses. Je suis vraiment préoccupé par les classes qui deviennent trop grandes. Aussi, qu'en est-il des moteurs et comment s'intègrent-ils?

Réponses:


384

Étant donné que Rails fournit une structure en termes de MVC, il est naturel de n'utiliser que les conteneurs de modèle, de vue et de contrôleur qui vous sont fournis. L'idiome typique pour les débutants (et même certains programmeurs intermédiaires) consiste à entasser toute la logique de l'application dans le modèle (classe de base de données), le contrôleur ou la vue.

À un moment donné, quelqu'un souligne le paradigme "modèle gras, contrôleur maigre", et les développeurs intermédiaires excisent tout à la hâte de leurs contrôleurs et le jettent dans le modèle, qui commence à devenir une nouvelle poubelle pour la logique d'application.

Les contrôleurs maigres sont, en fait, une bonne idée, mais le corollaire - tout mettre dans le modèle, n'est pas vraiment le meilleur plan.

Dans Ruby, vous avez quelques bonnes options pour rendre les choses plus modulaires. Une réponse assez populaire consiste à simplement utiliser des modules (généralement cachés lib) qui contiennent des groupes de méthodes, puis à inclure les modules dans les classes appropriées. Cela aide dans les cas où vous avez des catégories de fonctionnalités que vous souhaitez réutiliser dans plusieurs classes, mais où la fonctionnalité est toujours théoriquement attachée aux classes.

N'oubliez pas que lorsque vous incluez un module dans une classe, les méthodes deviennent des méthodes d'instance de la classe, vous vous retrouvez donc avec une classe contenant une tonne de méthodes, elles sont simplement bien organisées en plusieurs fichiers.

Cette solution peut bien fonctionner dans certains cas - dans d'autres cas, vous allez vouloir penser à utiliser des classes dans votre code qui ne sont pas des modèles, des vues ou des contrôleurs.

Une bonne façon d'y penser est le «principe de responsabilité unique», qui dit qu'une classe devrait être responsable d'un seul (ou petit nombre) de choses. Vos modèles sont responsables de la persistance des données de votre application vers la base de données. Vos contrôleurs sont responsables de la réception d'une demande et du retour d'une réponse viable.

Si vous avez des concepts qui ne correspondent pas parfaitement dans ces boîtes (persistance, demande / gestion de réponse), vous voulez sans doute penser à la façon dont vous voulez modéliser l'idée en question. Vous pouvez stocker des classes non modèles dans app / classes, ou n'importe où ailleurs, et ajouter ce répertoire à votre chemin de chargement en procédant comme suit:

config.load_paths << File.join(Rails.root, "app", "classes")

Si vous utilisez passager ou JRuby, vous souhaiterez probablement également ajouter votre chemin aux chemins de chargement désireux:

config.eager_load_paths << File.join(Rails.root, "app", "classes")

L'essentiel est qu'une fois que vous arrivez à un point dans Rails où vous vous trouvez à poser cette question, il est temps de renforcer vos côtelettes Ruby et de commencer à modéliser des classes qui ne sont pas seulement les classes MVC que Rails vous donne par défaut.

Mise à jour: cette réponse s'applique à Rails 2.x et supérieur.


D'oh. L'ajout d'un répertoire distinct pour les non-modèles ne m'était pas venu à l'esprit. Je peux sentir un rangement arriver ...
Mike Woodhouse

Yehuda, merci pour ça. Très bonne réponse. C'est exactement ce que je vois dans les applications dont j'hérite (et celles que je crée): tout dans les contrôleurs, les modèles, les vues et les assistants fournis automatiquement pour les contrôleurs et les vues. Viennent ensuite les mixins de lib, mais il n'y a jamais de tentative de faire de la vraie modélisation OO. Vous avez cependant raison: dans "applications / classes, ou ailleurs". Je voulais juste vérifier s'il y avait une réponse standard qui me manquait ...
Dan Rosenstark

33
Avec les versions plus récentes, config.autoload_paths utilise par défaut tous les répertoires de l'application. Vous n'avez donc pas besoin de modifier config.load_paths comme décrit ci-dessus. Cependant, je ne suis pas sûr de eager_load_paths, et je dois examiner cela. Est-ce que quelqu'un le sait déjà?
Shyam Habarakada

Passif agressif envers les intermédiaires: P
Sebastian Patten

8
Ce serait bien si Rails était livré avec ce dossier "classes" pour encourager le "principe de responsabilité unique" et pour permettre aux développeurs de créer des objets qui ne sont pas sauvegardés dans une base de données. L'implémentation «Concerns» dans Rails 4 (voir la réponse de Simone) semble avoir pris soin d'implémenter des modules pour partager la logique entre les modèles. Cependant, aucun outil de ce type n'a été créé pour les classes Ruby simples qui ne sont pas sauvegardées par une base de données. Étant donné que Rails est très opiniâtre, je suis curieux du processus de réflexion derrière NE PAS inclure un dossier comme celui-ci?
Ryan Francis

62

Mise à jour : l'utilisation de Préoccupations a été confirmée comme nouvelle valeur par défaut dans Rails 4 .

Cela dépend vraiment de la nature du module lui-même. Je place généralement les extensions contrôleur / modèle dans un dossier / Concern au sein de l'application.

# concerns/authentication.rb
module Authentication
  ...
end    

# controllers/application_controller.rb
class ApplicationController
  include Authentication
end



# concerns/configurable.rb
module Configurable
  ...
end    

class Model 
  include Indexable
end 

# controllers/foo_controller.rb
class FooController < ApplicationController
  include Indexable
end

# controllers/bar_controller.rb
class BarController < ApplicationController
  include Indexable
end

/ lib est mon choix préféré pour les bibliothèques à usage général. J'ai toujours un espace de noms de projet dans lib où je place toutes les bibliothèques spécifiques à l'application.

/lib/myapp.rb
module MyApp
  VERSION = ...
end

/lib/myapp/CacheKey.rb
/lib/myapp/somecustomlib.rb

Les extensions principales de Ruby / Rails ont généralement lieu dans les initialiseurs de configuration afin que les bibliothèques ne soient chargées qu'une seule fois sur le boostrap Rails.

/config/initializer/config.rb
/config/initializer/core_ext/string.rb
/config/initializer/core_ext/array.rb

Pour les fragments de code réutilisables, je crée souvent des (micro) plugins afin de pouvoir les réutiliser dans d'autres projets.

Les fichiers d'assistance contiennent généralement des méthodes d'assistance et parfois des classes lorsque l'objet est destiné à être utilisé par des assistants (par exemple, les générateurs de formulaires).

Ceci est un aperçu vraiment général. Veuillez fournir plus de détails sur des exemples spécifiques si vous souhaitez obtenir des suggestions plus personnalisées. :)


Chose bizarre. Je n'arrive pas à faire en sorte que require_dependency RAILS_ROOT + "/ lib / my_module" fonctionne avec quelque chose du répertoire lib. Il s'exécute définitivement et se plaint si le fichier n'est pas trouvé, mais il ne le recharge pas.
Dan Rosenstark

Les besoins de Ruby ne chargent les choses qu'une seule fois. Si vous souhaitez charger quelque chose sans condition, utilisez load.
Chuck

De plus, il me semble assez inhabituel que vous souhaitiez charger un fichier deux fois pendant la durée de vie d'une instance d'application. Générez-vous du code au fur et à mesure?
Chuck

Pourquoi utilisez-vous require_dependency au lieu de require? Notez également que si vous suivez les conventions de dénomination, vous n'avez pas du tout besoin d'utiliser require. Si vous créez MyModule dans lib / my_module, vous pouvez appeler MyModule sans demande préalable (même si l'utilisation de require devrait être plus rapide et parfois plus lisible). Notez également que les fichiers dans / lib ne sont chargés qu'une seule fois au démarrage.
Simone Carletti

1
L'utilisation des préoccupations est préoccupante
bbozo

10

... la tendance à créer d'énormes sous-classes ActiveRecord et d'énormes contrôleurs est tout à fait naturelle ...

"énorme" est un mot inquiétant ... ;-)

Comment vos contrôleurs deviennent-ils énormes? C'est quelque chose que vous devriez regarder: idéalement, les contrôleurs devraient être minces. En choisissant une règle empirique, je suggérerais que si vous avez régulièrement plus de, disons, 5 ou 6 lignes de code par méthode de contrôleur (action), alors vos contrôleurs sont probablement trop gros. Y a-t-il une duplication qui pourrait passer à une fonction d'aide ou à un filtre? Y a-t-il une logique commerciale qui pourrait être insérée dans les modèles?

Comment vos modèles deviennent-ils énormes? Devriez-vous chercher des moyens de réduire le nombre de responsabilités dans chaque classe? Y a-t-il des comportements communs que vous pouvez extraire dans les mixins? Ou des domaines de fonctionnalités que vous pouvez déléguer à des classes d'assistance?

EDIT: Essayer de développer un peu, avec un peu de chance, je ne déforme pas trop mal ...

Helpers: vivent app/helperset sont principalement utilisés pour simplifier les vues. Ils sont spécifiques au contrôleur (également disponibles pour toutes les vues de ce contrôleur) ou généralement disponibles ( module ApplicationHelperdans application_helper.rb).

Filtres: Supposons que vous ayez la même ligne de code dans plusieurs actions (assez souvent, récupération d'un objet à l'aide params[:id]ou similaire). Cette duplication peut être résumée d'abord dans une méthode distincte, puis complètement hors des actions en déclarant un filtre dans la définition de classe, tel que before_filter :get_object. Voir la section 6 du guide ActionController Rails. Laissez la programmation déclarative être votre amie.

La refonte des modèles est un peu plus une chose religieuse. Les disciples de l' oncle Bob vous suggéreront, par exemple, de suivre les cinq commandements de SOLID . Joel & Jeff peuvent recommander une approche plus, euh, "pragmatique", bien qu'ils aient semblé être un peu plus réconciliés par la suite. Trouver une ou plusieurs méthodes dans une classe qui opèrent sur un sous-ensemble clairement défini de ses attributs est une façon d'essayer d'identifier les classes qui pourraient être refactorisées à partir de votre modèle dérivé d'ActiveRecord.

Les modèles de rails ne doivent pas nécessairement être des sous-classes d'ActiveRecord :: Base. Ou pour le dire autrement, un modèle n'a pas à être un analogue d'une table, ni même lié à quoi que ce soit stocké. Encore mieux, tant que vous nommez votre fichier app/modelsselon les conventions de Rails (appelez #underscore sur le nom de la classe pour savoir ce que Rails recherchera), Rails le trouvera sans qu'aucun requires soit nécessaire.


C'est vrai à tous points de vue, Mike, et merci pour votre préoccupation ... J'ai hérité d'un projet dans lequel il y avait des méthodes sur les contrôleurs qui étaient énormes. Je les ai décomposées en méthodes plus petites, mais le contrôleur lui-même est toujours "gras". Donc ce que je recherche, ce sont toutes mes options pour décharger des trucs. Vos réponses sont «fonctions d'assistance», «filtres», «modèles», «mixins» et «classes d'assistance». Alors, où puis-je mettre ces choses? Puis-je organiser une hiérarchie de classes qui se charge automatiquement dans un env dev?
Dan Rosenstark

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.