Rendre vos super services incroyablement génériques

Présentation

Jusque-là nous avons standardisé la gestion de nos conversions. Si l’on regarde nos repository sont standardisés grâce à JPA. Cependant nos services ne sont pas standardisés et il peut être utile de le faire dans certains cas. À l’heure d’aujourd’hui je ne vois pas d’intérêt de le faire pour les contrôleurs. Ne pas standardiser les contrôleurs permet de contrôler ce qui rentre et qui sort de notre application. Les services eux sont limités par les contrôleurs, on peut donc les standardiser, dans le pire des cas le code de la classe abstraite ne sera jamais appelé. Allez, voyons la suite.

CrudService

Nous allons créer notre classe abstraite CRUDService. Elle va offrir une base à tous les services afin d’avoir un CRUD. Voici notre classe :

package com.raphlys.common;

import java.util.Collections;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;


public abstract class CrudService<D extends IDto<K>, M extends IModel<K>, K extends Object> {

	@Autowired
	private ConverterDtoModel<M, D> converter;

	@Autowired
	private JpaRepository<M, K> repository;

	protected JpaRepository<M, K> getRepository() {
		return repository;
	}

	public List<D> findAll() {
		return toDtos(repository.findAll());
	}

	public D get(K id) {
		return toDto(repository.findById(id).orElse(null), getPropertiesReadDto());
	}

	public boolean delete(K id) {
		repository.deleteById(id);
		return true;
	}

	public D create(D dto) {
		return toDto(repository.save(toModel(dto, getPropertiesModel())), getPropertiesCreateDto());
	}

	public D update(D dto) {
		return toDto(repository.save(toModel(dto, getPropertiesModel())), getPropertiesUpdateDto());
	}

	public List<Class<?>> getPropertiesUpdateDto() {
		return Collections.emptyList();
	}

	public List<Class<?>> getPropertiesCreateDto() {
		return Collections.emptyList();
	}

	public List<Class<?>> getPropertiesReadDto() {
		return Collections.emptyList();
	}

	public List<Class<?>> getPropertiesModel() {
		return Collections.emptyList();
	}

	protected D toDto(M model, List<Class<?>> classes) {
		return converter.toDto(model, classes);
	}

	protected M toModel(D model, List<Class<?>> classes) {
		return converter.toModel(model, classes);
	}

	protected D toDto(M model) {
		return converter.toDto(model);
	}

	protected M toModel(D model) {
		return converter.toModel(model);
	}

	protected List<D> toDtos(List<M> models) {
		return converter.toDtos(models);
	}

	protected List<M> toModels(List<D> dtos) {
		return converter.toModels(dtos);
	}

}

C’est une sacré classe abstraitre. On va la décortiquer ligne par ligne :

