DDD rencontre OOP: Comment implémenter un référentiel orienté objet?


12

Une implémentation typique d'un référentiel DDD n'a pas l'air très OO, par exemple une save()méthode:

package com.example.domain;

public class Product {  /* public attributes for brevity */
    public String name;
    public Double price;
}

public interface ProductRepo {
    void save(Product product);
} 

Partie infrastructure:

package com.example.infrastructure;
// imports...

public class JdbcProductRepo implements ProductRepo {
    private JdbcTemplate = ...

    public void save(Product product) {
        JdbcTemplate.update("INSERT INTO product (name, price) VALUES (?, ?)", 
            product.name, product.price);
    }
} 

Une telle interface s'attend Productà ce que a soit un modèle anémique, au moins avec des getters.

D'un autre côté, la POO dit qu'un Productobjet doit savoir comment se sauver.

package com.example.domain;

public class Product {
    private String name;
    private Double price;

    void save() {
        // save the product
        // ???
    }
}

Le fait est que lorsque le Productsait se sauver, cela signifie que le code d'infrastructure n'est pas séparé du code de domaine.

Peut-être pouvons-nous déléguer l'enregistrement à un autre objet:

package com.example.domain;

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage
            .with("name", this.name)
            .with("price", this.price)
            .save();
    }
}

public interface Storage {
    Storage with(String name, Object value);
    void save();
}

Partie infrastructure:

package com.example.infrastructure;
// imports...

public class JdbcProductRepo implements ProductRepo {        
    public void save(Product product) {
        product.save(new JdbcStorage());
    }
}

class JdbcStorage implements Storage {
    private final JdbcTemplate = ...
    private final Map<String, Object> attrs = new HashMap<>();

    private final String tableName;

    public JdbcStorage(String tableName) {
        this.tableName = tableName;
    }

    public Storage with(String name, Object value) {
        attrs.put(name, value);
    }
    public void save() {
        JdbcTemplate.update("INSERT INTO " + tableName + " (name, price) VALUES (?, ?)", 
            attrs.get("name"), attrs.get("price"));
    }
}

Quelle est la meilleure approche pour y parvenir? Est-il possible d'implémenter un référentiel orienté objet?


6
OOP dit qu'un objet produit devrait savoir comment se sauver - je ne suis pas sûr que ce soit vraiment correct ... OOP en soi ne le dicte pas vraiment, c'est plus un problème de conception / modèle (c'est là que DDD / n'importe quoi -use arrive)
jleach

1
N'oubliez pas que dans le contexte de la POO, il s'agit d'objets. Juste des objets, pas la persistance des données. Votre déclaration indique que l'état d'un objet ne doit pas être géré en dehors de lui, ce avec quoi je suis d'accord. Un référentiel est chargé de charger / enregistrer à partir d'une couche de persistance (qui est en dehors du domaine de la POO). Les propriétés et méthodes de classe doivent conserver leur propre intégrité, oui, mais cela ne signifie pas qu'un autre objet ne peut pas être responsable de la persistance de l'état. Et, les getters et setters doivent assurer l'intégrité des données entrantes / sortantes de l'objet.
jleach

