[RUBY] Considération sur les rails et l'architecture propre

J'ai eu l'occasion de réfléchir à la conception des applications Rails, je vais donc résumer ce que j'ai pensé dans le processus.

Contexte

Une entreprise qui utilise principalement Rails a décidé de créer une nouvelle application web basée sur 0. L'entreprise gère une application assez volumineuse sur Rails et Rails lui-même a suffisamment de connaissances.

D'un autre côté, je pense que la méthode de développement traditionnelle de Rails a la limite d'un développement à grande échelle, et il y a aussi une volonté de saisir cette occasion pour explorer le Prcatice du design.

L'auteur a une certaine confiance dans la conception de l'application, mais les langages que j'utilise habituellement sont principalement Scala et NodeJS (TypeScript). J'ai développé plusieurs fois avec Rails, et je ne suis pas particulièrement gêné dans le développement, mais je ne connais pas grand-chose aux détails du framework.

Cette exigence ne considère pas le remplacement de Rails lui-même par un autre langage ou un autre framework afin d'exploiter les compétences de l'équipe d'origine.

L'objectif principal de ce document est d'examiner comment tirer parti des rails et incorporer des techniques de conception modernes.

Politique de conception de base

Sous-couche Architecture propre.

L'architecture propre (CA) est une méthode de conception qui est rapidement devenue populaire ces dernières années, et de nombreux ingénieurs en sont venus à la mentionner au Japon à la suite de la publication de la version japonaise du livre.

L'auteur lui-même a une expérience du développement basé sur quelques CA (certains sont conçus par lui-même et d'autres sont développés sur la base de la conception humaine), et je trouve que cette méthode est très cool.

D'un autre côté, je ressens également des problèmes lors de la mise en place de CA sur le cadre d'application Web existant, et j'ai pensé que j'aimerais trouver mon propre modèle gagnant en y ajoutant de la considération, alors j'ai décidé de prendre la peine de documenter cela. C'est l'un des motifs.

D'ailleurs, au moment de la rédaction de cette section, il y a encore quelques points qui sont désagréables, j'espère donc qu'ils pourront être organisés en les écrivant.

Incompatibilité entre CA et Rails

Alors, comment appliquez-vous l'essence de CA à Rails? J'ai commencé à réfléchir à la proposition, mais avant de le savoir, je suis tombé sur son incompatibilité.

C'est une évidence quand on y pense.

En effet, Clean Architecture vise à «** une méthode de conception universelle qui fonctionne quel que soit le framework utilisé », tandis que les frameworks d'application tels que Rails « dépendent du framework». En effet, l'objectif est de minimiser les coûts de développement ** ".

En d'autres termes, la direction que vous visez depuis le début est exactement le contraire, elle ne peut donc pas être compatible.

En particulier, Rails est un framework monstre qui cherche à «minimiser les coûts de développement en le rendant dépendant du framework» à la plus grande limite, de sorte que la compatibilité peut être considérée comme la pire.

La solution à ce problème dans CA est qu'il doit être implémenté à l'intérieur de cercles concentriques (Entité et Usecase) sans utiliser la fonctionnalité du framework. En fait, les projets dans lesquels j'ai été impliqué ont adopté cette approche (sinon parfaite).

Vous pouvez adopter la même approche avec Rails, mais je ne peux pas honnêtement dire que c'est bien car cela gâche complètement la bonté de Rails.

En fait, si le Rails Way effectue un processus qui ne nécessite que 3 lignes, il peut nécessiter des dizaines de lignes de code sur plusieurs fichiers selon la méthode de CA, alors cela en vaut-il le coût? Est douteux.

Je veux dire, si vous développez vous-même, vous choisissez définitivement Rails Way, dans ce cas. .. .. ..

Peut-être que de nombreux programmeurs Rails dans le monde ressentent la même chose. Comme prévu, une approche entièrement conforme à la CA sera probablement inacceptable pour les ingénieurs de Rails.

En d'autres termes, la conclusion de cette section est que dans ce cas, nous devons penser à une nouvelle façon d'incorporer l'essence de CA tout en conservant la bonté de Rails. (Si vous adoptez la méthode conforme CA, je pense que vous avez besoin d'une bonne raison pour le convaincre, mais je ne peux pas y penser un instant. Comme mentionné ci-dessus, les objectifs sont différents, alors listez les avantages dans chaque axe. Mais je ne pense pas que ce soit une sorte de guerre de religion et je peux convaincre l'autre, et personnellement je ne pense pas que l'un a raison et l'autre a tort. )

Entité CA et ActiveRecord

Rails est un enregistrement actif. La commodité de Rails est en grande partie due à l'existence d'ActiveRecord, et il n'est pas exagéré de dire qu'il ne sert à rien d'utiliser Rails si vous n'utilisez pas ActiveRecord.

