web-dev-qa-db-fra.com

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

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

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 à ce qu'un Product soit un modèle anémique, au moins avec des getters.

D'un autre côté, OOP dit qu'un objet Product 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 Product sait se sauver, cela signifie que le code de l'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?

12
ttulka

Tu as écrit

D'un autre côté, OOP dit qu'un objet Product doit savoir comment se sauver lui-même

et dans un commentaire.

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

Il s'agit d'un malentendu courant. Product est un objet de domaine, il devrait donc être responsable des opérations du domaine qui impliquent un objet produit unique , ni moins, ni plus - 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 raisonnable 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 Product peut 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 véritables 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 un Product classe. Par exemple, il peut y avoir une opération CalcTotalPrice(int noOfItems) qui calcule le prix de `n articles d'un certain produit lorsque l'on tient compte des remises sur volume.

Donc, en bref, lorsque vous concevez des classes, vous devez penser à votre contexte, dans lequel les 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.

7
Doc Brown

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, cela rompt la règle OOP de masquer les données produit. Mais cela fonctionne 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.

5
Ewan

DDD rencontre OOP

Il 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 être OOP bien fait .

D'autre part, OOP dit qu'un objet Product doit savoir comment se sauver.

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 objet Product si vous ne supprimez jamais ces informations. Vous n'utiliserez pas nécessairement des "getters" - vous n'essayez pas de partager la structure de 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 y a va 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). Changer les 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 mocks in TDD .

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

Cette approche est un peu contraire à ce qu'Evans a décrit dans le Blue Book, où le retour de données via une requête était la manière 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 DDD comme une technique OOP et je veux donc bien comprendre cette apparente contradiction.

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 à Java génériques - nous avons beaucoup plus de techniques à notre disposition alors qu'Evans développait ses idées.

3
VoiceOfUnreason

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. LinkedHashMap est un de mes anciens favoris car il préserve l'ordre et son toString () a l'air bien 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.

1
candied_orange

Très bonnes observations, je suis entièrement d'accord avec vous sur elles. Voici un discours à moi (correction: diapositives uniquement) sur exactement ce sujet: Object-Oriented Domain-Driven Design .

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

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

De plus, save() dans un objet ne doit être autorisé que s'il fait partie du domaine (le "langage"). Par exemple, je ne devrais pas être obligé de "sauvegarder" explicitement un Account après avoir appelé transfer(amount). Je devrais à juste titre m'attendre à ce que la fonction métier transfer() persiste 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 bornés, etc. Les blocs de construction nécessitent cependant une refonte sérieuse pour être compatibles avec l'orientation objet . Voir le deck lié pour plus de détails.

1
Robert Bräutigam

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, l'implémentation interne est dissociée de toute spécificité de la persistance et il lui suffit de maintenir le marché public. De plus, votre objet de domaine ne doit pas 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" de Scott Millett et Nick Tune

0
Roman Weis