Points Clés
-
Qu'est-ce que la conception orientée objet ?
-
Qu'est-ce que la conception orientée données ?
-
Comment combiner deux paradigmes différents ?
-
Comment et pourquoi devrions-nous prendre en charge les données avec JPA et Jakarta EE ?
Les données de toute architecture moderne et distribuée, comme les microservices, fonctionnent comme une veine dans un système. Il s'intègre comme un état dans une application sans état. D'autre part, nous avons les paradigmes les plus populaires dans le code, en particulier lorsque nous parlons de POO en entreprise. Comment combinez-vous à la fois la conception d'archives et la conception de logiciels, principalement en Java ?
Cet article explore plus en détails le code, en particulier dans le monde de Jakarta EE, principalement pour répondre aux questions d'une discussion précédente sur Jakarta JPA : devrions-nous avoir un constructeur avec JPA, et pourquoi ?
Données de contexte et Java
Lorsque nous parlons de Java et de base de données, la manière la plus systématique d'intégrer les deux mondes consiste à utiliser des frameworks. Dans un framework, nous avons des types et des catégories basés sur les niveaux de communication et la convivialité de l'API.
-
Niveau de communication : il définit à quelle distance le code est d'une base de données ou plus proche du domaine de la POO.
-
Un pilote (driver) est un niveau du framework plus proche de la POO et du domaine, et éloigné d'une base de données. Un pilote sur lequel nous pouvons travailler en douceur est orienté données. Cependant, cela pourrait apporter plus de code passe-partout pour obtenir le code au domaine (par exemple, JDBC).
-
Un mapping va dans une autre direction, et donc, plus proche de la POO et loin de la base de données. Là où cela réduit le code passe-partout à un domaine, nous pouvons être confrontés à des problèmes d'impédance et de performances (par exemple, Hibernate et Panache).
-
-
Facilité d'utilisation de l'API : pour une API donnée, combien de fois l'utiliserez-vous pour différentes bases de données ? Une fois que nous avons SQL comme standard sur la base de données relationnelle, nous avons généralement une API pour tous les types de bases de données.
-
Une API spécifique est une API qui fonctionne exclusivement sur une base de données. Elle implique souvent des mises à jour de ce fournisseur ; néanmoins, remplacer une base de données signifie changer toute l'API (par exemple, Mophia, Neo4j-OGM Object Graph Mapper).
-
Une API agnostique est une API étendue où vous avez une API pour plusieurs bases de données. Il serait plus facile d'utiliser plus de bases de données, mais les mises à jour ou le comportement particulier de la base de données sont plus difficiles.
-
DDD vs Data-Oriented
Chaque fois que nous parlons de conception de logiciels avec Java, nous parlons principalement du paradigme POO. Dans le même temps, une base de données est généralement un paradigme différent. La principale différence est ce que nous appelons la désadaptation d'impédance.
La POO apporte plusieurs approches et bonnes pratiques, telles que l'encapsulation, la composition, l'héritage, le polymorphisme, etc., qui n'auront pas de support sur une base de données.
Vous pourriez lire le livre "Clean Code" où nous avons une citation d'oncle Bob : "La POO cache les données pour exposer le comportement". Le DDD fonctionne de cette manière pour avoir un langage et un domaine omniprésents souvent autour de la POO.
Dans son livre "Data-Oriented Programming", l'auteur Yehonathan Sharvit propose de réduire la complexité en valorisant et en traitant les données comme un "citoyen de premier ordre".
Ce patron résume trois principes:
-
Le code est séparé des données.
-
Les données sont immuables.
-
Les données ont un accès flexible.
C'est le plus gros problème avec les deux paradigmes : il est difficile d'avoir les deux simultanément, mais cela s'inscrit dans le contexte.
JPA et les données
Le JPA est la solution la plus populaire avec les bases de données relationnelles. C'est un standard Java, et nous pouvons voir plusieurs plates-formes l'utiliser, telles que Quarkus, Spring, etc.
Pour lutter contre l'impédance, JPA dispose de plusieurs fonctionnalités pour réduire cette attraction, comme l'héritage, où le moteur d'implémentation de JPA traduira vers/depuis la base de données.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class Product {
@Id
private long id;
@Column
private String name;
//...
}
@Entity
public class Computer extends Product {
@Column
private String version;
}
@Entity
public class Food extends Product {
@Column
private Localdate expiry;
}
JPA et les constructeurs
Une fois que nous avons le contexte, parlons de cette grande discussion sur les ambassadeurs de Jakarta EE, et nous avons également un issue GitHub.
Nous comprenons qu'il y a toujours des compromis à faire lorsque l'on discute de l'architecture et de la conception des logiciels. Ainsi, l'architecture d'entreprise nécessite à la fois DDD et une approche orientée données basée sur le contexte.
Récemment, Brian Goetz a écrit un article relatif à la programmation orientée données en Java dans laquelle il explique comment archiver le succès de la programmation de données à l'aide de fonctionnalités telles que les records et les classes scellées.
Ce serait bien si nous pouvions explorer et utiliser les records avec JPA, mais nous avons un problème hérité car JPA nécessite un constructeur par défaut.
La question est, cela devrait-il suffire ? Ou JPA devrait-il prendre en charge plus que POO/DDD, en ignorant la programmation des données ? De mon point de vue, nous devrions exécuter la programmation de données même si cela casse le constructeur par défaut précédemment requis.
JPA exigeant des constructeurs par défaut à peu près partout est une limitation importante de la conception des entités pour des dizaines de raisons. Les records rendent cela assez évident. Ainsi, bien que vous puissiez argumenter que la persistance n'a pas besoin de faire quoi que ce soit concernant cet aspect, je pense qu'il devrait le faire. Parce que l'amélioration de cet aspect profiterait largement à la persistance, et pas seulement à la persistance des records.
Nous pouvons imaginer plusieurs scénarios où nous pouvons bénéficier de l'approche de conception de code :
-
Une entité immuable : nous avons une entité en lecture seule. La source est la base de données.
public class City {
private final String city;
private final String country;
public City(String city, String country) {
this.city = city;
this.country = country;
}
public String getCity() {
return city;
}
public String getCountry() {
return country;
}
}
-
Forcer une entité à être valide : imaginons que nous voulions à la fois une entité immuable pour forcer la cohérence et que l'entité soit instanciée. Ainsi, nous pouvons la combiner avec Bean Validation pour toujours créer une entité lorsqu'elle apporte des valeurs valides.
public class Player {
private final String name;
private final String city;
private final MonetaryAmount salary;
private final int score;
private final Position position;
public Player(@Size(min = 5, max = 200) @NotBlank String name,
@Size(min = 5, max = 200) @NotBlank String city,
@NotNull MonetaryAmount salary,
@Min(0) int score,
@NotNull Position position) {
this.name = name;
this.city = city;
this.salary = salary;
this.score = score;
this.position = position;
}
}
Proposition pour JPA
Nous avons appris de la méthodologie Agile pour publier en continu et suivre un processus par étapes. Par conséquent, nous pouvons commencer par prendre en charge deux annotations, obtenir des commentaires, échouer rapidement, puis avancer.
Comme première étape, nous pouvons avoir une nouvelle annotation : @Constructor. Une fois que nous l'avons sur le constructeur, il ignorera les annotations de champ à utiliser sur le constructeur. Nous pouvons prendre en charge deux annotations : @Id et @Column.
@Entity
public class Person {
private final Long id;
private final String name;
@Constructor
public Person(@Id Long id, @Column String name) {
this.id = id;
this.name = name;
}
//...
}
Nous devrions également avoir un support sur la validation des beans à cette étape.
@Entity
public class Person {
@Id
private final Long id;
@Column
private final String name;
@Constructor
public Person(@NotNull @Id Long id, @NotBlank @Column String name) {
this.id = id;
this.name = name;
}
//...
}
Vous pouvez également explorer les records dans ce cas.
@Entity
public record Person(@Id @NotNull Long id, @NotBlank @Column String name){}
Le baby step est proposé et fait. L'étape suivante consiste à recevoir des commentaires et des retours de la communauté.
Conclusion
La conception de logiciels, principalement avec la POO, est un monde riche et apporte plusieurs nouvelles perspectives. Il est de coutume de revoir les anciens concepts pour en obtenir de nouveaux. C'est arrivé avec CDI, où il a amélioré le constructeur pour exprimer une meilleure conception, et cela devrait arriver à JPA avec la même proposition.