La plupart des modèles de conception tactiques DDD appartiennent à un paradigme orienté objet, et un modèle anémique décrit la situation dans laquelle toute la logique métier est mise en services plutôt qu'en objets, ce qui en fait une sorte de DTO. En d'autres termes, modèle anémique est synonyme de style procédural, ce qui n'est pas conseillé pour un modèle complexe.
Je n'ai pas beaucoup d'expérience en programmation fonctionnelle pure, mais j'aimerais savoir comment DDD entre dans le paradigme de la PF et si le terme «modèle anémique» existe toujours dans ce cas.
functional-programming
domain-driven-design
Pavel Voronin
la source
la source
Réponses:
La façon dont le problème de "modèle anémique" est décrit ne se traduit pas bien en PF en l'état. Tout d'abord, il doit être généralisé de manière appropriée. En son cœur, un modèle anémique est un modèle qui contient des connaissances sur la manière de l’utiliser correctement qui n’est pas encapsulé par le modèle lui-même. Au lieu de cela, ces connaissances sont réparties autour d’une pile de services connexes. Ces services ne devraient être que des clients du modèle, mais en raison de son anémie, ils en sont tenus responsables . Par exemple, considérons une
Account
classe qui ne peut pas être utilisée pour activer ou désactiver des comptes ou même rechercher des informations sur un compte si elle n'est pas gérée via uneAccountManager
classe. Le compte doit être responsable des opérations de base sur celui-ci, et non d'une classe de gestionnaire externe.En programmation fonctionnelle, un problème similaire existe lorsque les types de données ne représentent pas exactement ce qu'ils sont censés modéliser. Supposons que nous devions définir un type représentant les ID utilisateur. Une définition "anémique" indiquerait que les ID utilisateur sont des chaînes. C'est techniquement faisable, mais pose d'énormes problèmes car les ID utilisateur ne sont pas utilisés comme des chaînes arbitraires. Cela n'a aucun sens de les concaténer ou d'en découper des sous-chaînes. Unicode ne devrait pas avoir d'importance, et ils devraient pouvoir être incorporés facilement dans des URL et d'autres contextes avec des limitations strictes de caractères et de format.
La résolution de ce problème se produit généralement en quelques étapes. Une première coupe simple consiste à dire "Eh bien, a
UserID
est représenté de manière équivalente à une chaîne, mais ils sont de types différents et vous ne pouvez pas utiliser l'un de ceux que vous attendez de l'autre." Haskell (et quelques autres langages fonctionnels dactylographiés) fournit cette fonctionnalité vianewtype
:Ceci définit une
UserID
fonction qui, lorsqu'elle reçoit uneString
valeur, construit une valeur qui est traitée comme unUserID
système de types, mais qui reste justeString
à l'exécution. Désormais, les fonctions peuvent déclarer qu’elles nécessitent un à laUserID
place d’une chaîne; en utilisantUserID
s où vous utilisiez auparavant des chaînes, gardes contre le code concaténant deuxUserID
s ensemble. Le système de types garantit que cela ne peut pas arriver, aucun test requis.La faiblesse est que le code peut encore prendre toute arbitraire
String
comme"hello"
et construire unUserID
de lui. La prochaine étape consiste à créer une fonction "constructeur intelligent" qui, lorsqu'elle reçoit une chaîne, vérifie certains invariants et ne les renvoie queUserID
s'ils sont satisfaits. Ensuite, leUserID
constructeur "dumb" est rendu privé. Par conséquent, si un client le souhaite,UserID
il doit utiliser le constructeur intelligent, empêchant ainsi la création d’identificateurs d’utilisateur mal formés.Même les étapes suivantes définissent le
UserID
type de données de telle sorte qu'il est impossible d'en construire un qui est mal formé ou "impropre", par simple définition. Par exemple, définirUserID
une liste de chiffres:Pour construire une
UserID
liste de chiffres doit être fournie. Compte tenu de cette définition, il est trivial de montrer qu'il est impossibleUserID
d'exister sans pouvoir être représenté dans une URL. Définir de tels modèles de données dans Haskell est souvent facilité par des fonctionnalités de système de types avancées telles que les types de données et les types de données algébriques généralisées (GADT) , qui permettent au système de types de définir et de prouver davantage d'invariants dans votre code. Lorsque les données sont découplées du comportement, votre définition de données est le seul moyen dont vous disposez pour appliquer le comportement.la source
Dans une large mesure, grâce à l’immuabilité, il n’est pas nécessaire de coupler étroitement vos fonctions avec vos données, comme le préconise la programmation orientée objet. Vous pouvez effectuer autant de copies que vous le souhaitez, même en créant des structures de données dérivées, dans un code très éloigné du code d'origine, sans craindre que la structure de données d'origine ne se modifie de manière inattendue.
Cependant, une meilleure façon de faire cette comparaison est probablement de regarder quelles fonctions vous attribuez à la couche modèle par rapport à la couche services . Même si cela ne ressemble pas à la POO, c’est une erreur assez courante en PF d’essayer de regrouper ce qui devrait être plusieurs niveaux d’abstraction en une seule fonction.
Autant que je sache, personne n’appelle cela un modèle anémique, car c’est un terme OOP, mais l’effet est le même. Vous pouvez et devez réutiliser des fonctions génériques, le cas échéant, mais pour les opérations plus complexes ou spécifiques à une application, vous devez également fournir un ensemble complet de fonctions permettant de travailler avec votre modèle. La création de couches d'abstraction appropriées est une bonne conception, quel que soit le paradigme.
la source
Lors de l'utilisation de DDD dans OOP, l'une des principales raisons de placer la logique métier dans les objets de domaine eux-mêmes est que la logique métier est généralement appliquée en modifiant l'état de l'objet. Cela est lié à l’encapsulation:
Employee.RaiseSalary
lesalary
champ de l’Employee
instance subit probablement une mutation , ce qui ne devrait pas pouvoir être défini publiquement.En mode FP, la mutation est évitée. Vous devez donc implémenter ce comportement en créant une
RaiseSalary
fonction prenant uneEmployee
instance existante et renvoyant une nouvelleEmployee
instance avec le nouveau salaire. Donc, aucune mutation n'est impliquée: seulement lire à partir de l'objet d'origine et créer le nouvel objet. Pour cette raison, une telleRaiseSalary
fonction n'a pas besoin d'être définie comme une méthode sur laEmployee
classe, mais peut vivre n'importe où.Dans ce cas, il devient naturel de séparer les données du comportement: une structure représente les
Employee
données as (complètement anémique), tandis qu'un ou plusieurs modules contiennent des fonctions qui agissent sur ces données (en préservant leur immuabilité).Notez que lorsque vous associez données et comportement comme dans DDD, vous enfreignez généralement le principe de responsabilité unique (SRP): il
Employee
peut être nécessaire de changer si les règles de changement de salaire changent; mais il peut également être nécessaire de changer si les règles de calcul du bonus EOY changent. Avec l'approche découplée, ce n'est pas le cas, car vous pouvez avoir plusieurs modules, chacun avec une responsabilité.Donc, comme d'habitude, l'approche PF offre une plus grande modularité / composabilité.
la source
Je pense que l'essentiel du problème est qu'un modèle anémique avec toute la logique de domaine dans les services qui fonctionnent sur ce modèle est essentiellement une programmation procédurale - par opposition à une programmation "réelle" où des objets sont "intelligents" et ne contiennent pas uniquement des données. mais aussi la logique la plus étroitement liée aux données.
Et le même contraste existe avec la programmation fonctionnelle: «réel» FP signifie utiliser des fonctions en tant qu’entités de première classe, transmises en tant que paramètres, construites à la volée et renvoyées en tant que valeur de retour. Mais lorsque vous ne parvenez pas à utiliser tout ce pouvoir et que vous n'avez que des fonctions qui agissent sur des structures de données échangées entre elles, vous vous retrouvez au même endroit: vous effectuez essentiellement une programmation procédurale.
la source
Je pense que oui, mais en grande partie comme une approche tactique de la transition entre des objets de valeur immuables ou comme un moyen de déclencher des méthodes sur des entités. (Où la plupart de la logique habite encore dans l'entité.)
Eh bien, si vous voulez dire "d'une manière analogue à la POO traditionnelle", il est utile d'ignorer les détails d'implémentation habituels et de revenir à la base: quelle langue vos experts de domaine utilisent-ils? Quelle intention est votre capturer de vos utilisateurs?
En supposant qu'ils parlent de l'enchaînement de processus et de fonctions, il semble alors que les fonctions (ou au moins les objets "do-er") sont fondamentalement vos objets de domaine!
Ainsi, dans ce scénario, un "modèle anémique" se produirait probablement lorsque vos "fonctions" ne sont pas réellement exécutables et ne sont que des constellations de métadonnées interprétées par un service qui effectue le travail réel.
la source