web-dev-qa-db-fra.com

Comment convertir correctement des entités de domaine en DTO tout en tenant compte de l'évolutivité et de la testabilité

J'ai lu plusieurs articles et publications Stackoverflow pour convertir des objets de domaine en DTO et les ai essayés dans mon code. En ce qui concerne les tests et l'évolutivité, je suis toujours confronté à certains problèmes. Je connais les trois solutions possibles suivantes pour convertir des objets de domaine en DTO. La plupart du temps, j'utilise Spring.

Solution 1: méthode privée dans la couche de service pour la conversion

La première solution possible consiste à créer une petite méthode "d'assistance" dans le code de la couche de service qui convertit l'objet de base de données récupéré en mon objet DTO.

@Service
public MyEntityService {

  public SomeDto getEntityById(Long id){
    SomeEntity dbResult = someDao.findById(id);
    SomeDto dtoResult = convert(dbResult);
    // ... more logic happens
    return dtoResult;
  }

  public SomeDto convert(SomeEntity entity){
   //... Object creation and using getter/setter for converting
  }
}

Avantages:

  • facile à mettre en œuvre
  • aucune classe supplémentaire pour la conversion nécessaire -> le projet ne fait pas exploser avec des entités

Les inconvénients:

  • problèmes lors des tests, car new SomeEntity() est utilisé dans la méthode privée et si l'objet est profondément imbriqué, je dois fournir un résultat adéquat de ma when(someDao.findById(id)).thenReturn(alsoDeeplyNestedObject) pour éviter NullPointers si la conversion dissout également le structure imbriquée

Solution 2: constructeur supplémentaire dans le DTO pour convertir l'entité de domaine en DTO

Ma deuxième solution serait d'ajouter un constructeur supplémentaire à mon entité DTO pour convertir l'objet dans le constructeur.

public class SomeDto {

 // ... some attributes

 public SomeDto(SomeEntity entity) {
  this.attribute = entity.getAttribute();
  // ... nesting convertion & convertion of lists and arrays
 }

}

Avantages:

  • aucune classe supplémentaire pour la conversion nécessaire
  • conversion cachée dans l'entité DTO -> le code de service est plus petit

Les inconvénients:

  • utilisation de new SomeDto() dans le code de service et je dois donc fournir la structure d'objet imbriquée correcte à la suite de mon someDao mocking.

Solution 3: utiliser le convertisseur de Spring ou tout autre bean externalisé pour cette conversion

Si récemment vu que Spring propose une classe pour des raisons de conversion: Converter<S, T> Mais cette solution représente toutes les classes externalisées qui effectuent la conversion. Avec cette solution, j'injecte le convertisseur à mon code de service et je l'appelle lorsque je veux convertir l'entité de domaine en mon DTO.

Avantages:

  • facile à tester car je peux me moquer du résultat lors de mon test
  • séparation des tâches -> une classe dédiée fait le travail

Les inconvénients:

  • n'évolue pas autant à mesure que mon modèle de domaine se développe. Avec beaucoup d'entités, je dois créer deux convertisseurs pour chaque nouvelle entité (-> convertir DTO en droit et en DTO)

Avez-vous plus de solutions à mon problème et comment le gérez-vous? Créez-vous un nouveau convertisseur pour chaque nouvel objet de domaine et pouvez "vivre" avec la quantité de classes dans le projet?

Merci d'avance!

19
rieckpil

Solution 1: méthode privée dans la couche de service pour la conversion

Je suppose que La solution 1 ne fonctionnera pas bien, car vos DTO sont orientés domaine et non service. Il est donc probable qu'ils soient utilisés dans différents services. Une méthode de mappage n'appartient donc pas à un seul service et ne doit donc pas être implémentée dans un seul service. Comment réutiliseriez-vous la méthode de mappage dans un autre service?

La solution 1. fonctionnerait bien si vous utilisez des DTO dédiés par méthode de service. Mais plus à ce sujet à la fin.

Solution 2: constructeur supplémentaire dans le DTO pour convertir l'entité de domaine en DTO

En général, une bonne option, car vous pouvez voir le DTO comme un adaptateur à l'entité. En d'autres termes: le DTO est une autre représentation d'une entité. De telles conceptions enveloppent souvent l'objet source et fournissent des méthodes qui vous donnent une autre vue sur l'objet enveloppé.

Mais un DTO est un objet de transfert de données afin qu'il puisse être sérialisé tôt ou tard et envoyé sur un réseau, par ex. en utilisant capacités de communication à distance du printemps . Dans ce cas, le client qui reçoit ce DTO doit le désérialiser et a donc besoin des classes d'entité dans son chemin de classe, même s'il n'utilise que l'interface du DTO.

Solution 3: utiliser Spring's Converter ou tout autre bean externalisé pour cette conversion

La solution 3 est la solution que je préférerais également. Mais je créerais un Mapper<S,T> interface qui est responsable du mappage de la source à la cible et vice versa. Par exemple.

public interface Mapper<S,T> {
     public T map(S source);
     public S map(T target);
}

