Java: Melhorando o mapeamento de classes de domínio com ModelMapper

Há algumas semanas atrás estava atuando em um projeto onde tínhamos que lidar com grandes classes de domínios (beans, dtos…) daquelas com mais de 50 variáveis de vários tipos. Sabemos que trabalhar com POJOS deste tamanho para conversão (dtos para beans e vice versa) não é uma tarefa fácil se tivermos que fazer manualmente toda a lógica de mapeamento.

Precisava de uma ferramenta que pudesse, sem muito esforço, identificar, de maneira inteligente, a melhor forma de mapear esses dois objetos. Utilizei por algum tempo um uma implementação chamada MapStruct. Sua implementação me ajudou bastante no mapeamento em alguns projetos, porém com classes um pouco menores:

public class ProcessorTransactionDto implements Serializable {
    private String tipoOperacao;
    private String agencia;
    private String conta_DV;
    private BigDecimal valor;
    private String NSU;
    private String horaLocal;
    private String dataLocal;
    private String codigoEstabelecimento;
    private String nomeEstabelecimento;
    private String codigoMoeda;
    private String numeroCartao;
    private String dadosDoChip;
    private String senha;

//getters and setters

A implementação muito simples criando uma interface e anotando com @mapper:

@Mapper
public interface ProcessorTransactionMapper {
    @Mappings({@Mapping(target = "agency", source = "agencia"),
                @Mapping(target = "codeAccountDV", source = "conta_DV"),
                @Mapping(target = "processorNsu", source = "NSU"),
                @Mapping(target = "uidCode", source = "uidCode"),
                @Mapping(target = "shopCode", source = "codigoEstabelecimento"),
                @Mapping(target = "shopName", source = "nomeEstabelecimento"),
                @Mapping(target = "currency", source = "codigoMoeda"),
                @Mapping(target = "chipData", source = "dadosDoChip"),
                @Mapping(target = "password", source = "senha"),
                @Mapping(target = "cardNumber", source = "numeroCartao"))})
    TransactionBean transactionToBean(ProcessorTransactionDto dto);

Bem simples né? Posteriormente, para quem usa o maven nesse caso, era só rodar um clean install e tudo certo, a implementação criava toda a lógica pra gente numa classe dentro do target.

Porém, os contras do MapStruct naquele momento é que, ele não era inteligente suficiente para identificar as variáveis de nomes um pouco diferentes. Além disso se você tivesse uma classe com várias outras classes de domínios e a outra não, precisava explicitar isso no source ou no target dentro do mapping da interface. Então para um projeto com uma classe de mais de 50 variáveis em um dto e um bean com algumas classes de domínio que encapsulasse algumas outras variáveis (somente para organização mesmo) iria ter bastante trabalho pra mapear isso.

MerchantCheckoutDto.class:

public class MerchantCheckoutDto implements Serializable {

    private CardType cardType;
    private TransactionType transactionType;
    private String cardNumber;
    private String expireMonth;
    private String expireYear;
    private String cardCodeSecurity;
    private String cardBrand;
    private BigDecimal subTotalAmount;
    private BigDecimal deliveryAmount;
    private BigDecimal totalAmount;
    private Integer currency;
    private String orderId;
    private String merchantTransactionId;
    private String clientIpAddress;
    private LocalDateTime dateTransaction;
    private TransactionChannel transactionChannel;
    private String invoiceNumber;
    private String orderNumber;
    private String shopName;
    private String bankCode;
    private String accountNumber;
    private String mandateReferenceNumber;
    private String mandateType;
    private String customerId;
    private String customerName;
    private String customerCompany;
    private String customerAddress;
    private String customerCity;
    private String customerState;
    private String customerZipCode;
    private String customerPhone;
    private String customerFax;
    private String customerEmail;
    private String shippingType;
    private String shippingName;
    private String shippingAddress;
    private String shippingCity;
    private String shippingState;
    private String shippingCountry;
    private String shippingZipCode;
//...
//getters and setters

MerchantCheckoutBean.class:

public class MerchantCheckoutBean implements Serializable {

    private TransactionBean transaction;
    private CustomerBean customer;
    private ShippingBean shipping;

//getters and setters

Para resolver esse problema e alguns outros (performance também), encontrei o ModelMapper. O objetivo do ModelMapper é facilitar o mapeamento de objetos, determinando automaticamente como um modelo de objeto é mapeado para outro, com base em convenções, da mesma maneira que um humano faria – ao mesmo tempo em que fornece uma API simples e segura para refatoração para lidar com casos de uso específicos. Mas o que mais me chamou a atenção foi que o ModelMapper analisa seu modelo de objeto para determinar de maneira inteligente como os dados devem ser mapeados. Não é necessário mapeamento manual. O ModelMapper faz a maior parte do trabalho para você, projetando e nivelando automaticamente modelos complexos. Ah, e utiliza features do Java 8 para o mapeamento, como o stream por exemplo.

Para demonstrar como funciona o ModelMapper, precisamos importar a dependência do Maven (caso utilize o maven):

        <dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>${modelMapper.version}</version>
        </dependency>

A versão você pode pesquisar pela mais recente no site da central do maven.

Vamos reaproveitar as classes MerchantCheckoutDto e MerchantCheckoutBean, mostradas anteriormente. Nesse exemplo, estou utilizando o Spring boot para gerenciar os beans e poder injetar o ModelMapper na classe onde vamos utilizar ele. O trecho ficaria mais ou menos assim na sua classe de configuração:

@Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
    }

Caso você não esteja utilizando o Spring, pode simplesmente instanciar o Objeto na classe que será utilizado.

ModelMapper modelMapper = new ModelMapper();

E depois é só fazer algo do tipo:

MerchantCheckoutBean bean = mapper.map(merchantCheckout, MerchantCheckoutBean.class);

Quando o método map é chamado, os tipos de origem e destino são analisados para determinar quais propriedades correspondem implicitamente de acordo com uma estratégia de correspondência e outra configuração. Os dados são mapeados de acordo com essas correspondências.

Mesmo quando os objetos de origem e destino e suas propriedades são diferentes, como no exemplo acima, o ModelMapper fará o possível para determinar correspondências razoáveis entre propriedades de acordo com a estratégia de correspondência configurada. Simples assim né!

É isso pessoal, espero que tenham curtido esse artigo. Até a próxima!