Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rdd : Spécifications #258

Open
nfaugout-lucca opened this issue Oct 25, 2018 · 1 comment
Open

Rdd : Spécifications #258

nfaugout-lucca opened this issue Oct 25, 2018 · 1 comment

Comments

@nfaugout-lucca
Copy link
Contributor

nfaugout-lucca commented Oct 25, 2018

Je vais décrire le fonctionnement de Rdd tel que je le vois dans l'idéal. Ensuite vous me direz ce que vous en pensez. Et une fois qu'on sera alignés, on verra en quoi il est différent de Rdd v3 et on saura alors quoi refactorer !

Architecture en couches

Rdd possède 4 couches qui ont chacune des responsabilités bien différentes :

  • Web, qui expose la logique Rdd via des API REST. Elle gère la transformation d'une requête entrante vers un appel de code à la couche Application, puis gère la sérialisation de la réponse en Json. C'est égalament dans cette couche qu'on gère l'enregistrement de l'injection, étant donné que c'est la couche qui fait tourner l'application.

  • Application, qui expose la logique Rdd sous forme de code C# mais en empruntant le style architectural REST. De ce point de vue, les appels HTTP des controllers web sont routés en 1 pour 1 vers des méthodes correspondantes des controllers applicatifs. Cette couche gère les échanges avec l'Infra, notamment la sauvegarde des données (base, disque). C'est également elle qui valide les données en provenance de la couche Web afin de s'assurer qu'elles sont viables, car le Domain ne fonctionne qu'avec des données valides.

  • Domain, qui expose des entités en C# à travers des collections. Chaque entité est pilotée par une collection qui fait office de factory en écriture, et contrôle les accès aux entités en lecture. On ne doit pas pouvoir récupérer ou écrire des entités autrement qu'en passant par sa collection. La collection expose des méthodes générique pour les appels provenant de Web, et des méthodes explicites pour les appels C#. Le Domain est agnostic de la couche de persistance (base, disque). Les entités sont des objets riches qui contiennent un état et un comportement. Ils sont notamment capable de se valider eux-mêmes.

  • Infra, qui regroupe toutes les implémentations des abstractions dont les 3 autres couches ont besoin. On pourrait d'ailleurs éclater cette couche en des projets spécialisés chacun dans leur techno, de sorte qu'un projet client ne soit pas contraint de dépendre de toutes les librairies tierces utilisées par Infra.

Réponses HTTP

Rdd supporte les 4 verbes HTTP, et fait même une distinction sur le GET entre un GET vers une collection ou vers une entité en particulier.

Les controllers web exposent donc 5 routes (Get, GetById, Put, Post, Delete). Ils n'ont pas vocation à en exposer plus. Si tel est le cas, cela signifie qu'il faut créer une sous entité pour matérialiser le process qui se dessine.

Get renverra une 404 uniquement si le type d'entité n'existe pas (route inexistante).

GetById renverra une 404 si l'id ne correspond à aucun entité existante. Si une entité à été supprimée, alors son URL devra renvoyer une 404. Si en revanche elle est inactive, càd qu'on peut encore y accéder en passant un paramètre spécifique à la collection, alors son URL devra renvoyer une 200.

L'utilisation de Put n'est pas conseillée. Put ne doit être utilisé qu'en cas de modification de l'état d'une entité n'entraînant pas un processus métier identifié. Dans le cas contraire, il faut alors créer une nouvelle entité qui représentera ce processus métier et faire un Post sur cette nouvelle entité.

Put prend en entrée des clés/valeurs correspondant aux propriétés de l'entité, et renvoie en sortie l'entité complète dans son nouvel état.

Post permet de créer une entité. Actuellement il impose d'envoyer des clés/valeurs correspondant aux propriétés de l'entité. On pourrait imaginer qu'il soit bindé aux constructeurs de l'entité, et donc qu'on doivent envoyer des clés/valeurs correspondant aux paramètres des différents contructeurs. Ainsi on pourrait à la fois ouvrir un peu les contraintes en acceptant des paramètres ne correspondant pas systématiquement à des propriétés de l'entité, mais également renforcer les contraintes en n'acceptant QUE des combinaisons de paramètres correspondant à des constructeurs explicites. Ceci garantirait une sécurité très forte par rapport à la situation actuelle.