public abstract class CrudService<D extends IDto<K>, M extends IModel<K>, K extends Object> {

Donc on a affaire à une classe abstraire qui a besoin de 3 types générique D, M et K. D correspond au Dto, M au model et K a l’ID. On aura modifié les interfaces en conséquence.

@Autowired
private ConverterDtoModel<M, D> converter;

On y injecte notre converter générique avec les classes M et D. Celui-ci vous l’avez compris va nous permettre de transformer nos Dto en Model lors des enregistrements et nos model en DTO lors de la lecture.

@Autowired
private JpaRepository<M, K> repository;

On injecte le repository, c’est à dire la classe Springboot qui va nous permettre de lire nos objets de type M mais aussi de les écrire. C’est indispensable.

protected JpaRepository<M, K> getRepository() {
	return repository;
}

Un petit principe d’encapsulation, on ne veut pas qu’un enfant modifie le repository utilisé. On va pouvoir aussi redéfinir cette méthode plus tard et caster le repository dans un type plus spécifique si l’on y a ajouté des méthodes.

public List<D> findAll() {
	return toDtos(repository.findAll());
}

Là, ça commence à être intéréssant. On standardise ici la méthode findAll, qui va nous rapporter tous les éléments de la table (Il est fort possible qu’en production, vous ayez à paginer cette méthode). On passe par une méthode toDtos, interne qui fait appel au converter et qui est partagé avec les classes filles. Ainsi le code dans la classe fille sera plus lisible.

public D get(K id) {
	return toDto(repository.findById(id).orElse(null), getPropertiesReadDto());
}

On standardise la méthode get, à laquelle on passe en paramètre l’identifiant de type K et on retourne le Dto. On fait appel à la méthode getPropertiesReadDto(), cette méthode retourne la liste des attributs complexes que l’on voudra convertir pour la lecture. Par défaut cette méthode retourne une liste vide, mais on pourra la redéfinir dans la classe fille. N’est ce pas merveilleux?

public boolean delete(K id) {
	repository.deleteById(id);
	return true;
}

La méthode delete qui prend en paramètre l’id, vous pourriez créer les méthodes delete qui prennent en paramètre un Dto ou un Model pour des cas particuliers. Mais là on a notre petit delete qui va bien.

public D create(D dto) {
	return toDto(repository.save(toModel(dto, getPropertiesModel())), getPropertiesCreateDto());
}

La méthode create, qui permet de spécifier les propriétés complexes que l’on souhaite enregistrer grâce à getPropertiesModel() et celle que l’on retourne grâce à getPropertiesCreateDto(). J’ai fait le choix ici de mettre la même méthodes getPropertiesModel pour la création et la mise à jour, mais rien ne vous empêche d’avoir deux méthodes. Interdire à la création d’avoir des roues et donc obliger à la mise à jour de mettre des roues. Ou inversement une fois que l’on a définis des roues à le création on ne peut en changer…. Faites une classe abstraites suffisament générique afin de l’utiliser le plus souvent possible, mais pas trop afin de ne pas avoir une usine à gaz.

public D update(D dto) {
	return toDto(repository.save(toModel(dto, getPropertiesModel())), getPropertiesUpdateDto());
}

On a à peu prét la même méthode que pour la création. On change juste les propriétés à retourner.

public List<Class<?>> getPropertiesUpdateDto() {
	return Collections.emptyList();
}

public List<Class<?>> getPropertiesCreateDto() {
	return Collections.emptyList();
}

public List<Class<?>> getPropertiesReadDto() {
	return Collections.emptyList();
}

public List<Class<?>> getPropertiesModel() {
	return Collections.emptyList();
}

Là ce sont des méthodes qui vont nous permettre de personnaliser nos méthodes dans la classe fille. Par défaut elles retournent toutes une liste vide, mais au besoin on peut faire en sorte qu’elles retournent une liste avec des classes différentes.

protected D toDto(M model) {
	return converter.toDto(model);
}

protected M toModel(D model) {
	return converter.toModel(model);
}

protected List<D> toDtos(List<M> models) {
	return converter.toDtos(models);
}

protected List<M> toModels(List<D> dtos) {
	return converter.toModels(dtos);
}

Le converter est en private, on ne peut pas le changer et les classes filles ne peuvent pas utiliser ses méthodes sans passer par ces quatre méthodes. C’est plus propre du coté des classes filles.

Les interfaces

J’ai modifié les interfaces IDto et Imodel. Maintenant elles ressemblent à ça :

package com.raphlys.common;

public interface IDto<K> {

	public K getId();
}
package com.raphlys.common;

public interface IModel<K> {

	public K getId();
	
}

Rien de particulier à dire, si ce n’est que se sont des interfaces génériques. Il nous faudra donc simplement spécifié dans nos implémentations le type de l’id, en l’occurence Long. Et d’implémenter getId(), qui l’est déjà dans les faits.

Notre premier service

package com.raphlys.service;

import org.springframework.stereotype.Service;

import com.raphlys.common.CrudService;
import com.raphlys.dto.TruckDto;
import com.raphlys.model.TruckModel;

@Service
public class TruckService extends CrudService<TruckDto, TruckModel, Long>{
    	@Override
	public List<Class<?>> getPropertiesReadDto() {
		return List.of(WheelDto.class);
	}
	
	@Override
	public List<Class<?>> getPropertiesModel() {
		return List.of(WheelModel.class);
	}
}

Voilà on a notre service, qui est capable de faire un CRUD. On a fait en sorte qu’à la lecture on prenne en compte les roues et qu’à la création/modification aussi. Mais à la création/modification on ne retournera pas les roues. C’est un choix que vous pouvez modifier à votre guise.

Pour l’utilisation dans le contrôleur rien n’a été changé si ce n’est le fait que l’on passe de getAll à findAll.

Conclusion

Avec ces bases là vous pouvez ajouter de nombreux services Crud, vous pouvez ajouter beaucoup de classe de quelques lignes qui ferront presque tout. Maintenant que l’on a simplifié la mise en place d’un CRUD, ne faudrait il pas le sécuriser? On voit ça la semaine prochaine?

Votre incroyable convertisseur Dto <=> Model du tonnerre !!!

Sortez couvert avec Spring Security

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.