1
"cela ne signifie pas qu'un autre objet ne peut pas être responsable de la persistance de l'Etat." - Je n'ai pas dit ça. L'énoncé important est qu'un objet doit être actif . Cela signifie que l'objet (et personne d'autre) ne peut déléguer cette opération à un autre objet, mais pas l'inverse: aucun objet ne doit simplement collecter des informations à partir d'un objet passif pour traiter sa propre opération égoïste (comme le ferait un dépôt avec les getters) . J'ai essayé d'implémenter cette approche dans les extraits ci-dessus.
ttulka

1
@jleach Vous avez raison, notre compréhension de la POO est différente, pour moi les getters + setters ne sont pas du tout POO, sinon ma question n'avait aucun sens. Merci quand même! :-)
ttulka

1
Voici un article sur mon point: martinfowler.com/bliki/AnemicDomainModel.html Je ne suis pas contre le modèle anémique dans tous les cas, par exemple c'est une bonne stratégie pour la programmation fonctionnelle. Mais pas OOP.
ttulka

Réponses:


7

Tu as écrit

D'un autre côté, OOP dit qu'un objet Product devrait savoir comment se sauver

et dans un commentaire.

... devrait être responsable de toutes les opérations effectuées avec

Il s'agit d'un malentendu courant. Productest un objet de domaine, il devrait donc être responsable des opérations de domaine qui impliquent un seul objet produit, ni plus ni moins - donc certainement pas pour toutes les opérations. La persistance n'est généralement pas considérée comme une opération de domaine. Bien au contraire, dans les applications d'entreprise, il n'est pas rare d'essayer de parvenir à l'ignorance de la persistance dans le modèle de domaine (au moins dans une certaine mesure), et conserver la mécanique de la persistance dans une classe de référentiel distincte est une solution populaire pour cela. Le "DDD" est une technique qui vise ce type d'applications.

Alors, quelle pourrait être une opération de domaine sensible pour un Product? Cela dépend en fait du contexte de domaine du système d'application. Si le système est petit et ne prend en charge que les opérations CRUD, alors en effet, un Productpeut rester assez "anémique" comme dans votre exemple. Pour ce type d'applications, il peut être discutable si la mise en place des opérations de base de données dans une classe de référentiel distincte, ou l'utilisation de DDD, en vaut la peine.

Cependant, dès que votre application prend en charge de réelles opérations commerciales, comme l'achat ou la vente de produits, leur maintien en stock et leur gestion, ou le calcul des taxes pour eux, il est assez courant que vous commenciez à découvrir des opérations qui peuvent être judicieusement placées dans une Productclasse. Par exemple, il peut y avoir une opération CalcTotalPrice(int noOfItems)qui calcule le prix de `n articles d'un certain produit en tenant compte des remises sur volume.

Donc, en bref, lorsque vous concevez des classes, vous devez penser à votre contexte, dans lequel des cinq mondes de Joel Spolsky vous êtes, et si le système contient suffisamment de logique de domaine, DDD sera donc avantageux. Si la réponse est oui, il est peu probable que vous vous retrouviez avec un modèle anémique simplement parce que vous gardez les mécanismes de persistance hors des classes de domaine.


Votre point me semble très sensible. Ainsi, le produit devient une structure de données anémiques lors du franchissement d'une frontière d'un contexte de structures de données anémiques (base de données) et le référentiel est une passerelle. Mais cela signifie toujours que je dois donner accès à la structure interne de l'objet via getter et setters, qui deviennent alors une partie de son API et pourraient être facilement mal utilisés par un autre code, cela n'a rien à voir avec la persistance. Existe-t-il une bonne pratique pour éviter cela? Je vous remercie!
ttulka

"Mais cela signifie toujours que je dois donner accès à la structure interne de l'objet via getter et setters" - peu probable. L'état interne d'un objet de domaine ignorant la persistance est généralement donné exclusivement par un ensemble d'attributs liés au domaine. Pour ces attributs, des getters et setters (ou une initialisation par constructeur) doivent exister, sinon aucune opération de domaine "intéressante" ne serait possible. Dans plusieurs frameworks, il existe également des fonctionnalités de persistance qui permettent de conserver les attributs privés par réflexion, de sorte que l'encapsulation n'est rompue que pour ce mécanisme, pas pour les "autres codes".
Doc Brown

1
Je suis d'accord que la persistance ne fait généralement pas partie des opérations de domaine, mais elle devrait faire partie des opérations de domaine "réelles" à l'intérieur de l'objet qui en a besoin. Par exemple, Account.transfer(amount)devrait persister le transfert. La façon dont il le fait relève de la responsabilité de l'objet et non d'une entité externe. D'un autre côté, afficher l'objet est généralement une opération de domaine! Les exigences décrivent généralement très en détail à quoi doivent ressembler les éléments. Il fait partie de la langue des membres du projet, entreprises ou autres.
Robert Bräutigam

@ RobertBräutigam: le classique Account.transferimplique généralement deux objets de compte et une unité d'objet de travail. L'opération persistante transactionnelle pourrait alors faire partie de cette dernière (combinée à des appels vers des repos connexes), donc elle reste en dehors de la méthode de «transfert». De cette façon, Accountpeut rester ignorant de la persistance. Je ne dis pas que cela est nécessairement meilleur que votre solution supposée, mais la vôtre n'est également qu'une des nombreuses approches possibles.
Doc Brown

1
@ RobertBräutigam Je suis presque sûr que vous pensez trop à la relation entre l'objet et la table. Considérez l'objet comme ayant un état pour lui-même, tout en mémoire. Après avoir effectué les transferts dans les objets de votre compte, vous vous retrouvez avec des objets avec un nouvel état. C'est ce que vous souhaitez persister, et heureusement, les objets de compte fournissent un moyen de vous informer de leur état. Cela ne signifie pas que leur état doit être égal aux tables de la base de données - c'est-à-dire que le montant transféré pourrait être un objet monétaire contenant le montant brut et la devise.
Steve Chamaillard

5

Pratiquez la théorie des atouts.

L'expérience nous apprend que Product.Save () entraîne de nombreux problèmes. Pour contourner ces problèmes, nous avons inventé le modèle de référentiel.

Bien sûr, il enfreint la règle OOP de masquer les données produit. Mais ça marche bien.

Il est beaucoup plus difficile de créer un ensemble de règles cohérentes qui couvrent tout que de créer de bonnes règles générales qui comportent des exceptions.


3

DDD rencontre OOP

Cela aide à garder à l'esprit qu'il n'y a pas de tension entre ces deux idées - les objets de valeur, les agrégats, les référentiels sont un tableau de modèles utilisés est ce que certains considèrent comme une POO bien faite.

D'un autre côté, OOP dit qu'un objet Product doit savoir comment se sauvegarder.

Mais non. Les objets encapsulent leurs propres structures de données. Votre représentation en mémoire d'un produit est responsable de montrer les comportements du produit (quels qu'ils soient); mais le stockage persistant est là-bas (derrière le référentiel) et a son propre travail à faire.