Selon CA, Entity devrait être redéfini d'une manière indépendante d'ActiveRecord, mais dans Rails, je ne pense pas que ce soit une bonne idée d'éliminer ActiveRecord ici.

Dans Ruby, qui est un langage à typage dynamique, les interfaces ne peuvent pas être créées, et seule la newsletter DuckTyping gère les objets, donc dans le cas d'un modèle simple, même s'il est redéfini, il semble probable qu'il deviendra éventuellement un objet échangeable avec ActiveRecord. C'est vrai. .. ..

Si l'objet passé en argument à la méthode fonctionne, qu'il s'agisse d'une entité CA ou ActiveRecord, et qu'il n'y a aucun moyen de le limiter, la redéfinition peut être une tâche ardue. J'ai peur que.

Si tel est le cas, je pense qu'il serait fourmi d'utiliser le modèle ActiveRecord comme c'est le cas pour Entity qui peut faire un mappage de table simple pour le moment.

Cependant, je ne pense pas que ce soit une bonne idée de définir des méthodes directement liées à la logique métier dans ActiveRecord, donc si vous avez besoin de définir de telles méthodes, redéfinissez-les.

De plus, je pense qu'il vaut mieux redéfinir les éléments qui sont stockés sur plusieurs tables de la base de données, comme les factures, mais qui sont inséparables en tant qu'entités commerciales.

La frontière entre ce qui doit être redéfini et ce qui ne l'est pas est ambiguë, mais pour le moment, je pense qu'il est normal d'utiliser ActiveRecord directement au début, et de commencer par la règle de redéfinir lorsque vous en ressentez le besoin. Je vais. (C'est un choix basé sur le petit nombre de membres du projet au début. Je pense qu'il vaut mieux réfléchir un peu plus attentivement pour un projet avec de nombreux membres dès le début, mais en premier lieu, cela a quelque chose à voir avec un projet d'une telle envergure. Il n'y a pas d'autre considération ici.)

Lors de la redéfinition, il n'est pas nécessaire d'éliminer complètement ActiveRecord, il suffit de le définir sous une forme d'emballage. Par exemple, comme ça

#Facture d'achat
class Bill

  initialize(ar_model)
    @ar_model = ar_model
  end

  def corp_name
    @ar_model.corp_name
  end

  #relevé de facturation
  def bill_details
    #Je ne veux pas exposer Relation, alors_une
    #Si nécessaire, modifiez-le pour le mapper à l'entité redéfinie dans le constructeur.
    @ar_model.bill_details.to_a 
  end
end

Le problème est qu'ActiveRecord a beaucoup de méthodes d'effets secondaires (sauvegarde, mise à jour, etc.) qui peuvent être appelées de n'importe où, mais pour le moment, «Interdire l'utilisation de méthodes d'effets secondaires à partir de fichiers autres que le référentiel». Je pense qu'il suffit d'établir des règles. (C'est aussi un choix pour un petit nombre de personnes. Je pense qu'il est possible de vérifier cette règle avec Lint, que ce soit effectivement fait ou non.)

Pour être honnête, je ne pense pas que je pourrais changer d'avis en faisant ça, mais pour le moment, le début est comme ça. (Omis pour les petits groupes ou moins)

Structure du répertoire

La structure des répertoires lorsque j'ai fait la structure CA dans un projet autre que Rails était la suivante.

- domain
  - <Nom de domaine 1>
    - <Fichier d'entité>
    - <Fichier d'entité>
  - <Nom de domaine 2>
    - <Fichier d'entité>
    - <Fichier d'entité>
  - ...
- repository
  - <Nom de domaine 1>
    - <Fichier référentiel>
  - <Nom de domaine 2>
    - ...
- usecase
  - <Nom de domaine 1>
    - <Fichier de cas d'utilisation>
    - <Fichier de cas d'utilisation>
  - <Nom de domaine 2>
    - ...

Le nom de domaine est un nom qui sépare largement la zone gérée par l'application, telle que «Utilisateur» ou «Commande».

Pour les projets Scala, cette configuration est significative car les sous-projets peuvent également être utilisés pour rendre les fichiers de couche de domaine absolument indépendants des cas d'utilisation et des référentiels.

Mais qu'en est-il des rails?

Malheureusement, Rails ne semble pas être en mesure de limiter ces dépendances.

Il y a un autre problème, Rails recommande fortement de nommer en fonction du chargement automatique, donc dans la configuration ci-dessus, le nom du module + le nom de la classe est

Entity, Repository, Usecase se retrouvent tous dans le même espace de noms. Ce n'est pas très bon.

Descendez un peu plus loin,

