roookeee / datus

datus enables you to define a conversion process between two data structures in a fluent functional API
MIT License
41 stars 5 forks source link

Concept: add the ability to map to multiple outputs #20

Closed roookeee closed 5 years ago

roookeee commented 5 years ago

Say you have a Customer object as a field inside your Order object and want to extract both the cutomers name and adress to two separate fields of the output object. With datus you have to do getCustomer() twice which is cluttered and quite cumbersome.

See this benchmark suite with the following manual code:

public final class ManualMapper implements OrderMapper {

    @Override
    public OrderDTO map(Order source) {
        OrderDTO target = new OrderDTO();
        Customer customer = source.getCustomer();
        if (customer != null) {
            target.setCustomerName(customer.getName());
            if (customer.getBillingAddress() != null) {
                target.setBillingCity(customer.getBillingAddress().getCity());
                target.setBillingStreetAddress(customer.getBillingAddress().getStreet());
            }
            if (customer.getShippingAddress() != null) {
                target.setShippingCity(customer.getShippingAddress().getCity());
                target.setShippingStreetAddress(customer.getShippingAddress().getStreet());
            }
        }
        if (source.getProducts() != null) {
            List<ProductDTO> targetProducts = new ArrayList<ProductDTO>(source.getProducts().size());
            for (Product product : source.getProducts()) {
                targetProducts.add(new ProductDTO(product.getName()));
            }
            target.setProducts(targetProducts);
        }
        return target;
    }

}

which currently looks like this:


    private static final Mapper<Product, ProductDTO> productMapper = Datus.forTypes(Product.class, ProductDTO.class)
            .immutable((String name) -> new ProductDTO(name))
            .from(Product::getName).to(ConstructorParameter::bind)
            .build();

    private static final Mapper<Order, OrderDTO> orderMapper = Datus.forTypes(Order.class, OrderDTO.class)
            .mutable(OrderDTO::new)
            .from(Order::getCustomer)
                .given(Objects::nonNull, Customer::getName).orElseNull()
                .into(OrderDTO::setCustomerName)
            .from(Order::getCustomer)
                .given(Objects::nonNull, Customer::getBillingAddress).orElseNull()
                .given(Objects::nonNull, Address::getCity).orElseNull()
                .into(OrderDTO::setBillingCity)
            .from(Order::getCustomer)
                .given(Objects::nonNull, Customer::getBillingAddress).orElseNull()
                .given(Objects::nonNull, Address::getStreet).orElseNull()
                .into(OrderDTO::setBillingStreetAddress)
            .from(Order::getCustomer)
                .given(Objects::nonNull, Customer::getShippingAddress).orElseNull()
                .given(Objects::nonNull, Address::getCity).orElseNull()
                .into(OrderDTO::setShippingCity)
            .from(Order::getCustomer)
                .given(Objects::nonNull, Customer::getShippingAddress).orElseNull()
                .given(Objects::nonNull, Address::getStreet).orElseNull()
                .into(OrderDTO::setShippingStreetAddress)
            .from(Order::getProducts)
                .given(Objects::nonNull, productMapper::convert).orElseNull()
                .into(OrderDTO::setProducts)
            .build();

We have to do something about this and null checking is a problem too :/

roookeee commented 5 years ago

About the null-ability checks the example shows: maybe add a .nullsafe() fluent operation that makes all map's etc null-safe in the current .from()...to() chain? That would be fine I guess - explicit implicitness :). Will split the tickets either way (see #21).

roookeee commented 5 years ago

I just looked at the benchmark suite for java mapping frameworks and realized that the ModelMapper implementation doesn't have / use this kind of feature.

Will have to reconsider / reevaluate this feature

roookeee commented 5 years ago

Will exclude this (if its happening) from 1.3.0 as I don't want to rush it

roookeee commented 5 years ago

Gave this some thought and won't implement it. Reason: huge complexity, unintuitive, less gain than previously thought, no good syntax options