Il doit y avoir un moyen de copier les données entre la représentation en mémoire de la base de données et son souvenir persistant. À la frontière , les choses ont tendance à devenir assez primitives.

Fondamentalement, les bases de données en écriture seule ne sont pas particulièrement utiles, et leurs équivalents en mémoire ne sont pas plus utiles que le type "persistant". Il est inutile de mettre des informations dans un Productobjet si vous ne retirez jamais ces informations. Vous n'utiliserez pas nécessairement des «getters» - vous n'essayez pas de partager la structure des données du produit, et vous ne devriez certainement pas partager un accès mutable à la représentation interne du produit.

Peut-être pouvons-nous déléguer l'enregistrement à un autre objet:

Cela fonctionne certainement - votre stockage persistant devient effectivement un rappel. Je rendrais probablement l'interface plus simple:

interface ProductStorage {
    onProduct(String name, double price);
}

Il va y avoir un couplage entre la représentation en mémoire et le mécanisme de stockage, car les informations doivent aller d'ici à là (et vice-versa). La modification des informations à partager va avoir un impact sur les deux extrémités de la conversation. Nous pourrions donc tout aussi bien expliquer cela de manière explicite.

Cette approche - passer des données via des rappels, a joué un rôle important dans le développement de simulations en TDD .

Notez que la transmission des informations au rappel a toutes les mêmes restrictions que le renvoi des informations à partir d'une requête - vous ne devez pas transmettre des copies mutables de vos structures de données.

Cette approche est un peu contraire à ce qu'Evans a décrit dans le Livre bleu, où le retour de données via une requête était la façon normale de procéder, et les objets de domaine étaient spécifiquement conçus pour éviter de se mélanger dans des "problèmes de persistance".

Je comprends le DDD comme une technique de POO et je veux donc bien comprendre cette contradiction apparente.

Une chose à garder à l'esprit - Le Livre bleu a été écrit il y a quinze ans, lorsque Java 1.4 parcourait la terre. En particulier, le livre est antérieur aux génériques Java - nous avons beaucoup plus de techniques à notre disposition à l'époque où Evans développait ses idées.