Cependant, pour les locuteurs japonais, il est plus naturel que la modification principale soit

Est plus confortable.

Donc, sur cette base, je pense que la structure des répertoires devrait être la suivante.

- app
  - domain
    - <Nom de domaine 1>
      - entity
        - <Fichier d'entité>
      - repository
        - <Fichier référentiel>
      - usecase
        - <Fichier de cas d'utilisation>
    - <Nom de domaine 2>

En supposant que cette politique est adoptée, les problèmes et les réponses qui ont été soulevés à l'avance sont les suivants.

Les noms courants tels que "User" et "Order" sont des noms de module de premier niveau.

Je pense que c'est plutôt bien car il est facile de comprendre que le nom de la zone à traiter apparaît au niveau supérieur.

Cependant, dans Rails, il est habituel de définir ActiveRecord directement sous les modèles, et comme il semble que les noms tels que "User" et "Order" seront couverts autant que possible, le côté ActiveRecord est défini sur "AR :: User", etc. sous le module. Doit être placé.

Il est également proposé de définir la définition du domaine sur "Domain :: User :: Entity :: User", mais ce n'est pas "** Domain User " mais " Domain is User **". , Je pense que c'est redondant en tant que modification. (Personnellement, j'apprécie le sentiment que cela correspond bien au japonais. Sauf s'il y a des membres étrangers.)

Aussi, je pense qu'il est avantageux d'avoir une dénomination qui montre que le modèle ActiveRecord lui appartient, il est donc préférable de changer la dénomination côté ActiveRecord si possible.

D'ailleurs, dans la section précédente, j'ai écrit que "ActiveRecord peut être utilisé directement comme entité de domaine", mais ActiveRecord lui-même sera placé sous les modèles comme avant avec un accent sur la listabilité. (Au moment d'écrire ceci, je commence à penser qu'il est préférable de toujours définir un wrapper fin comme Domaine / Entité ... mais j'y réfléchirai en le faisant.)

Le cas d'utilisation peut fonctionner sur plusieurs objets de domaine

Je suis d'accord. N'est-ce pas correct?

Je pense que l'entité et le référentiel devraient être complétés dans ce domaine, mais je ne pense pas qu'il y ait de problème avec Usecase traitant de plusieurs domaines.

S'il ne vous semble pas approprié de placer le fichier, vous pouvez définir un autre domaine sans Entité ou Repsitory et mettre uniquement Usecase.

C'est dommage, mais je pense qu'il est normal d'inclure le module Entity du même domaine dans Repository. Mais Usecase n'est pas bon. (En fait, si vous passez divers Repository dans le constructeur, je pense qu'il n'y a presque pas de scène où le nom de la modification du nom du module apparaît.)

Simplification

Ce qui suit n'est pas un manuel CA, mais je pense le faire pour accélérer le développement.

L'acquisition d'entité par ID peut être une fonction de module du module de domaine

Comme ça

module Order extend self

  order_repository
    @order_repository ||= Order::Repository::OrderRepository.new
  end

  module_function
  def get_order_by_id(order_id)
    order_repository.get(order_id)
  end
end

C'est une règle que vous pouvez créer un raccourci si nécessaire car il existe des situations dans lesquelles vous souhaitez utiliser l'acquisition d'entités en spécifiant un ID même lors du débogage. (Il n'est pas toujours créé pour toutes les entités.)

Si le processus équivalent à GET / XXXXs /: id des routes peut être effectué à temps, je pense qu'il n'est pas nécessaire de définir Usecase, mais dans la plupart des cas, il s'agit en fait d'un contrôle d'autorisation (dans le cas de la commande ci-dessus, moi-même (Invisible sauf pour la commande) est requis, donc Usecase est requis dans ce cas.

Définir le cas d'utilisation comme fonction de module du module de domaine

En général, Usecase prend divers Repository comme arguments dans son constructeur. En effet, il est difficile d'écrire un test à moins que le référentiel ne soit remplacé au moment du test.

Cependant, il est difficile de spécifier le référentiel chaque fois que vous utilisez Usecase à partir du contrôleur, créez donc un raccourci pour celui-ci dans le module de domaine.

Comme ça

module Order extend self

  order_repository
    @order_repository ||= Order::Repository::OrderRepository.new
  end

  module_function
  def create_order_usecase
    Order::Usecase::CreateOrder.new(
      order_repository
    )
  end
end

Le côté utilisateur

  val res = Order.create_order_usecase.run(...)

Le contenu que vous souhaitez faire est clair.

Je pense que c'est correct de spécifier les arguments par défaut du côté Usecase, mais c'est une question de goût.

tester

