web-dev-qa-db-fra.com

Est-il toujours valable de parler de modèle anémique dans le contexte de la programmation fonctionnelle?

La plupart des modèles de conception tactique DDD appartiennent au paradigme orienté objet, et le modèle anémique décrit la situation où toute la logique métier est mise dans les services plutôt que dans les objets, ce qui en fait une sorte de DTO. En d'autres termes, le modèle anémique est synonyme de style procédural, ce qui n'est pas conseillé pour les modèles complexes.

Je ne suis pas très expérimenté en programmation fonctionnelle pure, mais j'aimerais savoir comment DDD s'inscrit dans le paradigme FP et si le terme "modèle anémique" existe toujours dans ce cas.

Mise à jour : Recentlry a publié livre et vidéo sur le sujet.

Et un autre vidéo de Scott.

44
Pavel Voronin

La façon dont le problème du "modèle anémique" est décrit ne se traduit pas bien en FP tel quel. D'abord, il doit être généralisé de manière appropriée. À son cœur, un modèle anémique est un modèle qui contient des connaissances sur la façon 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 clients du modèle, mais en raison de son anémie, ils en sont tenus responsables . Par exemple, considérons une classe Account qui ne peut pas être utilisé pour activer ou désactiver des comptes ou même pour rechercher des informations sur un compte, sauf si elles sont gérées via une classe AccountManager. 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 devons 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 cela 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 de découper des sous-chaînes d'entre eux, Unicode ne devrait pas vraiment avoir d'importance, et ils devraient être facilement incorporables dans des URL et d'autres contextes avec des limitations strictes de caractère 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, un UserID est représenté de manière équivalente à une chaîne, mais ce sont des types différents et vous ne peut pas utiliser l'un là où vous attendez l'autre. " Haskell (et certains autres langages fonctionnels typés) fournit cette fonctionnalité via newtype:

newtype UserID = UserID String

Ceci définit une fonction UserID qui, lorsqu'elle reçoit un String, construit une valeur qui est traitée comme un UserID par le système de type, mais qui n'est encore qu'un String au moment de l'exécution. Les fonctions peuvent désormais déclarer qu'elles nécessitent un UserID au lieu d'une chaîne; en utilisant UserIDs alors que vous utilisiez précédemment des chaînes pour empêcher le code de concaténer deux UserIDs ensemble. Le système de type garantit que cela ne peut pas se produire, aucun test requis.

La faiblesse ici est que le code peut toujours prendre n'importe quel String comme "hello" et en construire un UserID. Les autres étapes incluent la création d'une fonction "constructeur intelligent" qui, lorsqu'elle reçoit une chaîne, vérifie certains invariants et ne renvoie un UserID que s'ils sont satisfaits. Ensuite, le constructeur "stupide" UserID est rendu privé, donc si un client veut un UserID il doit utiliser le smart constructeur, empêchant ainsi les ID utilisateur mal formés d'exister.

D'autres étapes encore définissent le type de données UserID de telle manière qu'il est impossible d'en construire un qui est malformé ou "incorrect", simplement en définition. Par exemple, définir un UserID comme une liste de chiffres:

data Digit = Zero | One | Two | Three | Four | Five | Six | Seven | Eight | Nine
data UserID = UserID [Digit]

Pour construire un UserID une liste de chiffres doit être fournie. Compte tenu de cette définition, il est trivial de montrer qu'il est impossible pour un UserID d'exister qui ne peut pas être représenté dans une URL. La définition de modèles de données comme celui-ci dans Haskell est souvent facilitée par des fonctionnalités avancées du système de type comme types de données et types de données algébriques généralisés (GADT) , qui permettent au système de type de définir et de prouver plus d'invariants sur votre code. Lorsque les données sont dissociées du comportement, votre définition de données est le seul moyen dont vous disposez pour appliquer le comportement.

27
Jack

Dans une large mesure, l'immuabilité rend inutile de coupler étroitement vos fonctions avec vos données comme le préconise OOP. Vous pouvez faire 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 crainte que la structure de données d'origine ne change de façon inattendue sous vous.

Cependant, une meilleure façon de faire cette comparaison est probablement de regarder quelles fonctions vous allouez au modèle couche par rapport aux services couche. Même si cela ne ressemble pas à la POO, c'est une erreur assez courante dans FP pour essayer de regrouper ce qui devrait être plusieurs niveaux d'abstraction en une seule fonction.

Pour autant que je sache, personne ne l'appelle 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 plus complexe ou des opérations spécifiques à l'application, vous devez également fournir un ensemble complet de fonctions uniquement pour travailler avec votre modèle. La création de couches d'abstraction appropriées est une bonne conception dans tout paradigme.

10
Karl Bielefeldt

Lorsque vous utilisez DDD dans la POO, 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 mutant l'état de l'objet. Ceci est lié à l'encapsulation: Employee.RaiseSalary mute probablement le champ salary de l'instance Employee, qui ne doit pas être réglable publiquement.

Dans FP, la mutation est évitée, vous implémentez donc ce comportement en créant une fonction RaiseSalary qui prend une instance existante de Employee et retourne une nouvelleEmployee instance avec le nouveau salaire. Aucune mutation n'est donc impliquée: seulement lire à partir de l'objet d'origine et créer le nouvel objet. Pour cette raison, une telle fonction RaiseSalary n'a pas besoin d'être définie comme une méthode sur la classe Employee, mais pourrait vivre n'importe où.

Dans ce cas, il devient naturel de séparer les données du comportement: une structure représente les Employee en tant que données (complètement anémiques), tandis qu'un (ou plusieurs) modules contiennent des fonctions qui opèrent sur ces données (préservant l'immuabilité) .

Notez que lorsque vous associez des données et un comportement comme dans DDD, vous violez généralement le principe de responsabilité unique (SRP): Employee peut avoir besoin 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é.

Ainsi, comme d'habitude, l'approche FP offre une plus grande modularité/composabilité.

8
la-yumba

Je pense que l'essence de la question est qu'un modèle anémique avec toute la logique de domaine dans les services qui fonctionnent sur le modèle est essentiellement une programmation procédurale - par opposition à une programmation "réelle" OO où vous avez des objets qui sont "intelligents" et contiennent non seulement 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 comme des entités de première classe, qui sont transmises en tant que paramètres, ainsi construites à la volée et retournées comme valeur de retour Mais lorsque vous ne parvenez pas à utiliser tout ce pouvoir et que vous n'avez que des fonctions qui fonctionnent sur des structures de données qui sont transmises entre elles, vous vous retrouvez au même endroit: vous faites essentiellement de la programmation procédurale.

0