2
Il convient également de mentionner: «se sauvegarder» nécessiterait toujours une interaction avec d'autres objets (soit un objet de système de fichiers, soit une base de données, soit un service Web distant, certains d'entre eux pourraient en outre nécessiter l'établissement d'une session pour le contrôle d'accès). Un tel objet ne serait donc pas autonome et indépendant. La POO ne peut donc pas l'exiger, car son intention est d'encapsuler l'objet et de réduire le couplage.
Christophe

Merci pour une excellente réponse. Tout d'abord, j'ai conçu l' Storageinterface de la même manière que vous, puis j'ai envisagé un couplage élevé et l'ai changé. Mais vous avez raison, il y a quand même un couplage incontournable, alors pourquoi ne pas le rendre plus explicite.
ttulka

1
"Cette approche est un peu contraire à ce qu'Evans a décrit dans le Blue Book" - donc il y a une certaine tension après tout :-) C'était en fait le point de ma question, je comprends le DDD comme une technique de POO et donc je veux comprendre pleinement cette contradiction apparente.
ttulka

1
D'après mon expérience, chacune de ces choses (OOP en général, DDD, TDD, pick-your-acronym) sonnent toutes bien et bien en elles-mêmes, mais chaque fois qu'il s'agit de mise en œuvre "dans le monde réel", il y a toujours un compromis ou moins que l'idéalisme qui doit être pour que cela fonctionne.
jleach

Je ne suis pas d'accord avec l'idée que la persistance (et la présentation) sont en quelque sorte "spéciales". Ils ne sont pas. Ils doivent faire partie de la modélisation pour étendre la demande d'exigences. Il n'est pas nécessaire qu'il y ait une frontière artificielle (basée sur les données) à l' intérieur de l'application, sauf s'il existe des exigences contraires.
Robert Bräutigam

1

Très bonnes observations, je suis entièrement d'accord avec vous sur elles. Voici un exposé à moi (correction: diapositives uniquement) sur exactement ce sujet: Conception orientée domaine orientée objet .

Réponse courte: non. Il ne doit pas y avoir d'objet dans votre application qui soit purement technique et sans pertinence de domaine. Cela revient à implémenter le cadre de journalisation dans une application de comptabilité.

Votre Storageexemple d'interface est excellent, en supposant que le Storagesoit alors considéré comme un cadre externe, même si vous l'écrivez.

En outre, save()dans un objet ne doit être autorisé que s'il fait partie du domaine (la "langue"). Par exemple, je ne devrais pas être obligé de «sauvegarder» explicitement un Accountaprès avoir appelé transfer(amount). Je dois à juste titre m'attendre à ce que la fonction commerciale transfer()persiste dans mon transfert.

Dans l'ensemble, je pense que les idées de DDD sont bonnes. Utiliser un langage omniprésent, exercer le domaine avec la conversation, les contextes délimités, etc. Les blocs de construction ont cependant besoin d'une refonte sérieuse pour être compatibles avec l'orientation objet. Voir le deck lié pour plus de détails.


Votre conversation est-elle quelque part à regarder? (Je vois ne sont que des diapositives sous le lien). Merci!
ttulka

Je n'ai qu'un enregistrement allemand de la conférence, ici: javadevguy.wordpress.com/2018/11/26/…
Robert Bräutigam

Great talk! (Heureusement, je parle allemand). Je pense que votre blog vaut la peine d'être lu ... Merci pour votre travail!
ttulka

Curseur très perspicace Robert. Je l'ai trouvé très illustratif mais j'ai eu l'impression qu'à la fin, de nombreuses solutions visant à ne pas casser l'encapsulation et la LoD reposent sur le fait de donner beaucoup de responsabilités à l'objet domaine: impression, sérialisation, formatage de l'interface utilisateur, etc. t qui augmentent le couplage entre le domaine et le technique (détails d'implémentation)? Par exemple, le AccountNumber couplé à l'API Apache Wicket. Ou compte avec quel que soit l'objet Json? Pensez-vous que c'est un couplage qui en vaut la peine?
Laiv

