DEV Community

roookeee
roookeee

Posted on

abandon your factories - data conversion with datus

(disclaimer: I am the author of datus)

You are working with Java again, maybe you are developing a new (micro)service oriented architecture and are currently integrating another service, PersonService, into your AService. You receive a PersonResource and as you have learned you take that object and convert it into your own Person object to not rely on data structures that you don't own in your business logic:

//external object
class PersonResource {
    //getters + setters omitted for brevity
    private String firstName;
    private String lastName;
}
//internal object
class Person {
    //getters + setters omitted for brevity
    private String firstName;
    private String lastName;
}

Let's build that handy factory / converter object:

public PersonFactory {
    public Person create(PersonResource resource) {
      Person person = new Person();
      person.setFirstName(resource.getFirstName());
      person.setLastName(resource.setLastName());
      return person;
    }
}

That works, but what about the other 5-10 services and their 15-30 data objects? All this boiler plate is quite annoying, isn't it? And just now you realize a PersonResource can be null so your test coverage tool complains about not writing a test for such a trivial class.

What if there were a way to declare how the conversion is done, define how its done but not implement it ? Enter datus:

Mapper<PersonResource, Person> mapper = Datus.forTypes(PersonResource.class, Person.class)
    .mutable(Person::new)
    .from(PersonResource::getFirstName).into(Person::setFirstName)
    .from(PersonResource::getLastName).into(Person::setLastName)
    .build();

Pretty concise and self-explanatory - the declarative approach looks fine for now.
But what about that null check I was talking about? Let's implement that, but how?

Mapper<PersonResource, Person> mapper = Datus.forTypes(PersonResource.class, Person.class)
    .mutable(Person::new)
    .from(PersonResource::getFirstName).into(Person::setFirstName)
    .from(PersonResource::getLastName).into(Person::setLastName)
    .build();

Mapper<PersonResource, Optional<Person>> notNullMapper = 
    mapper.predicateInput(Objects::nonNull)

Well that was easy, but the project evolves: the lastName can be null and should be upper-cased in your service. You don't question that bizarre requirement and add the new functionality:

Mapper<PersonResource, Person> mapper = Datus.forTypes(PersonResource.class, Person.class)
    .mutable(Person::new)
    .from(PersonResource::getFirstName).into(Person::setFirstName)
    .from(PersonResource::getLastName)
         .given(Objects::nonNull, ln -> ln.toUpperCase()).orElseNull()
         .into(Person::setLastName)
    .build();

Mapper<PersonResource, Optional<Person>> notNullMapper = 
    mapper.predicateInput(Objects::nonNull)

Fancy! But you don't want any Person objects without a lastName to be valid, so let's add a predicate for the output object (of course you could already do it on the input object, but let me show some more features ;) ):

Mapper<PersonResource, Person> mapper = Datus.forTypes(PersonResource.class, Person.class)
    .mutable(Person::new)
    .from(PersonResource::getFirstName).into(Person::setFirstName)
    .from(PersonResource::getLastName)
         .given(Objects::nonNull, ln -> ln.toUpperCase()).orElseNull()
         .into(Person::setLastName)
    .build();

Mapper<PersonResource, Optional<Person>> predicatedMapper = 
    mapper.predicate(Objects::nonNull, p -> p.getLastName() != null)

So what did we gain?

  • declare your mappings, don't implement them
  • no need for unit tests for such trivial nullabilty checks: they are declared and once the need for them arises simply added (this is an opinioated point)
  • no more factory classes just one interface
  • datus even works for immutable destination objects (checkout the readme examples)
  • no reflection is used, everything is statically type checked

Oh and did I mention that Mapper<A,B> also provides List<B> convert(List<A> input) and Map<A,B> convertToMap(List<A> input) out of the box? Find out more in the readme or the extended user guide that only takes about 15 minutes to read!

You like yourself a more implicit / automatic / annotation driven conversion process? Checkout alternatives to datus like MapStruct or ModelMapper.

Thank you for your time, hit me up if you have any questions :)

Top comments (0)