immutables / immutables

Annotation processor to create immutable objects and builders. Feels like Guava's immutable collections but for regular value objects. JSON, Jackson, Gson, JAX-RS integrations included
http://immutables.org
Apache License 2.0
3.42k stars 271 forks source link

java 15 record as generated immutable implementation #1259

Closed mickroll closed 3 years ago

mickroll commented 3 years ago

How about generating the immutable implementation of a given interface as a java 15 record (JEP 359)?

This would allow all the benefits of records to be combined with the greatness and ecosystem integration of immutables (in my case: mainly compatibility with mapstruct and openapi).

WDYT?

elucash commented 3 years ago

this is a good idea, I think. In general, I would actually dream that Java would be a language in which tools like Immutables would not be needed. Even with new records we still probably need builders and some other stuff generated even if we delegate basic fields/hashCode/equals to the record implementation. The other thing is that considering record syntax is more compact than equivalent interface definition, some would even prefer having just record + generated builder akin to following:

record Abc(int a, double b, String c) {
  @RecordBuilder// or something on Abc - doesn't matter
  class Builder extends ImmutableAbc.Builder {}
}

var abc = new Abc.Builder().a(1).b(0.2).c("C").build();

In order to get to this we need some time + experimenting. I would call for help here, but because of experimental nature of it, the path to merged PR might not be straightforward, yet we're open to consider proposed designs/implementations

mickroll commented 3 years ago

Since java records I think of Immutables in its current state as a bridging technology. Many libs depend on bean-style getters, some expect them to be annotated. Java records do not provide this (afaik). On the other hand, newer libs might expect/allow data objects to be java records. This is where immutables could step in:

Backing immutable implementations with java records would enable a seamless interoperation between 'old-style' bean-expecting libs and newer ones that already allow 'record-style' getters (do they have a special name?)

elucash commented 3 years ago

Can you provide some code snippets as an illustration? interfaces, signatures, and/or usage pseudocode

mickroll commented 3 years ago

I thought of something along the lines of:

Data object definition:

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import javax.validation.constraints.NotEmpty;
import org.immutables.value.Value;

@Value.Immutable(record = true)
@Value.Style(get = { "get*", "is*" }, init = "set*", allParameters = true)
@JsonDeserialize(builder = ImmutableLoginAttemptUser.Builder.class)
public interface LoginAttemptUser {

  @NotEmpty
  String getUsername();

  @NotEmpty
  String getPassword();
}

Record-style implementation, generated by immutables:

@javax.annotation.processing.Generated("org.immutables.processor.ProxyProcessor")
public record ImmutableLoginAttemptUser(String username, String password)
    implements LoginAttemptUser {

  @JsonProperty("username")
  @Override
  public String getUsername() {
    return username;
  }

  @JsonProperty("password")
  @Override
  public String getPassword() {
    return password;
  }

  public static ImmutableLoginAttemptUser.Builder builder() {
    return new ImmutableLoginAttemptUser.Builder();
  }

  @Generated(from = "LoginAttemptUser", generator = "Immutables")
  public static final class Builder {
    [... same as before]
  }
}

Usage, bean-style (these getters need to be generated by immutables to fulfill interface declarations):

  var user = ImmutableLoginAttemptUser.builder().setUsername("foo").setPassword("bar").build();
  String x = user.getUsername() + ":" + user.getPassword();

The jackson json deserializer would use the builder in a similar way. The jackson json serializer would use the bean-style getters.

Usage, record-style (these 'getters' are generated by compiler):

  var user = ImmutableLoginAttemptUser.builder().setUsername("foo").setPassword("bar").build();
  String x = user.username() + ":" + user.password();

I think the overall behaviour of the generated records are the same in comparison to created immutable classes.

Note the restrictions of records (https://openjdk.java.net/jeps/395#Rules-for-record-classes), that is why i would recommend to enable usage of records on an opt-in basis via @Value.Immutable(record = true).

elucash commented 3 years ago

Interesting. So the most compelling use case for generating records would allow to use records while still implementing older get* accessors from interfaces? I thinks it's by design that it will contain both .getVal() and .val() accessors for compatibility. I think Jackson piece would be negligible in future as Jackson can (if not already) easily provide a module to serialize/deserialize records.

mickroll commented 3 years ago

Well, maybe what I really need is a builder for records. And thats already possible using @Builder.Constructor (https://immutables.github.io/factory.html)

Thanks for the discussion!