Les différents types d’objets #
Plusieurs termes spécifiques peuvent préciser la nature de l’objet. Il est préférable de distinguer les objets de leur comportement logique (objets du domaine), de leurs préoccupations extérieures et techniques (représentés à l’aide d’objets de données).
Entendons par préoccupations extérieures et techniques la représentation d’un objet en base de données ou la sérialisation de l’objet pour son transfert sur le réseau (bytestream, JSON, …). Pour ce dernier cas, seules les données sont intéressantes alors que pour la logique-métier, seul le comportement est essentiel.
Les avantages sont multiples. Il devient facile de tester le comportement d’un objet s’il est découplé des détails d’une base de données par exemple. Il est possible d’avoir un objet contenant une structure différente de sa représentation en base de données également. Un objet du domaine ne devrait pas connaître le monde extérieur. Il ne devrait pas savoir qu’il est persisté par exemple. Tous ces avantages permettent d’augmenter la maintenabilité.
Si une application met à disposition beaucoup de logique, il devient important de séparer la modélisation des objets du domaine et les objets de données. La couche-métier est celle qui change le plus. Il est primordial de pouvoir la modifier et la tester facilement sans se préoccuper de la technologie ou de l’infrastructure. De plus, une entité logique ne correspond pas toujours à une entité dans une base de données. Il est rare d’avoir un modèle de classes identique à un modèle de base de données.
Objet du domaine #
L’objet tel qu’il a été décrit jusqu’à maintenant peut être appelé objet du domaine ou objet logique pour spécifier qu’il se trouve dans la logique métier. Un tel objet cache sa représentation des données pour exposer ses fonctionnalités qui agissent sur ses données.
L’objet du domaine est donc caractérisé par ses fonctionnalités.
Objet de données #
A l’inverse, un objet de données est utilisé pour exposer des données sans proposer de fonctionnalités utiles. On les appelle parfois des classes anémiques. De telles classes sont souvent immutables et proposent un constructeur avec des accesseurs. C’est une manière de mettre un nom sur une agrégation de valeurs ; de réunir des données en une information sémantique.
L’objet de données est donc caractérisé par sa structure.
Exemple d’une classe de données #
|
|
Le (nouveau) type Record de Java #
Nous remarquons que pour réaliser un objet de ce type sans logique, beaucoup de code doit être généré. Java 14 et 15 offre en mode preview une nouvelle syntaxe pour ce genre de structure. Elle devrait être officiellement intégrée dans la version TLS Java 16. Il s’agit du type Record. Celui-ci est immutable et offre gratuitement le equals
, le hashCode
et le toString
(ouf!).
|
|
Il est toujours possible d’y ajouter des méthodes à l’intérieur du bloc de déclaration, mais il est impossible de modifier les champs.
DTO #
Un DTO pour Data Trasfer Object est un objet de données sans logique et qui permet de représenter des entités récupérées d’une base de données ou que l’on souhaite envoyer (ou récupérer) à travers le réseau. Le DTO a pour rôle d’être sérialisé et désérialisé.
Active Record #
Il s’agit également d’un objet de données. Celui-ci propose les fonctionnalités de persistances usuelles CRUD (Create Read Update Delete sont les opérations classiques d’une application sans logique, mais fortement liées à la persistance des données).
|
|
Ce type de pattern est appelé Active Record et sont très souvent hybrides et ont trop de responsabilités : ils exposent les opérations techniques d’accès aux données en plus des méthodes intrinsèques à l’objet. Leur utilisation est déconseillée dans bien des cas.
DataMapper #
Un DataMapper
est un service qui permet de découpler un objet logique de son mécanisme de stockage. Il offre un minimum de fonctionnalités (opérations CRUD élémentaires) contrairement au Repository
. Il reçoit et retourne des objets du domaine bien qu’il puisse utiliser d’autres représentations en interne de manière cachée (cf.: ORM).
Voici un exemple d’un DataMapper
de Reservation
s avec l’utilisation d’un mécanisme d’abstraction:
ORM #
Un ORM (Object Relational Mapper
) est une bibliothèque qui permet de convertir la structure d’une base de données en objets à moindre effort. Les librairies mettent à disposition des ActiveRecord
s ou des DataMapper
s.
De tels outils utilisent en Java un mécanisme appelé en anglais reflection. Ce mécanisme autorise l’accès et la modification de champs sans avoir à connaître leur identifiant à la compilation. Cela s’avère utile pour récupérer tous les champs d’un objet pour les enregistrer dans un fichier sans avoir à appeler explicitement toutes les méthodes get
.
Pour que cela fonctionne, une convention nous oblige à mettre à disposition de nos objets des constructeurs vides, des getters et des setters, raison pour laquelle il ne faut jamais appliquer ce mécanisme à nos objets du domaine qui doivent constamment rester dans un état valide.
Les dangers d’un objet hybride #
Selon le principe de responsabilité unique décrite par Robert C. Martin, un module (ou classe) ne devrait avoir qu’une seule raison de changer. Un objet hybride en aurait deux : si nous souhaitons ajouter ou modifier une nouvelle fonctionnalité ou si la base de données a changé de schéma.
Il existe des outils pour transformer des entités d’une base de données en objet ou pour sérialiser des objets (en JSON par exemple). Comme nous l’avons vu pour les _ORM_s, de tels outils utilisent un mécanisme de reflection qui force les utilisateurs à déclarer des classes avec des constructeurs vides, des getters et des setters pour chaque attribut !!. L’outil nous offre un moyen simple de sérialiser, sauvegarder ou récupérer un objet sans avoir à lui dire comment le faire. Mais en imposant des constructeurs vides et des setters, leur utilisation est dangereuse et permet de compromettre facilement leur état.
Malheureusement, les développeurs ont tendance:
- à ajouter des méthodes métiers à l’intérieur,
- à abuser de ce genre d’outils, et ce, même s’ils ne les utilisent pas. Ils continuent de créer des classes selon le même principe (constructeurs vides, setters dangereux…) qui permettrait de les rendre incohérents. Il s’agit d’un anti-pattern en POO.
L’objet hybride a le seul avantage de réduire l’effort et de créer une seule classe multiresponsabilités. Le seul cas où un tel objet serait justifié est lorsqu’une application comporte très peu de logique et qu’il s’agit d’une application fortement liée à la persistance (application CRUD).
Conseil
Dans tous les autres cas, si vous les employés, cachez-les derrière un service et transformez-les en objet du domaine avant de les exposer dans la logique-métier.
Dans l’exemple ci-dessous, un ORM est utilisé dans l’implémentation d’un DataMapper. L’objet-orm n’est pas exposé dans la logique-métier. L’objet
SQLReservationMapper
devra mettre en oeuvre des fonctions pour convertir un ORM en un objet du domaine et inversément.
Exemple de version hybride #
Voici un exemple de Reservation
hybride. Il s’agit d’un Active Record qui comporte également une fonctionnalité-métier (isInConflictWith
).
|
|
- La méthode statique
get
et la méthode d’instancesave
sont des préoccupations liée à la persistance - La méthode
isInConflictWith
est une fonctionnalité-métier.
Exemple de version découplée #
Dans cette version, la persistance est déléguée à un service. L’objet métier Reservation
ne se préoccupe plus de savoir comment il sera persisté.
|
|
Extrait du service:
|
|
Rien n’empêche d’utiliser ensuite un DTO ou un ORM caché derrière une façade. Nous pouvons facilement changer de stratégie pour notre base de données sans que la logique-métier ne soit impactée et inversément.
Les services #
Un service est un composant qui offre des fonctionnalités qui concernent plusieurs classes. Il peut être également utilisé pour offrir des moyens de communiquer avec les autres couches.
Dans le modèle N-tiers, ils sont parfois placés dans une couche appelée couche service.
Dans notre exemple de tout à l’heure, la méthode isInConflictWith
était rattachée à la classe Resource
puisqu’elle concerne deux ressources. Par contre, nous pourrions imaginer un service qui réalise un horaire à partir de réservations dont la date n’a pas encore été assignée et en fonction des réservations déjà agendées.
|
|
|
|
Conclusion #
Nous avons abordé les différents composants couramment utilisés en POO pour concevoir un design de bonne qualité. Le principe de responsabilité unique stipule qu’un composant ne devrait avoir qu’une seule raison de changer. Un autre principe de programmation associé est la séparation des préoccupations (SoC pour Separation of Concerns). Il préconise qu’un composant ne doive avoir qu’une préoccupation, ce qui permet de l’isoler et de gérer un aspect bien précis d’une problématique générale.
Ces deux principes s’apparentent également à la philosophie UNIX telle que Do one thing and do it well.