BT

Diffuser les Connaissances et l'Innovation dans le Développement Logiciel d'Entreprise

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Articles DTO : Hipster Ou Dépassé ?

DTO : Hipster Ou Dépassé ?

Favoris

Points Clés

  • Comprendre les DTO
  • Quand le DTO a t-il du sens ?
  • Compromis avec les DTO
  • Pour mapper le framework au DTO
  • DTO et JAX-RS

Les objets de transfert de données, ou DTO (Data Transfert Object), font l'objet de nombreuses discussions lorsque l'on parle de développement d'applications Java. Les DTO sont nés dans le monde Java avec les Enterprise JavaBean (EJB) pour deux raisons. Premièrement, pour contourner le problème de la sérialisation des EJB ; deuxièmement, car ils définissent implicitement une phase d'assemblage où toutes les données qui seront utilisées pour la visualisation sont rassemblées avant d'aller dans la couche de présentation. Cependant, comme l'EJB n'est plus utilisé à grande échelle, les DTO peuvent-ils également être rendus obsolètes ? Le but de cet article est de parler de l'utilité des DTO et d'aborder cette question.

Après tout, dans un environnement où plusieurs nouveaux sujets sont abordés (par exemple, le cloud et les micro-services), cette couche a-t-elle un sens ? Lorsqu'on a une bonne architecture logicielle, la réponse est pratiquement unanime : cela dépend de la manière dont vous voulez que votre entité soit jumelée à la couche de visualisation.

Pour une architecture sous-jacente en couches, et en se divisant en trois parties interconnectées, nous avons le fameux MVC.

Il convient de noter que cette stratégie n'est pas exclusive à la pile d'applications web comme Spring MVC et JSF. En exposant vos données dans une application restful avec JSON, les données JSON fonctionnent comme une visualisation, même si elles ne sont pas facilement abordables pour un utilisateur typique.

Après avoir expliqué brièvement le MVC, nous parlerons des avantages et des inconvénients de l'utilisation de DTO. Concernant les applications à plusieurs niveaux, l'objectif des DTO est avant tout de séparer le modèle de la vue. Réflexion sur les problèmes de DTO :

  • Augmente la complexité
  • Il est possible de dupliquer le code
  • Ajouter une nouvelle couche impose de la traverser, ce qui ajoute un délai et une possible perte de performances.

Dans les systèmes simples qui n'ont pas besoin d'un modèle riche comme prémisse, le fait de ne pas utiliser de DTO finit par apporter de grands avantages à l'application. Le point intéressant est que de nombreux frameworks de sérialisation finissent par forcer les attributs à avoir des méthodes d'accès ou de mises à jour qui sont toujours obligatoirement présentes et publiques, ce qui, à un moment donné, aura un impact sur l'encapsulation et la sécurité de l'application.

L'autre option consiste à ajouter la couche DTO, qui garantit essentiellement le découplage de la vue et du modèle, comme mentionné précédemment.

  • Il indique clairement quels champs seront envoyés à la couche d'affichage. Oui, il y a plusieurs annotations dans divers frameworks qui indiquent quels champs ne seront pas affichés. Toutefois, si vous oubliez d’annoter, vous pouvez exporter un champ critique par accident, par exemple le mot de passe de l'utilisateur.
  • Permet une conception plus respectueuse des principes de la programmation objet. Un des points que le code propre rend clair sur l'orientation des objets est que la Programmation Orientée Objet (POO) cache les données pour exposer le comportement et l'encapsulation aide à cela.
  • Facilite la mise à jour de la base de données. Il est souvent indispensable de remanier, de migrer la base de données sans que ce changement n'ait d'impact sur le client. Cette séparation facilite les optimisations, les modifications de la base de données sans affecter la visualisation.
  • Le versionnage, la rétrocompatibilité est un point important, surtout lorsque vous disposez d'une API à usage public et avec plusieurs clients. Il est donc possible d'avoir un DTO pour chaque version et de faire évoluer le modèle métier sans souci.
  • Un autre bénéfice est qu'il est plus facile de travailler directement avec le modèle complet, ce qui permet la création d'une API que l'on peut valider point par point. Par exemple, dans mon modèle, je peux utiliser une API monétaire ; cependant, dans ma couche de visualisation, j'exporte comme un simple objet avec seulement la valeur monétaire pour la visualisation. C'est-à-dire, la bonne vieille chaîne de caractères en Java.
  • CQRS. Oui, l'approche CQRS consiste à séparer les opérations d'écriture et de lecture de données. Comment réaliser cette séparation sans les DTOs ?