Un Post renvoie la nouvelle entité dans son état final.

Delete permet de supprimer une entité. Qu'il s'agisse d'une suppression logique ou physique, le fait d'être passé par un Delete implique que l'entité n'est plus accessible par aucun moyen. Son URL directe (avec son Id) renvoie une 404. C'est pour cette raison qu'un Delete ne renvoie pas l'entité, car elle n'existe plus. Il renvoie un 200 OK vide.

Rdd supporte les requêtes multiples. Elles sont atomiques, càd que si une seule plante, aucune modification n'est enregistrée pour aucune requête. Voici les détails pour chaque verbe :

  • un Delete multiple prend en entrée la liste des Id des entités à supprimer
  • un Post multiple prend en entrée une liste d'objets contenant des clés/valeurs correspondant aux entités à créer
  • un Put multiple prend en entrée une liste d'objets contenant pour chacun l'id de l'entité à modifier ainsi que les clés/valeurs des propriétés à modifier

Pour faire des requêtes multiples non atomiques, càd indépendantes les unes des autres, il faut créer une route spécifique, par ex /batchs, qu'on utilise toujours en Post, en envoyant des objets décrivant les requêtes qu'on veut faire, par ex [ { "method": "Put", "href": "/api/users/123", "data": { "address": "newAddress", "phoneNumber": "01 02 03 04 05" } }, ... ]. Cette feature n'est actuellement pas implémentée par Rdd.

Si une route n'est pas décrite explicitement dans le controller web, elle devrait planter en 404.

Controller Applicatif

Les controllers applicatif sont les seuls points d'entrée dans une application Rdd. En général appelé depuis la couche Web, mais également depuis des tests. Ils peuvent également être appelé depuis un script ou une application console. Ils représentent la surface d'exposition publique de l'application.

Si un controller A veut appeler un controller B, alors la méthode exposée sur B devra être internal de sorte qu'elle ne soit pas exposée au monde extérieur.

Si un même repo GitHub comporte plusieurs applications, avec des projets mutualisés, il ne faut pas appeler un controller B d'une appli P2 depuis un controller A d'une appli P1 ! Si P1 et P2 sont 2 applis qui tournent séparément (ont chacune leur projet web), alors P1 ne peut appeler P2 qu'en faisant une requête HTTP, de sorte de ne pas faire tourner une partie de P2 dans P1 !

Un controller applicatif permet de séquencer un ou plusieurs appels vers des collections. Néanmoins, il est important de noter que si un concept émerge dans la couche Application sans exister dans le Domain, ce n'est pas normal. Il convient alors de créer une collection et une entité dédiée à ce concept et ainsi avoir un controller applicatif qui ne fait qu'un seul appel au lieu de 2 ou 3.

Le controller a également comme responsabilité de valider les données provenant de Web avant d'appeler Domain, de sorte que le Domain ne fonctionne que sur des données valides.

Enfin, il décide de persister les données, de sorte que le Domain n'ait jamais à s'en préoccuper.

Si des traitements sont à lancer après la persistance, notamment l'envoi des mails de notif ou encore l'émission d'un événement (EventBroker power !) alors ce sera également au controller applicatif de le gérer.

Collections du Domain

Les Collections du Domain représentent les points d'entrées vers le Domain. Elles permettent de centraliser l'accès aux entités, et également de leur injecter des services en dépendance.

Elles peuvent s'appuyer sur des helpers, notamment pour l'instanciation des entités et/ou leur patching leur d'un Put.

Si on applique la logique exposée plus haut à propos de l'instanciation des entités (via leur constructeur uniquement), et si ce sont les Controllers applicatifs qui lancent la validation des entités, alors la création se résume à vérifier les droits et ajouter l'entité au Repo.

Lors d'un Put, l'entité est remontée par le Repo, donc censée être valide, puis patchée dans la Collection, qui doit alors la valider. Ici aussi, il faut vérifier les droits, c'est fait lors de la remontée de l'entité.

