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.
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.
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.
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. )
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)
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.
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.)
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.)
Ce qui suit n'est pas un manuel CA, mais je pense le faire pour accélérer le développement.
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.
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.
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.
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