Pour être honnête, je n'ai pas beaucoup de connaissances sur les tests avec Rails et je tâtonne. De plus, en termes d'effectifs, je ne peux pas me permettre d'écrire un test, donc je pense que ce sera une forme d'écriture en partie là où c'est nécessaire.

Donc en gros,

Tant que vous pouvez écrire un test avec un accent sur **, vous pouvez en fait écrire un test plus tard. Si le référentiel est un simple wrapper pour ActiveRecord, le test doit avoir la priorité la plus basse. (Si cela dépend d'un service externe, un test séparé ou une maquette est nécessaire.)

Tester l'entité ne devrait pas être un problème. Pour Usecase, écrire un test peut être gênant pour certaines choses, mais en général, Usecase est

--validated --Validation des paramètres d'entrée --collect --collect l'entité requise à partir du référentiel --execute --Exécute le processus que vous souhaitez exécuter avec Usecase

Il se compose souvent de 3 étapes, et vous voudrez peut-être tester uniquement la partie d'exécution, donc je pense que ce serait bien si vous ne pouviez tester que la partie d'exécution si nécessaire.

Comme ça

class Domain1::Usecase::HogeHogeUsecase

  initialize(domain1_repository)
    @domain1_repository = domain1_repository
  end

  def run(params)
    error = validate(params)
    return ErrorCase.new(error) if error

    [v1, v2, error] = collect(params)
    return ErrorCase.new(error) if error

    return execute(v1, v2)
  end

  def validate(params)
    ...
  end

  def collect(params)
    ...
  end

  def execute(v1, v2)
    ...
  end
end

Vous devriez maintenant pouvoir écrire des tests sans vous soucier de l'état des données persistantes.

Sommaire

Jusqu'à présent, j'ai résumé ce à quoi je pensais aux premiers stades du développement. N'est-ce pas la plus grosse récolte que l'organisation ait fait progresser en moi en l'écrivant?

La puissance de ma Rails n'est pas élevée et je pense qu'il y a des avantages et des inconvénients. Mais bon, ça n'a pas vraiment d'importance. La conception sur site ne doit pas être acceptée par tous.

Cependant, je pense qu'il suffit que les membres de l'équipe partagent l'idée que ce projet est "fait avec ce genre de politique" et maintiennent la cohérence.

Chercher la bonne réponse est un travail dans le domaine des universitaires, et je pense que si un ingénieur dans le domaine la demande, il restera bloqué.

Il est impossible de tout prendre positivement, mais j'espère qu'il y a quelque chose d'utile.

En cours de réalisation, je l'ajouterai à nouveau s'il y a quelque chose à changer. (Parce que ce document est le document même pour partager "Je suis en train d'établir cette politique" avec les membres de l'équipe.)

Recommended Posts

Considération sur les rails et l'architecture propre
Prise en compte des classes et des instances
À propos des rails 6
À propos du routage des rails
À propos du contrôleur Rails
[Rails / Active Record] À propos de la différence entre créer et créer!
Un mémo sur le flux de Rails et Vue
[Rails] J'ai étudié la différence entre les ressources et les ressources
[Rails] À propos des fichiers de migration
[Rails] À propos du hachage actif
Rails valides et invalides?
Concevoir et implémenter un jeu de rupture de bloc avec une architecture propre
À propos de Bean et DI
À propos des classes et des instances
À propos de la spécification de version des rails
Concevoir et implémenter un jeu de rupture de bloc avec une architecture propre
À propos de la redirection et du transfert
À propos de Serializable et serialVersionUID
[rails] concevoir les valeurs par défaut
Un mémorandum sur les types de données de table et les commandes (Rails)
rails Paramètres forts
[Débutant] À propos de la session Rails
À propos de l'instruction et de l'instruction if
À propos du verrouillage synchronisé et réentrant
Poteaux Rails et liaison utilisateur
[Rails] nécessitent une méthode et une méthode d'autorisation
Registres du didacticiel Rails et mémorandum n ° 0
chemins de rails et méthodes d'URL
Les rails sont difficiles et douloureux!
A propos de l'attribution d'un nom aux méthodes de modèle Rails
[Java] À propos de String et StringBuilder
À peu près la même et la même valeur
[Rails] À propos de la structure des dossiers scss
Les rails sont difficiles et douloureux! Ⅱ
À propos des classes et des instances (évolution)
À propos de la méthode Pluck et de la méthode ID
[Rails] À propos du test de réponse Rspec
[Rails] strftime ceci et cela
À propos du package Java et de l'importation
À propos de Ruby, modèle objet
Prise en compte de la méthode des temps
À propos des classes et des instances Ruby
Serveur Web et serveur d'applications Rails
À propos des variables d'instance et attr_ *
À propos de la méthode de raclage des rails Mechanize
[Critique de livre] Structure et conception du logiciel d'architecture propre appris des maîtres