Bonjour à tous,

Je suis sur un projet personnel en J2EE et je voudrais introduire la notion de DTO afin d'ajouter une couche intermédiaire entre la couche d'accès aux données et la couche présentation JSF.

J'ai bien compris le principe des DTOs, cela sert à diminuer le couplage entre la persistance et la présentation en ne transférant que des objets POJO "light" au lieu des BO annotées et pouvant contenir du code métier non utilisé par les couches plus hautes.
Jusque la tout va bien ...

Je me suis donc penché sur un pattern permettant de faire un mapping BO->DTO et DTO->BO.
Le principe du mapping BO->DTO est trivial:
Je crée un objet DTO que je remplie avec les données du BO correspondant.
Le principe inverse est quand à lui plus délicat:
En effet les BO contiennent beaucoup plus de données que les DTOs. On ne peut donc pas créer une BO de toute pièce en l'initialisant avec les données du DTO.

Exemple simple:
ArticleBO: id, title, creationDate, content
ArticleDTO: id, title, content

Si je crée une BO à partir de la DTO, que j'initialise son id et que je la merge(), j'écraserai l'attribut creationDate.

Il faut donc récupérer l'article BO correspondant par un find(id) puis le modifier avec les données du DTO.

C'est la ou l'idée d'un Mapper m'est venu:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
 
public abstract class Mapper<BO extends AbstractRootEntity, DTO extends BusinessTO> {
 
    protected static Mapper instance; //singleton
 
    public abstract void mapDTOtoBO(DTO dto, BO bo);
    public abstract void mapBOtoDTO(BO bo, DTO dto);
 
}
Ceci est un Mapper abstrait qui va être la base de l'héritage de tous les Mapper.
AbstractRootEntity est la super class abstraite des BO, BusinessTO pour les DTO.

Voici par exemple un Mapper concret:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
public class UserSiteMapper extends Mapper<SiteUser, SiteUserDTO>{
 
    public static UserSiteMapper getInstance() {
        if(instance == null)
            instance = new UserSiteMapper();
        return (UserSiteMapper) instance;
    }
 
    @Override
    public void mapDTOtoBO(SiteUserDTO dto, SiteUser bo) {
        bo.getAccount().setLogin(dto.getPseudo());
    }
 
    @Override
    public void mapBOtoDTO(SiteUser bo, SiteUserDTO dto) {
        dto.setPseudo(bo.getAccount().getLogin());
    }
 
}
Tout cela semble bien mais les choses se compliquent lorsqu'on a des agrégations.
Je reprend un exemple simple de mon projet:
J'ai 3 BO: Article, SiteUser et Comment (un article (Article) a un auteur (SiteUser) et n commentaires (Comment))
J'ai créé les DTO à l'identique niveau architecture mais bien évidement elles contiennent moins de données.

Je sais que c'est long à lire mais j'aimerait bien tout expliquer ^^.
Je voudrais avoir vos points de vue sur ces implémentations:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
 
public class ArticleMapper extends Mapper<Article, ArticleTO> {
 
    public static ArticleMapper getInstance() {
        if (instance == null) {
            instance = new ArticleMapper();
        }
        return (ArticleMapper) instance;
    }
    private static CommentMapper commentMapperInstance;
 
    public ArticleMapper() {
        commentMapperInstance = new CommentMapper();
    }
 
    @Override
    public void mapDTOtoBO(ArticleTO ato, Article abo) {
        //je modifie le BO avec les données du DTO
        //je ne modifie pas l'auteur car il est dans la dto que pour être affiché et non modifié
        abo.setContent(ato.getContent());
        //ici ça devient compliqué car je dois savoir quels sont les commentaires à ajouter ou a modifier
        for (CommentTO cto : ato.getComments()) {//pour tout les commentaires de l'article ato
            Long id = cto.getId();
            if (id != null) {//le commentaire existe en temps que BO
                for (Comment cbo : abo.getComments()) {//je le cherche dans l'article abo
                    if (cbo.getId() == id) {
                        commentMapperInstance.mapDTOtoBO(cto, cbo);
                    }
                }
            } else {//sinon j'ajoute un commentaire cbo qui sera persisté en cascade plus tard
                Comment cbo = new Comment();
                commentMapperInstance.mapDTOtoBO(cto, cbo);
                abo.getComments().add(cbo);
            }
        }
    }
 
    @Override
    public void mapBOtoDTO(Article abo, ArticleTO ato) {
        //je remplie mon DTO avec les données de mon BO
        ato.setCreationDate(abo.getCreationDate());
        //j'utilise les mapper annexes
        UserSiteMapper.getInstance().mapBOtoDTO(abo.getCreator(), (SiteUserTO) ato.getAutor());
        ato.setTitle(abo.getTitle());
        for (Comment cbo : abo.getComments()) {//pour chaque commentaires de ato
            CommentTO cto = new CommentTO();//je crée un commentaire DTO
            commentMapperInstance.mapBOtoDTO(cbo, cto);
            ato.getComments().add(cto);//que j'ajoute a l'article DTO
        }
 
    }
    //j'ai fait cette subclass car elle n'est utilisée que dans la classe ArticleMapper
    private class CommentMapper extends Mapper<Comment, CommentTO> {
 
        @Override
        public void mapDTOtoBO(CommentTO cto, Comment cbo) {
            cbo.setContent(cto.getContent());
        }
 
        @Override
        public void mapBOtoDTO(Comment cbo, CommentTO cto) {
            cto.setId(cbo.getId());
            cto.setContent(cbo.getContent());
        }
    }
}
Voila donc où j'en suis... Je me rend compte que créer cette couche intermédiaire est un boulot de fourmi et je ne sais pas si mon design est optimal ou peut être amélioré...
Vos points de vue me seraient super utiles.

Merci beaucoup