At this point, we’re fairly comfortable with using Immutables. In Part 1, we introduced how to create and use generated immutable objects. In Part 2, we looked at how we can use various features of the Immutables library to make our immutables nicer to work with.
Here, we’ll take a look at styling and usage patterns. This will allow our immutables to be more idiomatic and homogenous with the rest of our code base.
Generated Objects
Default
So far, our immutables usage has been “out of the box.” From before, we defined our immutable as:
@Value.Immutable
public abstract class Color {
@Value.Parameter
public abstract String rgb();
// ...
}
This generates ImmutableColor
. By default, we create an immutable instance like this:
Color black =
ImmutableColor.builder()
.rgb("000000")
.build();
There are a few other ways to create immutable instances. Last time, we added a factory method, allowing us to do:
Color black = ImmutableColor.of("000000");
We also looked at wither methods, which let us create immutables based off of an existing immutable. It looks a bit silly with our simple model, but that looks like this:
Color red = ImmutableColor.copyOf(black).withRgb("FF0000");
All three of these methods have something in common—we are declaring our variable as a Color
and also depending on ImmutableColor
to actually create an instance. This is a bit annoying—our code will end up depending on both Color
and ImmutableColor
.
Although this isn’t exactly wrong, it’s a bit weird. We don’t have two classes because we’re trying to abstract our code—this is just an artifact of having a generated class. It would be nicer if our code base only had to depend on one of these two classes.1
Reverse
One way to do this is with the reverse style. In this style, we will refer exclusively to the generated object. In other words, we declare our variables to be an ImmutableColor
:
ImmutableColor green = ImmutableColor.of("00FF00");
This fixes our problem of depending on both Color
and ImmutableColor
, but our code isn’t really any nicer. We can fix this by specifying a style for our generated immutables. That would look something like this:
@Value.Style(
typeAbstract = "Abstract*",
typeImmutable = "*"
)
@Value.Immutable
public abstract class AbstractColor {
@Value.Parameter
public abstract String rgb();
// ...
}
We’ll cover style in more detail below, but this annotation says that our base class starts with the prefix Abstract
and our generated immutable class should use its name verbatim.
To follow this style, we changed our base class to be named AbstractColor
, rather than Color
. The class that gets generated is now named Color
, instead of ImmutableColor
. Creating a color would now look like this:
Color green = Color.of("00FF00");
This fixes our dependency issue and feels much more natural.
There is a downside—it’s hard for IDE navigation tools to help you find how Color
is defined. Instead, you have to know that you actually want to navigate to AbstractColor
. This would be less of an issue if you adopt a convention for your base class name.
Sandwich
There’s a third style as well—the sandwich style. It’s only somewhat-documented in the Immutables github repo. It’s called a sandwich because your base class “sandwiches” the generated immutable class. This is my preferred style.
@Value.Immutable
public abstract class Color {
public abstract String rgb();
// ...
public static class Builder
extends ImmutableColor.Builder {}
public static Builder builder() {
return new Builder();
}
}
We have our base Color
class, which is extended by the generated ImmutableColor
class. We also have a Color.Builder
, which extends ImmutableColor.Builder
. That’s the sandwich—our Color
class both generates and depends on ImmutableColor
.
This pattern looks a bit funky, but it’s just exposing ImmutableColor.Builder
. Color.Builder
is really just ImmutableColor.Builder
under the hood. It works in the exact same way as before:
Color black =
Color.builder()
.rgb("000000")
.build();
There’s a second, optional piece to the sandwich style. Generated immutable classes come with wither methods, allowing us to quickly create objects that share fields. With the default style, that looks like this:
Color red = ImmutableColor.copyOf(black).withRgb("FF0000");
In our sandwich style, we can add wither methods to Color
itself. We do this by having Color
implement WithColor
:
@Value.Immutable
public abstract class Color implements WithColor {
public abstract String rgb();
// ...
public static class Builder
extends ImmutableColor.Builder {}
public static Builder builder() {
return new Builder();
}
}
WithColor
is a generated interface. Now, we can copy objects more succinctly (and without depending on ImmutableColor
):
Color red = black.withRgb("FF0000");
The sandwich style solves some of the issues with the reverse style—in particular, our IDE will know how to navigate to the correct class. The downside is that our base classes are more complex and will be confusing for anyone not used to this pattern.
Style
We can also tweak stylistic aspects of our base objects and our generated objects. For this post, we’ll be configuring our style so that our immutables are similar to using protocol buffers. For the rest of this post, we’ll also by assuming that our immutables are using sandwich style unless otherwise noted.
As we saw when looking at the reverse style, style is controlled through the @Value.Style
annotation. We can add it directly to a base class, as we did before:
@Value.Style(
typeAbstract = "Abstract*",
typeImmutable = "*"
)
@Value.Immutable
public abstract class AbstractColor {
// ...
However, this can get tedious and hard to maintain across multiple classes. Instead, we can define a shared style as an annotation.
@Value.Style(
typeAbstract = "Abstract*",
typeImmutable = "*"
)
public @interface MyStyle {}
This defines a @MyStyle
annotation that has the exact same style effect as above. We’d now annotate AbstractColor
with @MyStyle
instead of @Value.Style
.
Now, we’ll take a look at a few particularly useful style configurations. The full list of configurable styles is available here.
Getters
@Value.Style(get = {"get*", "has*", "is*"})
@Value.Immutable
public abstract class Color {
public abstract String getRgb();
// ...
The get
style is used to detect the name of our attributes. The patterns above specify that our attributes may be prefixed with get
, has
, or is
. This allows our attributes to look more like getter methods.
black.getRgb(); // "000000"
Setters
@Value.Style(init = "set*")
@Value.Immutable
public abstract class Color {
public abstract String rgb();
// ...
We can’t actually add setters to our base object because it’s an immutable. However, we can configure the setters on our builder objects. init
lets us define a prefix for the setter methods on our builder:
Color black =
Color.builder()
.setRgb("000000")
.build();
New Builder
We can also customize the name of the method used to create a new builder, which is just builder
by default. This is done with newBuilder
.
@Value.Style(
init = "set*",
newBuilder = "newBuilder"
)
@Value.Immutable
public abstract class Color {
public abstract String rgb();
// ...
Now, creating a builder looks like this:
Color black =
Color.newBuilder()
.setRgb("000000")
.build();
Optionals
We’re able to create Optional
attributes in our immutables. These are nice if you’re already used to using optionals, but they have an annoying downside—the generated immutable objects will always expect you to pass in an Optional
type when using init and copy methods.
As an example, let’s make our rgb
attribute optional.
@Value.Immutable
public abstract class Color {
public abstract Optional<String> rgb();
// ...
Now, creating a Color
looks like this:
Color black =
Color.builder()
.rgb(Optional.of("000000"))
.build();
It’s quite annoying to have to wrap a value as an Optional
each time we create an immutable object. Fortunately, we can change this with optionalAcceptNullable
:
@Value.Style(optionalAcceptNullable = true)
@Value.Immutable
public abstract class Color {
public abstract Optional<String> rgb();
// ...
Now, our builder will have two ways to set rgb
—with an Optional<String>
and with a String
. The latter is wrapped in an Optional
for us, with null
turning into Optional.empty()
.
Color black =
Color.builder()
.rgb("000000")
.build();
Much nicer.
Sample Style
We won’t be looking at other style configurations in this post, but there are many other configurable elements.
To wrap up, I’ll share the custom style annotation that I typically use. To use it, I just need to annotate my base classes with @Built
.
@Value.Style(
get = {"get*", "has*", "is*"},
init = "set*",
newBuilder = "newBuilder",
visibility = ImplementationVisibility.PACKAGE,
optionalAcceptNullable = true,
depluralize = true
)
public @interface Built {}
At this point, you’ve got everything you need to configure and adapt your immutables to your own stylistic preferences!
- I’ve also noticed that both the base type and the generated type can be confusing for junior developers. [return]