Introduction
Aujourd’hui, on va parler des convertisseurs Dto <=> Model. C’est indispensable! La plupart de vos services ne vont pas se contenter de faire de la conversion. Il va y avoir des vérifications, peut être des calculs … C’est pourquoi on va sortir toutes les conversions dans d’autres services spécialisés ans la conversion. Le service Converter n’est rien d’autre qu’un service Sprinbboot.
La classe Abstraite
Alors directement dans le concret. Ce que nous allons créer une classe abstraire qui sera la base de tous nos converters. Je vous présente le code et je vous l’explique :
package com.raphlys.common;
import java.util.Collection;
import java.util.List;
public abstract class ConverterDtoModel<M extends IModel, D extends IDto> {
private final Class<D> dtoClass;
private final Class<M> modelClass;
public ConverterDtoModel(Class<D> dtoClass, Class<M> modelClass) {
this.dtoClass = dtoClass;
this.modelClass = modelClass;
}
protected abstract D internalToDto(M model, List<Class<?>> classes);
protected abstract M internalToModel(D dto, List<Class<?>> classes);
public final D toDto(M model, List<Class<?>> classes) {
return internalToDto(model, classes.stream().filter(cla -> cla.equals(this.dtoClass)).toList());
}
public final M toModel(D dto, List<Class<?>> classes) {
return internalToModel(dto, classes.stream().filter(cla -> cla.equals(this.modelClass)).toList());
}
public final D toDto(M model) {
if (model == null) {
return null;
}
return toDto(model, List.of());
}
public final M toModel(D dto) {
if (dto == null) {
return null;
}
return toModel(dto, List.of());
}
public List<D> toDtos(Collection<M> models, List<Class<?>> classes) {
return models.stream().map(m -> toDto(m, classes)).toList();
}
public List<M> toModels(Collection<D> dtos, List<Class<?>> classes) {
return dtos.stream().map(m -> toModel(m, classes)).toList();
}
public List<D> toDtos(Collection<M> models) {
return toDtos(models, List.of());
}
public List<M> toModels(Collection<D> dtos) {
return toModels(dtos, List.of());
}
}
On ne s’attarde pas sur les imports qui sont assez standards, c’est juste pour la gestion des collections et Lists. Juste, on note le package com.raphlys.common
on a donc affaire à une classe utilisable de façon commune, quand on fait des modifications sur cette classe, on a conscience que ça peut impacter beaucoup de code.
public abstract class ConverterDtoModel<M extends IModel, D extends IDto<?>> {
- public => La classe est donc utilisable partout dans le code.
- abstract => C’est une classe qui ne sera pas instanciée
- class => C’est une class
- ConverterDtoModel => Le nom de la classe, c’est donc un Converter qui s’occupe des Dto des models.
- <M extends IModel, D extends IDto<?>> => On a affaire à une classe générique abstraite.
- M : représente notre Model, les classes utilisées devront étendre l’interface IModel. IModel est une interface générique, ici elle est vide, mais elle pourrait contenir des méthodes l’Id du modèle… (
public interface IModel {}
) - D : représente notre Dto, les classes utilisées devront étendre l’interface IDto. On peut aussi imaginer ajouter l’id du dto. (
public interface IDto {}
)
- M : représente notre Model, les classes utilisées devront étendre l’interface IModel. IModel est une interface générique, ici elle est vide, mais elle pourrait contenir des méthodes l’Id du modèle… (
protected abstract D internalToDto(M model, List<Class<?>> classes);
protected abstract M internalToModel(D dto, List<Class<?>> classes);
Ce sont les deux méthodes abstraites que devront implémenter tout les converters. Elles prennent en paramètre l’objet à convertir et une liste de classe. Vous l’aurez compris l’objet à convertir est indispensable, afin d’obtenir le Dto ou le Modèle. Quant à la liste des classes, elle me permet d’indiquer au converter, si je veux qu’il me convertisse aussi les objets liés, les roues, le volant… Ainsi pour chaque cas je peux contrôler où je m’arrête.
public final D toDto(M model, List<Class<?>> classes) {
return internalToDto(model, classes.stream().filter(cla -> cla.equals(this.dtoClass)).toList());
}
public final M toModel(D dto, List<Class<?>> classes) {
return internalToModel(dto, classes.stream().filter(cla -> cla.equals(this.modelClass)).toList());
}
Ces deux méthodes permettent de supprimer la classe actuellement en cours de conversion. Ainsi on évite d’avoir des appels cycliques, par exemple on transforme un camion, qui va transformer un volant, qui va transformer un camion…
public final D toDto(M model) {
if (model == null) {
return null;
}
return toDto(model, List.of());
}
public final M toModel(D dto) {
if (dto == null) {
return null;
}
return toModel(dto, List.of());
}
Là, ce sont simplement des méthodes sans paramètres qui appels les méthodes avec paramètres et qui mette par défaut une liste vide. On vérifie aussi par la même occasion si l’objet est null. Je ne vois pas de difficulté particulière par rapport à ces méthodes.
public List<D> toDtos(Collection<M> models, List<Class<?>> classes) {
return models.stream().map(m -> toDto(m, classes)).toList();
}
public List<M> toModels(Collection<D> dtos, List<Class<?>> classes) {
return dtos.stream().map(m -> toModel(m, classes)).toList();
}
Dans ces méthodes, on s’occupe des collections. Il arrivera régulièrement que l’on ait besoin de transformer une collection. On prend en paramètre le type Collection qui est le plus générique possible, et on retourne une liste qui est au contraire l’interface la plus spécifique possible. En faisant ainsi on s’assure que notre méthode est la plus générique et que si des fois en sortie on avait besoin de méthode spécifique à la List, on n’est pas besoin de caster.
Je ne détaille pas les 2 autres méthodes qui sont les mêmes que les précédentes, sauf que l’on ajoute une Liste vide.
L’implémentation
On fait en sorte que toutes nos classes Dto étendent IDto et toutes nos classes models étendent IModel. Je vous mets deux exemples, mais il faut le faire pour toutes les classes concernées.
public class TruckModel implements IModel{
public class TruckDto implements IDto {
On va créer un package com.raphlys.converter
et on met le fichier suivant (TruckConverter) à l’intérieur :
@Service
public class TruckConverter extends ConverterDtoModel<TruckModel, TruckDto> {
public TruckConverter() {
super(TruckDto.class, TruckModel.class);
}
@Autowired
private WheelConverter wheelConverter;
@Autowired
private JpaRepository<TruckModel, Long> repository;
@Override
public TruckDto internalToDto(TruckModel model, List<Class<?>> classes) {
TruckDto dto = new TruckDto();
dto.setId(model.getId());
dto.setBrand(model.getBrand());
dto.setName(model.getName());
if (classes.contains(WheelDto.class)) {
dto.setWheels(wheelConverter.toDtos(model.getWheels(), classes));
}
return dto;
}
@Override
public TruckModel internalToModel(TruckDto dto, List<Class<?>> classes) {
TruckModel model = new TruckModel();
if (dto.getId() != null) {
model = repository.findById(dto.getId()).get();
}
model.setBrand(dto.getBrand());
model.setName(dto.getName());
if (classes.contains(TruckModel.class)) {
model.setWheels(wheelConverter.toModels(dto.getWheels(), classes));
}
return model;
}
}
Analysons ensemble ce code.
On remarque immédiatement que c’est un service grâce à l’annotation @Service
. On le lie à d’autres converter que l’on injecte grâce à l’annotation @Autowired
, qui permettra au besoin de convertir une propriété de l’objet. On ajoute aussi le repository, ainsi à l’update d’un objet on s’assure de ne pas modifier une propriété de la classe sans le vouloir.
La méthode internalToDto
Ici on instancie un objet et on set chacune des propriétés. Si c’est une des propriétés est un objet complexe alors on vérifie que si l’on souhaite le convertir (sinon la valeur sera null).
if (classes.contains(WheelDto.class)) {
dto.setWheels(wheelConverter.toDtos(model.getWheels(), classes));
}
On vérifie que la liste des type classe contient la classe d’un des attributs, si c’est le cas on demande au converter de faire la convertion.
La méthode internalToModel
Ici aussi on instancie un objet et on set les propriétés. Mais si le dto a un Id, on récupère l’objet dans la BDD et ensuite on set les propriétés. Ceci afin de ne pas permettre des propriétés lors de la mise à jour de l’objet.
Et comme précédement si l’on veut ajouter une propriété de l’objet, on vérifie que la classe est présente dans la liste passé en paramètre. Si c’est le cas on fait la conversion.
L’utilisation
Au getAll
public Collection<TruckDto> getAll() {
return truckRepository.findAll().stream().map(truck -> truckConverter.toDto(truck)).toList();
}
On a remplacé la méthode internet toDto par la méthode du converter. Le changement ici n’a aucun impact fonctionnel.
À la création
public Long create(TruckDto truck) {
return truckRepository.save(truckConverter.toModel(truck, List.of(WheelModel.class))).getId();
}
Ici afin de créer notre objet dans la table, on a ajouté la classe WheelModel. Ainsi quand on va créer un objet Truck, celui-ci créera aussi le volant. Ou si l’on met un ID à notre volant. On va créer le lien entre le volant et le camion.
Conclusion
Maintenant vous pouvez créer vos converters Dto/Model. Vous pouvez utiliser des classes génériques afin d’éviter le code dupliqué. Grâce à nos converters nous allons pouvoir simplifier la mise en place d’objet complexe. La semaine prochaine je vous proposerais de rendre vos services génériques, ainsi vous pourrez ajouter un CRUD rapidement pour chaque objet.
Laisser un commentaire