Converters and Populators (Design Patterns)

Converters and Populators (Design Patterns)

Overview

In this tutorial we’re going to cover the following design patterns: converters and populators.

Data objects can be split into two types of representation:

  • Models stored in the database
  • DTO (Data Transfer Object) models used by Facade and Controller levels

Database models usually have more internal information that’s not always should be visible/available to a client.

DTO models usually have a subset of the information stored in database models.

Converter is a design pattern used for conversion between corresponding types. Converter can have one or more populators groupped by some criterion.

Populator is responsible for setting a piece of information to a data object.

There is no doubt that you saw large data objects a lot of times while developing applications. Likely that you don’t need all of its information for all use-cases. In one case you may need only basic information, while in other you may need it fully. Some attributes can be quite heavy to fetch (e.g. if it requires additional database calls or calls to external API). So, it can be pretty convenient to use converters with different sets of populators based on a use-case.

The following picture shows schematically how converters and populators work:

converters and populators

Implementation

Let’s say we have a User entity.

package com.keepcodeclean.model;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class UserModel {
    private String id;
    private String name;
    private String surname;
    private String city;
    private String street;
}
package com.keepcodeclean.dto;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class UserDTO {
    private String name;
    private String surname;
    private String city;
    private String street;
}

Populator

Let’s implement a Populator interface:

package com.keepcodeclean.populator;

public interface Populator<SOURCE, TARGET> {
    void populate(SOURCE source, TARGET target);
}

SOURCE is class for a model object and TARGET is class for DTO.

Then let’s create two populators:

  • UserNamePopulator – sets an information related to a user name (firstName, lastName, etc.)
  • UserAddressPopulator – sets an information related to an address of a user
package com.keepcodeclean.populator.user;

import com.keepcodeclean.populator.Populator;
import com.keepcodeclean.dto.UserDTO;
import com.keepcodeclean.model.UserModel;

public class UserAddressPopulator implements Populator<UserModel, UserDTO> {
    @Override
    public void populate(UserModel userModel, UserDTO userDTO) {
        userDTO.setCity(userModel.getCity());
        userDTO.setStreet(userModel.getStreet());
    }
}
package com.keepcodeclean.populator.user;

import com.keepcodeclean.populator.Populator;
import com.keepcodeclean.dto.UserDTO;
import com.keepcodeclean.model.UserModel;

public class UserNamePopulator implements Populator<UserModel, UserDTO> {
    @Override
    public void populate(UserModel userModel, UserDTO userDTO) {
        userDTO.setName(userModel.getName());
        userDTO.setSurname(userModel.getSurname());
    }
}

Converter

Let’s create a PopulatingConverter class which contain a list of populators and can convert one entity or a list of entities:

package com.keepcodeclean.converter;

import com.keepcodeclean.populator.Populator;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

@Getter
@Setter
public class PopulatingConverter<SOURCE, TARGET> {
    
    private Class<TARGET> targetClass;
    
    private List<Populator> populators = new ArrayList<>();

    public PopulatingConverter(Class<TARGET> targetClass) {
        this.targetClass = targetClass;
    }

    public static <SOURCE, TARGET> PopulatingConverter<SOURCE, TARGET> of(Class<TARGET> targetClass) {
        return new PopulatingConverter<SOURCE, TARGET>(targetClass);
    }
    
    public TARGET convert(SOURCE source) {
        TARGET target = createFromClass();

        for (Populator populator : populators) {
            populator.populate(source, target);
        }
        
        return target;
    }
    
    public List<TARGET> convertAll(List<SOURCE> objectsToConvert) {
        List<TARGET> convertedList = new ArrayList<>();
        for (SOURCE objectToConvert : objectsToConvert) {
            convertedList.add(convert(objectToConvert));
        }
        return convertedList;
    }
    
    private TARGET createFromClass() {
        try {
            return targetClass.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void addPopulator(Populator<SOURCE, TARGET> populator) {
        if (Objects.nonNull(populator)) {
            populators.add(populator);
        }
    }

    public <SOURCE, TARGET> PopulatingConverter<SOURCE, TARGET> withPopulator(Populator<SOURCE, TARGET> populator) {
        if (Objects.nonNull(populator)) {
            populators.add(populator);
        }
        return (PopulatingConverter<SOURCE, TARGET>) this;
    }
}

Try it out

Let’s create a test application to try out these patterns.

package com.keepcodeclean;

import com.keepcodeclean.converter.PopulatingConverter;
import com.keepcodeclean.dto.UserDTO;
import com.keepcodeclean.model.UserModel;
import com.keepcodeclean.populator.Populator;
import com.keepcodeclean.populator.user.UserAddressPopulator;
import com.keepcodeclean.populator.user.UserNamePopulator;

import java.util.ArrayList;
import java.util.List;

public class App {


    public static void main(String[] args) {
        Populator<UserModel, UserDTO> userAddressPopulator = new UserAddressPopulator();
        Populator<UserModel, UserDTO> userNamePopulator = new UserNamePopulator();

        PopulatingConverter<UserModel, UserDTO> converter = PopulatingConverter.of(UserDTO.class)
                .withPopulator(userNamePopulator)
                .withPopulator(userAddressPopulator);

        UserModel userModelOne = new UserModel();
        userModelOne.setName("Ivan");
        userModelOne.setSurname("Ivanov");
        userModelOne.setCity("Lviv");
        userModelOne.setStreet("Test Str. 1");

        UserModel userModelTwo = new UserModel();
        userModelTwo.setName("John");
        userModelTwo.setSurname("Doe");
        userModelTwo.setCity("Kyiv");
        userModelTwo.setStreet("Test Str. 2");
        
        UserDTO userDTO = converter.convert(userModelOne);

        System.out.println(userDTO);
        
        List<UserModel> userModels = new ArrayList<>();
        userModels.add(userModelOne);
        userModels.add(userModelTwo);
        List<UserDTO> userDTOList = converter.convertAll(userModels);

        System.out.println(userDTOList);
    }
    
}

Let’s run the application and check the results

UserDTO(name=Ivan, surname=Ivanov, city=Lviv, street=Test Str. 1)

[UserDTO(name=Ivan, surname=Ivanov, city=Lviv, street=Test Str. 1), UserDTO(name=John, surname=Doe, city=Kyiv, street=Test Str. 2)]

As you can see, the objects have been successfully converted into DTOs using a list of specified populators.

Leave a Reply

Your email address will not be published. Required fields are marked *