Appel:
cargo run
de la racine du référentiel et vous êtes prêt à partir.
Essayez également d'explorer les documents API pour avoir une idée de l'endroit où tout vit:
cargo doc --open
Ce référentiel contient un exemple d'application de rouille pour une boutique en ligne. L'objectif est d'explorer certains modèles de conception qui exploitent le langage de la rouille pour créer des applications évolutives et maintenables.
C'est une aire de jeux pour différentes idées, certaines pourraient ne pas se dérouler dans la pratique. Si vous avez des commentaires sur quelque chose ici, n'hésitez pas à ouvrir un problème!
Il est difficile de concevoir des logiciels dans le vide. Lorsque vous n'avez pas de domaine réel pour conduire ce qui est important, les décisions de conception peuvent sembler arbitraires. J'ai fait un effort pour documenter les décisions et les raisons derrière eux, mais des questions comme nous devrions partager les articles des commandes des commandes? Ou les requêtes dans les commandes devraient-elles pouvoir accéder à des tables de base de données pour les produits? Je ne peux pas vraiment être répondu d'un point de vue purement technique. Ils nécessitent également une perspective sur les objectifs du projet. Pour tous ceux qui lisent ce code, je vous encourage à les examiner en fonction de ces décisions de conception arbitraires, de penser aux contraintes auxquelles vous êtes confronté dans votre propre environnement et comment ceux-ci pourraient éclairer vos propres décisions lors de la création de demandes de rouille.
Il ne s'agit pas de frameworks ou de bibliothèques de rouille spécifiques, ou de résoudre des problèmes inhérents à une application d'achat en ligne.
Les sections suivantes décrivent des parties de l'application et expliquent pourquoi elles sont rassemblées comme elles sont.
La disposition du projet est axée sur la confidentialité. En limitant la portée de certains éléments, vous limitez également la portée de la rupture potentielle. En limitant la portée de certains éléments, vous limitez également la portée de la charge de maintenir l'état d'application. Dans Rust, les articles privés dans un module sont visibles à tous les enfants de ce module . Cela peut sembler une mauvaise chose, mais nous en levons pour empêcher les API du domaine de fuir les détails de l'implémentation pour des préoccupations extérieures, comme la sérialisation et le stockage.
Chaque concept d'entreprise de base de l'application est divisé en son propre dossier autonome (principalement), comme products ou customers . Chaque module résume tout ce qu'il y a à savoir sur un ensemble particulier d'entités:
/store )/queries )/commands ) Les entités peuvent dépendre des entités d'un autre module, comme une Order en fonction d'un Product lors de l'ajout. Il y a une hiérarchie de confidentialité dans chaque module de domaine:
from_datafrom_data aux entités hydratéesCes modules sont un peu lourds, mais dans une application appropriée, l'ajout de nouveaux modules de domaine pourrait être simplifié à l'aide de macros. Je n'ai pas utilisé de macros dans cette application, le code reste donc facile à suivre.
Un problème avec une hiérarchie de module parfaitement conçue est que tout peut se désagréger lorsque vous vous retrouvez avec un concept qui ne correspond tout simplement pas à la disposition actuelle. Plus cela se produit fréquemment, plus il devient difficile de se conformer à la disposition qui existait auparavant car il devient impossible de dire ce qu'il devrait être.
Nous voulons que ces modules gèrent leur propre destin, mais nous ne voulons pas qu'ils soient autonomes au point où ils pourraient être divisés en services distincts. C'est pour garder les choses simples. Si vous vouliez le faire, je suggérerais d'utiliser des caisses séparées au lieu de simples modules séparés.
L'application suit une simple conception de ségrégation de la responsabilité de la requête de commande. Il s'agit d'une approche qui fonctionne bien pour l'application basée sur les données sans beaucoup de logique complexe. Les commandes capturent une certaine interaction du domaine et fonctionnent directement sur les entités tandis que les requêtes sont totalement arbitraires. Cette application n'utilise aucune infrastructure spéciale pour réaliser les CQR, ce ne sont que des traits simples implémentés à l'aide d'un modèle d'injection de dépendance. Essentiellement:
Result<()>Result<T>&mut self&selfLa différence de mutabilité signifie que les commandes peuvent appeler les requêtes, mais les requêtes ne peuvent pas appeler les commandes.
Les entités sont au cœur de l'application. Malgré l'absence d'une vraie entreprise, j'ai fait un effort pour garder le modèle de domaine riche. Les entités ne sont pas seulement des sacs d'État cruddy. Ils sont:
.to_data() . Lors de la visualisation d'une entité, vous ne pouvez pas appeler le comportement de modification dessus. Ceci est garanti par le système d'emprunt de Rust. Une entité peut déplacer la propriété dans ses données en lecture seule avec .into_data() . Il s'agit d'une opération à sens unique, donc toutes les modifications apportées à l'État ne peuvent pas être revenues au magasin.Le but d'une entité est d'encapsuler les invariants d'un concept de domaine clé. Les entités ici sont faciles à utiliser avec une simulation de magasin en mémoire ou une base de données externe. Nous devons faire attention de ne pas compter sur les changements d'état avec une entité reflétée dans une autre, car elles pointent vers la même source.
Les entités doivent également faire attention à ne pas dépendre des types de données d'une autre entité car il n'y a aucune garantie que les données sont réellement valides. Au lieu de cela, ils dépendent d'une entité et les convertissent en données selon les besoins, ils savent donc toujours que l'état est valide.
Nous utilisons les fonctionnalités de rouille suivantes pour protéger notre état d'entité:
Serialize ou Deserialize . Cela peut être modifié sur la piste, mais je trouve plus facile de garder un état sérialisable rapidement et de loger pour une compatibilité vers l'arrière.Les entités encapsulent certains états ou des données et garantissent que toutes les modifications apportées à ces données ne cassent aucune invarition que les données prévoient de contenir. Plutôt que d'implémenter Getters, nous exposons une vue en lecture seule des données en tant que structure. L'avantage est que vous n'avez pas à abandonner les belles fonctionnalités de Rust pour travailler avec des données, comme vous le feriez avec les méthodes Getter. Cette vue est en lecture seule , donc les modifications ne peuvent pas être réécrites directement à la structure. L'entité fournit toujours des méthodes de setter pour cela.
Vous pourriez affirmer que l'exposition de l'état de cette manière divulgue les détails de l'implémentation, comme la version qui n'a aucune valeur publique. C'est probablement vrai. Pour y contourner, vous pouvez déplacer la durée de vie de la vue en lecture seule sur les champs, et composer une vue empruntée potentiellement différente de l'État, et garder la structure de données gérée par l'entité privée.
Vous pourriez également affirmer que la tenue des invariants sur une structure qui ne les stockait pas est cassante. Cela a du sens lorsque la limite de confidentialité pour un champ est au niveau de l'objet, comme il est en C #. La rouille est un peu différente cependant. La frontière de la confidentialité la plus serrée se trouve au module et à ses enfants . Ainsi, le fardeau de maintenir les invariants d'un champ donné incombe à tous les éléments du module dans lequel il est défini, ainsi que tous les enfants de ce module.
Cela peut sembler une fuite horrible, mais cette application exploite celle pour créer un stockage bien abstrait. Au lieu d'avoir à exposer des trous dans notre API pour soutenir un ORM, le maintien de l'état des invariants s'étend simplement dans le magasin de modèles, sans ramener au public.
Les types Id et Version ont tous deux un paramètre générique fantôme. Ce paramètre existe uniquement pour vous permettre d'exprimer des ID avec des types incompatibles, comme Id<ProductData> et Id<OrderData> , mais partagent toujours d'autres détails d'implémentation.
C'est un motif plus facile à suivre que d'utiliser une macro pour réduire le chauffeur, car il y a toujours une difinition dans la source à laquelle vous pouvez revenir.
Chaque entité persistante a un champ version . Ce champ est un identifiant non séquentiel qui correspond à l'état de l'entité à un moment donné. Lorsqu'une entité est récupérée dans le magasin, nous hydrates sa version, ceci est ensuite vérifié juste avant la mise à jour et s'il ne correspond pas, nous rechignions.
La vérification de la version fonctionne bien pour le magasin en mémoire car nous avons un verrou exclusif sur les données (un seul appelant peut modifier l'état à la fois), mais aura besoin d'une approche différente pour une base de données appropriée. Nous pouvons probablement mettre à jour où l'identification et la version correspondent, sélectionnez le nombre d'enregistrements mis à jour et BALK s'il est 0 (signifie que la version ne correspond pas, ou elle n'existe pas).
La couche de stockage utilise un schéma transactionnel simple qui permet aux magasins de données indépendants de participer à des transactions. Un référentiel central garde une trace des transactions actives et est consultée lorsque les données sont récupérées dans les magasins de données pour s'assurer qu'ils sont prêts à être utilisés. La concurrence optimiste sur les données garantit que plusieurs transactions actives ne peuvent pas essayer de définir la même valeur en même temps. Cela viole l'isolement véritable, mais maintient les choses simples et nous permet de minimiser l'état nécessaire pour chaque valeur stockée.
L'injection de dépendance est bénéfique en tant que pratique sur laquelle s'appuyer lors de la conception des applications. Il vous permet de séparer les préoccupations de la résolution de dépendance de la logique de l'application. Il vous donne également une façon évidente de mettre à l'échelle une application. Cette application adopte un modèle simple qui nous offre ces avantages sans beaucoup d'infrastructures.
Cette application n'utilise pas une inversion de conteneur de contrôle comme vous pourriez être habitué si vous écrivez des applications .NET. C'est principalement parce qu'il n'y en a pas vraiment pour la rouille. C'est un problème difficile. Il utilise un modèle d'injection de dépendance simple pour composer des commandes et des requêtes, même sans conteneur sophistiqué.
L'objectif principal de l'injection de dépendance ici n'est pas de soutenir la moquerie. C'est pour réduire la complexité en poussant les préoccupations périphériques plus loin de la logique d'un composant individuel.
Les composants injectables vivent dans leur propre module. Ce module contient:
Resolver partagé qui contient une méthode qui renvoie l'implémentation par défaut sans nécessiter ses dépendances.impl Trait . Vous ne savez jamais quel type de béton que cette implémentation par défaut utilise.Arc , Box . Le Resolver partagé sonne un peu de service de service, et c'est le cas, mais parce que la résolution de dépendance est entièrement contenue dans les blocs IMP sur le Resolver lui-même, nous évitons la question de la dépendance de l'état mondial magique dans notre logique d'applications.
Pour réduire la plaque d'ébullition, pour les composants avec une seule méthode, nous les couverts également pour les implémenter pour les traits Fn . Cela vous permet d'éviter de déclarer une structure pour eux qui est générique sur toutes leurs dépendances. Le compilateur de rouille s'en occupera pour vous.
Ce modèle est difficile à décrire en prose, vous devez le voir. Jetez un œil au module domain/products/commands/create_product , ou les modules domain/products/model/store pour des exemples de ce modèle d'injection de dépendance au travail.
Resolver n'est-il pas un "objet Dieu"? Un objet dieu " est un objet de votre application qui collecte toute la logique importante à un point que vous ne pouvez pas travailler avec des composants sans travailler également via l'objet dieu. Ils sont un problème car ils deviennent difficiles à construire ou à changer. Le modèle Resolver ici est un objet dieu, mais n'est pas nécessaire pour construire des composants individuels. Le Resolver ne traite que des composants eux-mêmes.