L'implémentation peut être effectuée en utilisant un cadre de mappage comme modelmapper .


Vous avez également dit qu'un convertisseur pour chaque entité

n'évolue pas autant à mesure que mon modèle de domaine se développe. Avec beaucoup d'entités, je dois créer deux convertisseurs pour chaque nouvelle entité (-> convertir DTO en droit et en DTO)

Je doute que vous n'ayez à créer que 2 convertisseurs ou un mappeur pour un DTO, car votre DTO est orienté domaine.

Dès que vous commencez à l'utiliser dans un autre service, vous reconnaîtrez que l'autre service doit généralement ou ne peut pas renvoyer toutes les valeurs que le premier service fait. Vous allez commencer à implémenter un autre mappeur ou convertisseur pour chaque autre service.

Cette réponse deviendrait trop longue si je commence par les avantages et les inconvénients des DTO dédiés ou partagés, donc je ne peux que vous demander de lire mon blog avantages et inconvénients des conceptions de couches de service .

14
René Link

J'aime la troisième solution de la réponse acceptée.

Solution 3: utiliser Spring's Converter ou tout autre bean externalisé pour cette conversion

Et je crée DtoConverter de cette façon:

marqueur de classe BaseEntity:

public abstract class BaseEntity implements Serializable {
}

marqueur de classe AbstractDto:

public class AbstractDto {
}

Interface GenericConverter:

public interface GenericConverter<D extends AbstractDto, E extends BaseEntity> {

    E createFrom(D dto);

    D createFrom(E entity);

    E updateEntity(E entity, D dto);

    default List<D> createFromEntities(final Collection<E> entities) {
        return entities.stream()
                .map(this::createFrom)
                .collect(Collectors.toList());
    }

    default List<E> createFromDtos(final Collection<D> dtos) {
        return dtos.stream()
                .map(this::createFrom)
                .collect(Collectors.toList());
    }

}

Interface CommentConverter:

public interface CommentConverter extends GenericConverter<CommentDto, CommentEntity> {
}

Implémentation de la classe CommentConveter:

@Component
public class CommentConverterImpl implements CommentConverter {

    @Override
    public CommentEntity createFrom(CommentDto dto) {
        CommentEntity entity = new CommentEntity();
        updateEntity(entity, dto);
        return entity;
    }

    @Override
    public CommentDto createFrom(CommentEntity entity) {
        CommentDto dto = new CommentDto();
        if (entity != null) {
            dto.setAuthor(entity.getAuthor());
            dto.setCommentId(entity.getCommentId());
            dto.setCommentData(entity.getCommentData());
            dto.setCommentDate(entity.getCommentDate());
            dto.setNew(entity.getNew());
        }
        return dto;
    }

    @Override
    public CommentEntity updateEntity(CommentEntity entity, CommentDto dto) {
        if (entity != null && dto != null) {
            entity.setCommentData(dto.getCommentData());
            entity.setAuthor(dto.getAuthor());
        }
        return entity;
    }

}
9
Maksym Pecheniuk

À mon avis, la troisième solution est la meilleure. Oui, pour chaque entité, vous devrez créer deux nouvelles classes de conversion, mais lorsque vous arriverez au moment des tests, vous n'aurez pas beaucoup de maux de tête. Vous ne devriez jamais choisir la solution qui vous obligera à écrire moins de code au début, puis à en écrire beaucoup plus quand il s'agit de tester et de maintenir ce code.

6
Petar Petrov

J'ai fini par NE PAS utiliser de bibliothèque de mappage magique ou de classe de convertisseur externe, mais simplement ajouter un petit bean qui a convert méthodes de chaque entité à chaque DTO dont j'ai besoin. La raison en est que la cartographie était:

soit stupidement simple et je copierais simplement quelques valeurs d'un champ à un autre, peut-être avec une petite méthode utilitaire,

o était assez complexe et serait plus compliqué à écrire dans les paramètres personnalisés d'une bibliothèque de mappage générique, par rapport à la simple écriture de ce code. C'est par exemple dans le cas où le client peut envoyer du JSON mais sous le capot, cela est transformé en entités, et lorsque le client récupère à nouveau l'objet parent de ces entités, il est reconverti en JSON.

Cela signifie que je peux simplement appeler .map(converter::convert) sur n'importe quelle collection d'entités pour récupérer un flux de mes DTO.

Est-il évolutif de tout avoir dans une seule classe? Eh bien, la configuration personnalisée de ce mappage devrait être stockée quelque part, même si vous utilisez un mappeur générique. Le code est généralement extrêmement simple, à l'exception de quelques cas, donc je ne suis pas trop inquiet de voir cette classe exploser en complexité. Je ne m'attends pas non plus à avoir des dizaines d'entités supplémentaires, mais si je le faisais, je pourrais regrouper ces convertisseurs dans une classe par sous-domaine.

Ajouter une classe de base à mes entités et DTO pour que je puisse écrire une interface de convertisseur générique et l'implémenter par classe n'est tout simplement pas nécessaire (encore?) Pour moi.