@Laiv La grammaire de votre question suggère qu'il y a un problème avec l'utilisation de la technologie pour implémenter des fonctions métier? Disons-le de cette façon: ce n'est pas le couplage entre domaine et technologie qui est le problème, c'est le couplage entre différents niveaux d'abstraction. Par exemple AccountNumber devrait savoir qu'il peut être représenté comme un TextField. Si d'autres (comme une "vue") le savent, c'est un couplage qui ne devrait pas exister, car ce composant devrait savoir de quoi il AccountNumbers'agit, c'est-à-dire les internes.
Robert Bräutigam

1

Peut-être pouvons-nous déléguer la sauvegarde à un autre objet

Évitez de diffuser inutilement la connaissance des domaines. Plus vous en savez sur un champ individuel, plus il devient difficile d'ajouter ou de supprimer un champ:

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage.save( toString() );
    }
}

Ici, le produit n'a aucune idée si vous enregistrez dans un fichier journal ou une base de données ou les deux. Ici, la méthode de sauvegarde n'a aucune idée si vous avez 4 ou 40 champs. C'est vaguement couplé. C'est une bonne chose.

Bien sûr, ce n'est qu'un exemple de la façon dont vous pouvez atteindre cet objectif. Si vous n'aimez pas construire et analyser une chaîne à utiliser comme DTO, vous pouvez également utiliser une collection. LinkedHashMapest un ancien de mes favoris car il préserve l'ordre et son toString () semble bon dans un fichier journal.

Quoi que vous fassiez, veuillez ne pas diffuser la connaissance des domaines environnants. C'est une forme de couplage que les gens ignorent souvent jusqu'à ce qu'il soit trop tard. Je veux aussi peu de choses pour savoir statiquement combien de champs mon objet a que possible. De cette façon, l'ajout d'un champ n'implique pas beaucoup de modifications à de nombreux endroits.


C'est en fait le code que j'ai affiché dans ma question, non? J'ai utilisé un Map, vous proposez un Stringou un List. Mais, comme @VoiceOfUnreason l'a mentionné dans sa réponse, le couplage est toujours là, mais pas explicite. Il n'est toujours pas nécessaire de connaître la structure de données du produit pour l'enregistrer à la fois dans une base de données ou un fichier journal, au moins lors de la lecture en tant qu'objet.
ttulka

J'ai changé la méthode de sauvegarde mais sinon, c'est à peu près la même chose. La différence est que le couplage n'est plus statique, ce qui permet d'ajouter de nouveaux champs sans forcer un changement de code sur le système de stockage. Cela rend le système de stockage réutilisable sur de nombreux produits différents. Cela vous oblige simplement à faire des choses qui semblent un peu contre nature, comme transformer un double en chaîne et revenir en double. Mais cela peut aussi être contourné si c'est vraiment un problème.
candied_orange


Mais comme je l'ai dit, je vois le couplage toujours là (par l'analyse), seulement parce qu'il n'est pas statique (explicite) apporte l'inconvénient de ne pas pouvoir être vérifié par un compilateur et donc plus sujet aux erreurs. Le Storagefait partie du domaine (ainsi que l'interface du référentiel) et fait une telle API de persistance. En cas de modification, il est préférable d'informer les clients au moment de la compilation, car ils doivent de toute façon réagir pour ne pas être interrompus lors de l'exécution.
ttulka

C'est une idée fausse. Le compilateur ne peut pas vérifier un fichier journal ou une base de données. Tout ce qu'il vérifie, c'est si un fichier de code est cohérent avec un autre fichier de code qui n'est pas non plus garanti d'être cohérent avec le fichier journal ou la base de données.
candied_orange

0

Il existe une alternative aux modèles déjà mentionnés. Le modèle Memento est idéal pour encapsuler l'état interne d'un objet de domaine. L'objet memento représente un instantané de l'état public de l'objet domaine. L'objet de domaine sait comment créer cet état public à partir de son état interne et vice versa. Un référentiel ne fonctionne alors qu'avec la représentation publique de l'État. Avec cela, la mise en œuvre interne est découplée de toute spécificité de la persistance et il lui suffit de maintenir le marché public. Votre objet de domaine ne doit pas non plus exposer de getters qui le rendraient un peu anémique.

Pour en savoir plus sur ce sujet, je recommande le grand livre: "Patterns, Principles and and Practices of Domain-Driven Design" par Scott Millett et Nick Tune

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.