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 Product
objet 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 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?
Réponses:
Tu as écrit
et dans un commentaire.
Il s'agit d'un malentendu courant.
Product
est 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, unProduct
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 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
Product
classe. Par exemple, il peut y avoir une opérationCalcTotalPrice(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.
la source
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.Account.transfer
implique 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,Account
peut 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.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.
la source
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.
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
Product
objet 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.Cela fonctionne certainement - votre stockage persistant devient effectivement un rappel. Je rendrais probablement l'interface plus simple:
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".
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.
la source
Storage
interface 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.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
Storage
exemple d'interface est excellent, en supposant que leStorage
soit 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 unAccount
après avoir appelétransfer(amount)
. Je dois à juste titre m'attendre à ce que la fonction commercialetransfer()
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.
la source
AccountNumber
devrait savoir qu'il peut être représenté comme unTextField
. Si d'autres (comme une "vue") le savent, c'est un couplage qui ne devrait pas exister, car ce composant devrait savoir de quoi ilAccountNumber
s'agit, c'est-à-dire les internes.É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:
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 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.
la source
Map
, vous proposez unString
ou unList
. 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.Storage
fait 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.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
la source