En général, ajouter une couche signifie découpler et faciliter la maintenance au détriment de l'ajout de classes et de complexité, car il faut aussi penser à l'opération de conversion entre ces couches. C'est la raison, par exemple, de l'existence de MVC. Il est donc très important de comprendre que tout est basé sur l'impact et les compromis ou sur ce qui fait mal dans une application ou une situation donnée. L'absence de ces couches est très mauvaise, elle peut aboutir à un modèle de type Highlander (il ne peut y en avoir qu'un seul) dont il existe une classe avec toutes les responsabilités. De la même manière, les couches en excès adoptent le modèle en oignon, où le développeur pleure en passant sur chaque couche.

Une critique plus fréquente concernant les DTO est pour effectuer la conversion. La bonne nouvelle est qu'il existe plusieurs frameworks de conversion, c'est-à-dire qu'il n'est pas nécessaire d'effectuer les mises à jours manuellement. Dans cet article, nous allons en choisir un qui est modelmapper.

La première étape consiste à définir les dépendances du projet, par exemple, dans le POM Maven :


<dependency>
    <groupid>org.modelmapper</groupid>
    <artifactid>modelmapper</artifactid>
    <version>2.3.6</version>
</dependency>

Pour illustrer ce concept de DTO, nous allons créer une application utilisant JAX-RS connecté à MongoDB, tout cela grâce à Jakarta EE, en utilisant Payara comme serveur. Nous gérons un utilisateur avec son nom d'utilisateur, son salaire, son anniversaire et la liste des langues qu'il peut parler. Comme nous allons travailler avec MongoDB sur Jakarta EE, nous utiliserons Jakarta NoSQL.

import jakarta.nosql.mapping.Column;
import jakarta.nosql.mapping.Convert;
import jakarta.nosql.mapping.Entity;
import jakarta.nosql.mapping.Id;
import my.company.infrastructure.MonetaryAmountAttributeConverter;

import javax.money.MonetaryAmount;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@Entity
public class User {

    @Id
    private String nickname;

    @Column
    @Convert(MonetaryAmountAttributeConverter.class)
    private MonetaryAmount salary;

    @Column
    private List<String> languages;

    @Column
    private LocalDate birthday;

    @Column
    private Map<String, String> settings;

   //only getter
}

En général, il n'est pas logique que les entités aient des getters et des setters pour tous les attributs ; après tout, ce serait la même chose que de laisser l'attribut public directement. Comme notre article ne porte pas sur la DDD ou les modèles riches, nous omettrons les détails de cette entité. Pour notre DTO, nous aurons tous les champs de l'entité ; cependant, pour la visualisation, notre MonetaryAmount sera une String, et la date d'anniversaire suivra le même modèle.

import java.util.List;
import java.util.Map;

public class UserDTO {

    private String nickname;

    private String salary;

    private List<String> languages;

    private String birthday;

    private Map<String, String> settings;

    //getter and setter
}

Le grand avantage du mapper est que nous n'avons pas à nous soucier de le faire manuellement. Le seul point à noter est que certains types particuliers, par exemple le MonetaryAmount de l’API "Money & Currency", devront créer une conversion pour devenir un String et vice versa.

import org.modelmapper.AbstractConverter;

import javax.money.MonetaryAmount;

public class MonetaryAmountStringConverter extends AbstractConverter<MonetaryAmount, String> {

    @Override
    protected String convert(MonetaryAmount source) {
        if (source == null) {
            return null;
        }
        return source.toString();
    }
}


import org.javamoney.moneta.Money;
import org.modelmapper.AbstractConverter;

import javax.money.MonetaryAmount;

public class StringMonetaryAmountConverter extends AbstractConverter<String, MonetaryAmount> {

    @Override
    protected MonetaryAmount convert(String source) {
        if (source == null) {
            return null;
        }
        return Money.parse(source);
    }
}

Les convertisseurs sont prêts ; notre prochaine étape est d'instancier la classe qui effectue la conversionModelMapper. Un avantage de l'injection de dépendance est que l'on peut définir cette opération au niveau de l'application. À partir de maintenant, toute l'application peut utiliser le même mappeur ; pour cela, il suffit d'utiliser l'annotationInject` comme nous le verrons plus loin.

import org.modelmapper.ModelMapper;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import java.util.function.Supplier;

import static org.modelmapper.config.Configuration.AccessLevel.PRIVATE;

@ApplicationScoped
public class MapperProducer implements Supplier<ModelMapper> {

    private ModelMapper mapper;

    @PostConstruct
    public void init() {
        this.mapper = new ModelMapper();
        this.mapper.getConfiguration()
                .setFieldMatchingEnabled(true)
                .setFieldAccessLevel(PRIVATE);
        this.mapper.addConverter(new StringMonetaryAmountConverter());
        this.mapper.addConverter(new MonetaryAmountStringConverter());
        this.mapper.addConverter(new StringLocalDateConverter());
        this.mapper.addConverter(new LocalDateStringConverter());
        this.mapper.addConverter(new UserDTOConverter());
    }


    @Override
    @Produces
    public ModelMapper get() {
        return mapper;
    }
}

L'un des avantages significatifs de l'utilisation de Jakarta NoSQL est sa facilité d'intégration de la base de données. Par exemple, dans cet article, nous utiliserons le concept de référentiel à partir duquel nous créerons une interface pour laquelle Jakarta NoSQL se chargera de cette mise en œuvre.

import jakarta.nosql.mapping.Repository;

import javax.enterprise.context.ApplicationScoped;
import java.util.stream.Stream;

@ApplicationScoped
public interface UserRepository extends Repository<User, String> {
    Stream<User> findAll();
}

Dans la dernière étape, nous lancerons notre appel avec JAX-RS. Le point critique est que l'exposition des données se fera entièrement à partir de DTO, c'est-à-dire qu'il est possible d'effectuer toute modification au sein de l'entité à l'insu du client, grâce au DTO. Comme mentionné, le mappeur a été injecté, et la méthode map facilite grandement cette intégration entre le DTO et l'entité sans trop de code.

import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Path("users")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {

    @Inject
    private UserRepository repository;

    @Inject
    private ModelMapper mapper;

    @GET
    public List<UserDTO> getAll() {
        Stream<User> users = repository.findAll();
        return users.map(u -> mapper.map(u, UserDTO.class))
                .collect(Collectors.toList());
    }

    @POST
    public void insert(UserDTO dto) {
        User map = mapper.map(dto, User.class);
        repository.save(map);

    }

    @POST
    @Path("id")
    public void update(@PathParam("id") String id, UserDTO dto) {
        User user = repository.findById(id).orElseThrow(() ->
                new WebApplicationException(Response.Status.NOT_FOUND));
        User map = mapper.map(dto, User.class);
        user.update(map);
        repository.save(map);
    }

    @DELETE
    @Path("id")
    public void delete(@PathParam("id") String id) {
       repository.deleteById(id);
    }
}

La gestion des bases de données, du code et des intégrations est toujours difficile, même dans le cloud. En effet, le serveur est toujours là, et quelqu'un doit le surveiller, effectuer des installations et des sauvegardes, et maintenir sa bonne santé en général. Et l'APP à douze facteurs exige une séparation stricte entre la configuration et le code.

Heureusement, Platform.sh fournit un PaaS qui gère les services tels que les bases de données et les files d'attente de messages, avec un support dans plusieurs langages, y compris Java. Tout est construit sur le concept d'Infrastructure as Code (IaC), qui gère et fournit des services par le biais de fichiers YAML.

Dans les articles précédents, nous avons mentionné comment cela est fait sur Platform.sh, principalement avec trois fichiers :

Un pour définir les services utilisés par les applications (services.yaml).

mongodb:
  type: mongodb:3.6
  disk: 1024

Un pour définir les routes publiques (routes.yaml).

"https://{default}/":
  type: upstream
  upstream: "app:http"

"https://www.{default}/":
  type: redirect
  to: "https://{default}/"

Il est important de souligner que les routes sont destinées à des applications que nous voulons partager publiquement.

Platform.sh simplifie la configuration des applications individuelles et des micro-services grâce au fichier .platform.app.yaml. Contrairement aux applications uniques, chaque application de microservice aura son propre répertoire à la racine du projet et son propre fichier .platform.app.yaml associé à cette application unique. Chaque application décrira son langage et les services auxquels elle se connectera. Comme l'application cliente coordonnera chacun des microservices de notre application, elle spécifiera ces connexions en utilisant le bloc relationships de son fichier .platform.app.yaml.


name: app

type: "java:11"
disk: 1024

hooks:
    build:  mvn clean package payara-micro:bundle

relationships:
    mongodb: 'mongodb:mongodb'

web:
    commands:
        start: |
          export MONGO_PORT=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].port"`
          export MONGO_HOST=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].host"`
          export MONGO_ADDRESS="${MONGO_HOST}:${MONGO_PORT}"
          export MONGO_PASSWORD=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].password"`
          export MONGO_USER=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].username"`
          export MONGO_DATABASE=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|jq -r ".mongodb[0].path"`
          java -jar -Xmx1024m -Ddocument.settings.jakarta.nosql.host=$MONGO_ADDRESS \
          -Ddocument.database=$MONGO_DATABASE -Ddocument.settings.jakarta.nosql.user=$MONGO_USER \
          -Ddocument.settings.jakarta.nosql.password=$MONGO_PASSWORD \
          -Ddocument.settings.mongodb.authentication.source=$MONGO_DATABASE \
          target/microprofile-microbundle.jar --port $PORT

Dans cet article, nous avons parlé de l'intégration d'une application avec DTO, en plus des outils permettant de livrer et de mapper le DTO avec votre entité de manière simple. Nous avons également abordé les avantages et les inconvénients de cette couche.

 

Evaluer cet article

Pertinence
Style

Contenu Éducatif

Bonjour étranger!

Vous devez créer un compte InfoQ ou cliquez sur pour déposer des commentaires. Mais il y a bien d'autres avantages à s'enregistrer.

Tirez le meilleur d'InfoQ

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

Commentaires de la Communauté

  • Modelmapper condition

    by Sven Lee,

    Ce message a été marqué comme possible SPAM. Un modérateur le relira et le publiera sans notification dans les 24 heures. Merci.

    Merci pour cet article, comment feriez-vous avec modelmapper si vous deviez créer des DTO différents en fonction du rôle de l'utilisateur, par exemple un article contient une liste de tarifs, et selon le rôle du user on ne renverrai que certains tarifs. Ce mapper me paraissent complétement statique et impossible de les dynamiser en fournissant des paramètres extérieurs qui serait pris en compte à chaque requête.

  • Re: Modelmapper condition

    by Otavio Santana,

    Ce message a été marqué comme possible SPAM. Un modérateur le relira et le publiera sans notification dans les 24 heures. Merci.

    Très heureux que vous ayez aimé l'article. Je pense que dans ce cas, une bonne solution serait représentative du DTO avec une interface.
    Ensuite, vous pouvez utiliser un mappeur conditionnel:
    modelmapper.org/user-manual/property-mapping/#c...

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

BT