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:
Les inconvénients:
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éeSolution 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:
Les inconvénients:
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:
Les inconvénients:
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!
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 .
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;
}
}
À 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.
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.