Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Modifying Immutable Objects With Chained Methods

DZone's Guide to

Modifying Immutable Objects With Chained Methods

See how you can change your immutable objects using chained methods and how that approach stacks up against using a builder.

· Java Zone ·
Free Resource

Verify, standardize, and correct the Big 4 + more– name, email, phone and global addresses – try our Data Quality APIs now at Melissa Developer Portal!

When you are coding with immutable objects, there are many times where you not only need to initially define them, but may want to create derivative versions of an existing instance. You might want to consider using chained methods to make your code more concise or to take advantage of default or optional parameters.

You have probably implemented an immutable class with final fields with values passed into the constructor like this immutable ConnectionProperties class below:

    public class ConnectionProperties {
        private final int retries;
        private final int port;
        private final String address;
        private final int timeout;
        private final String username;
        private final int maxAge;

        public ConnectionProperties(int retries, int port, String address, int timeout, String username, int maxAge) {
            super();
            this.retries = retries;
            this.port = port;
            this.address = address;
            this.timeout = timeout;
            this.username = username;
            this.maxAge = maxAge;
        }

        //getters go here
    }
}


So you have an immutable class, but it can be verbose to construct with so many parameters. The only way to specify default values is to overload the constructor and use defaults, but with this many fields, it can be hard to write and use so many overloads. We could create a builder, which would be a respectable solution, but we are going to explore an implementation with chained methods — and I’ll discuss why that may be better than a builder, and cases where it is not.

A chained method on an instance of a class is used to return the instance for the purposes of calling another method on the instance. The method usually alters the state of the instance in some way. With immutable classes, we can’t modify the state but must create a new instance with the mutated state. In our chained method, instead of returning the same instance, we build a new mutated instance and return that from the chained method.

public ConnectionProperties retries(int newRetries) {
    return new ConnectionProperties(newRetries, port, address, timeout, username, maxAge);
}
 
public ConnectionProperties port(int newPort) {
    return new ConnectionProperties(retries, newPort, address, timeout, username, maxAge);
}


Here we create a new instance of the class each time we call a chained method and return it with one field value changed. The other values passed into the constructor are the field values in the current instance. Here is an example of how we might use this class:

properties = new ConnectionProperties(...).port(8080).retries(5);


I left the constructor there a bit vague for reasons you’ll see in a minute. I renamed the parameter by prefixing it with ‘new’. Another alternative would be to not rename the parameters and leave them matching the field name:

public ConnectionProperties retries(int retries) {
    return new ConnectionProperties(retries, port, address, timeout, username, maxAge);
}
 
public ConnectionProperties port(int port) {
    return new ConnectionProperties(retries, port, address, timeout, username, maxAge);
}


If you look at the implementation of those two methods, the two lines of code to construct the new properties class are identical, and yet they produce two completely different objects. This works because of the scope of the variables passed into the constructor. In the first method, we call the constructor, but the port, address, timeout, username, and maxAge parameters all refer to the fields in the current instance. The retries parameter is scoped to the parameter passed into the method call. The object will be constructed with the passed-in retries parameter, and all the other values will be the same as the current instance field values.

As a bonus, if you use code completion in your IDE, chances are that it will (Eclipse does) autocomplete that constructor with those same parameter names, or you can just copy and paste the constructor line of code so it pretty much writes itself. However, some people might not like this alternative because it isn’t very clear.

Constructors and Default Values

Let’s get back to the constructors for a second since we still need to create that first instance that we are calling our chained methods on. The constructor gives us a chance to set default values on any or all of the fields.

public ConnectionProperties() {
    this(3,8080,"127.0.0.1",5000,null,10000);
}


With this constructor, we can create an instance with sensible default values and call the chained methods to create mutated instances. We may even want to make the parameterized constructor protected to hide it from developers.

properties = new ConnectionProperties().address("192.168.1.11").username("Roger");


This creates a new connection with the default values that only requires minimal property changes. If we add another property to the class, we don’t have to worry about breaking existing code since it will already have a sensible default from the no-arg constructor.

Compared to Builder

One benefit chained methods have over a builder is that this doesn’t require implementing and maintaining a new class and duplicating all the fields and some methods. It also allows you to modify an existing instance, or at least create a new one based on an existing one rather than starting from a blank builder each time:

public CallResult makeCallWithLongTimeout(ConnectionProperties connectionProperties) {
    ConnectionProperties modfiedProperties = connectionProperties.timeOut(LONG_TIMEOUT);
    return makeCall(modfiedProperties);
} 


To do this with a builder, we would need to either pass the builder instead of the properties to the method or have a method in the builder that can default it to the existing instance.

public CallResult makeCallWithLongTimeout(ConnectionProperties connectionProperties) {
    ConnectionPropertiesBuilder builder = new ConnectionPropertiesBuilder().copyFrom(connectionProperties);
    builder.timeOut(LONG_TIMEOUT);
    ConnectionProperties modifiedProperties = builder.build();
    return makeCall(modifiedProperties);
} 


However, builders allow you to set values one by one in no specific order and then validate them when you call the build() method. We don’t need to worry about whether a field is set or not until the build() method is called and the values are validated.

Depending on how you use the values objects, there may be issues with creating an instance of the class without values set. For example, can the username be null, or do we do a null check when we set the value? If we are required to set the value of every field and there is no sensible default then we have lost the ability to have optional constructor arguments.

In general, you want to reduce the amount of initial and maintenance code per field (including testing!) so if you can get away with just using an immutable class with chained methods without a builder, you should. The best use cases for these is where the all values are required and have sensible defaults. The no-arg constructor can build a valid object and we can just tweak the properties on the default as needed.

Developers! Quickly and easily gain access to the tools and information you need! Explore, test and combine our data quality APIs at Melissa Developer Portal – home to tools that save time and boost revenue. 

Topics:
java ,immutable objects ,chained methods ,builder ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}