En lecture, la Collection passe la Query (en provenance de la couche Web) au Repo qui va alors générer une requête SQL correspondante. Les droits sont également joués par le Repo (au plus proche des données).

Entités

Le concept d'entité arrive tout droit du DDD, et nous essayons de le respecter au maximum. Pour ce faire, une entité a forcément un Id, ce qui permet de l'identifier de façon unique. Etant donné qu'on est dans un paradigme REST, les entités ont également un nom et une URL en sortie de la couche Web.

Une Entité doit posséder tout ce qui permet son bon fonctionnement, et n'est pilotée que par sa collection. L'entité doit être capable de gérer son état, ainsi que ses modifications (via des méthodes appropriée). Elle doit aussi gérer sa validation et son instanciation.

Elle peut s'appuyer sur des ValueObjects pour mieux organiser son état.

Elle peut également gérer des sous entités notamment s'il s'agit de commandes.

Repo de l'Infra

Les Repos, à l'instar du concept éponyme dans le DDD, sont là pour reconstruire les entités du Domain lors d'un Get, et pour assurer leur persistance en écriture.

En lecture, ils interprètent la Query qui vient du Web pour la traduire en requête SQL. On peut décomposer le Get en étapes :

  • le Set() permet à une Repo d'aller chercher les entités, indépendamment de la Query ou du user qui fait la requête. C'est l'équivalent d'un SELECT * FROM T en SQL.
  • ApplyRights() permet de jouer les droits sur les entités par rapport au user connecté
  • ApplyFilters() va parser la Query et transformer les filtres web en filtres SQL sur les entités
  • ApplyOrderBys() et ApplyPage() gèrent le tri et le paging
  • ApplyIncludes() permet d'inclure des objets présents en base dans des autres tables que la table principale hébergeant les entités. Attention à ne pas ramener d'autres entités qui seraient alors dénuées de toute gestion des droits !

Un Repo s'appuie sur un IStorageService pour savoir où sont les données et permettre d'en ajouter. Il n'est plus responsable de persister les données. Cette responsabilité a été transférée à la couche Application, qui utilise un IUnitOfWork, en général branché sur le même DbContext que les Repos, pour persister effectivement les données.

Query

La Query est un objet central dans Rdd, car il matérialise sous forme d'objet la requête Web. Il est interprété par les Repos mais passe à travers le controller applicatif et la collection. C'est notamment pour cela qu'il est dans le Domain.

Néanmoins, quand une requête à une collection est faite depuis une autre collection ou un controller applicatif, on sait exactement dans quel cadre on se situe, et donc on n'a pas besoin d'un objet Query :

  • pas besoin de Fields puisqu'on récupère un objet C# complet
  • pas besoin de filtres puisqu'on peut créer une méthode explicite qui prend des filtres explicites en paramètre
  • pas besoin de orderby ni de paging puisqu'on est en C#, on peut faire ça dans une méthode explicite du Repo
  • pas besoin des options, notamment CheckRights puisqu'encore une fois, étant dans des méthodes explicites, on peut préciser ça dans le Repo explicitement

Au final, on pourrait interdire l'usage de Query depuis le Domain, et avoir 2 façons d'interagir avec des entités :

  • chemin générique, matérialisé par Query, et interprété par le Repo
  • chemin explicite, matérialisé par des méthodes explicites à tous les niveaux (controller, collection, repo)
@Poltuu
Copy link
Contributor

Poltuu commented Oct 25, 2018

Avant tout, je rajouterai que RDD est un framework destiné à simplifier la création d'api d'une application

Couche Web:

  • gère l'authentication et le routing

Réponses HTTP:

Je préciserai que RDD supporte le post, le put et le delete en masse, et que, concernant le post et le put, c'est la version en masse qui doit être prioritisée, car c'est celle ou RDD à le plus de valeur à ajouter, notamment en garantissant que les optimisations de perf sur le sujet sont garanties.

Query:

je dirais plutot que Query est censé correspondre au QuerySpecification pattern, au sens ou rien de web n'est censé transpirer de cet objet.
Je suis pour rendre impossible la construction de Query depuis les couches domain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants