11 October 2024

JDK23: Builder pattern simplicity with JEP-455s

The runtime initiation of any program requires allocating a given memory and after many additional steps the first object could be created. In the previous post we have explored how to improve maintainability of complex creation processes by using the factory creational pattern[4] and JEP-482, flexible constructor bodies. The factory design may be a good fit for many cases but it may also create obstacles due to the pattern code centralized nature. In this post we review how to shift an object initiation process across various parties with creational pattern builder and JEP-455.

Building a vehicle

It may not be obvious from a simple example, but especially when a project moves away from an coding imperative style, it pushes the requirements on the object initiation process. Separation of concerns principle along with the use of different design patterns[3] not only improves the maintainability and readability of the code, but also serves as an opportunity to control the object creation process.

The vehicle assembly process is a nice example of relatively complex steps that can be thought of as a collection of small pieces tied together with respect to their hierarchy (Example 1.).

enum VehicleType {
   ELECTRIC(1, (byte) 1), 
   PETROL(2, (byte) 2), 
   DIESEL(3, (byte) 3), 
   UNKNOWN(-1, (byte) -1);


   private final int sensorId;
   private final byte value;


   VehicleType(int sensorId, byte value) {
       this.sensorId = sensorId;
       this.value = value;
   }
...
}


record Sensor(String name, byte value) {}

class TestVehicle implements Vehicle {
   final static class Builder {
	...
        Builder addSensor(Sensor s) {
            this.sensors.add(s);
            return this;
        }
        ...
        Builder addType(VehicleType type) {
            var requiredSensors = switch (type.sensorId()) {
                case 1 -> createSensorByType(ELECTRIC);
                case 2 -> createSensorByType(PETROL);
                case 3 -> createSensorByType(DIESEL);
                default -> createSensorByType(UNKNOWN);
            };
            sensors.add(requiredSensors);
            this.type = type;
            return this;
        }

        TestVehicle build(String name) {
            return new TestVehicle(name, type, sensors);
        }
    }...

Example 1.: JEP-455 simplifies a builder pattern removing by verbose casting steps inside addType method

The introduced TestVehicle builder with final method build is not only testable, easy to understand and extensible, but can also be passed as an argument to various services or methods contributing to the vehicle composition process. JEP-455 aims to provide a way to securely pass and cast primitive values ​​without data loss. This can occur when working with low-level sensors or abstractions that don’t require storing reference types as Integer, Double or etc. inside the heap. In such cases primitive types are a good choice. JEP-455 enables creation of validation or transformation methods without typecasting necessity. Nice example is the method that could be used inside the Builder design pattern for direct validation without compromising object creation (Example 2.).

private static byte evaluateSensorByteValue(Object value) {
   return value instanceof byte b ? b : (byte) -1;
}

Example 2.: JEP-455 removes necessity of casting with information loss

Reducing the verbosity of the code will not be complete without introducing local variable type inference by JEP-286 [5], which greatly contributes to readability (Example 1, addType method). The process of creating an instance of a specific class becomes clearer and smoother with the ability to perform some advanced operations while maintaining full control over the entire process[3].

var testVehicleBuilder = new TestVehicle.Builder()
       .addType(VehicleType.DIESEL);
// Complex construction process
var electricPlatformSensor = getPlatformSensor();
// Retrieve required object
var producedVehicle = testVehicleBuilder
       .addSensor(electricPlatformSensor)
       .build("testVehicle");

Example 3.: Builder allows you to add required parts downstream

Conclusion

While a factory hides the instantiation logic behind a method, a single point of truth, which can turn out to be complex and not entirely maintainable, the Builder pattern serves to make each added component clear and testable. The factory pattern may be a good fit for the early stages of the development, but with the new enhancements coming with JDK23[2], the builder pattern may be re-evaluated as it may support the functional nature of current business requirements.

Resources

[1] JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview)
[2] Java 23 Has Arrived, And It Brings a Truckload of Changes
[3] Practical Design Patterns for Java Developers
[4] JDK23: Factory pattern with flexible constructor bodies, JEP-482
[5] JEP 286: Local-Variable Type Inference


Miro Wengner

Miro is a member of the JCP program for very long time. He contributes to the OpenJDK, Mission Control project. His focus is on java performance and maintainability. Miro's involvement can be seen in various another open-source projects such as OpenTracing, Pi4J and etc. He is also co-author of Robo4j project which has been awarded by DukeChoice Award 2017. Miro has been recognized as JavaChampion, Oracle ACEPro, RockStar speaker. Aside of his daily duties as a Principal Engineer at OpenValue he shares his knowledge over conferences (JavaOne, CodeOne, Devoxx, GeeCON etc.) and blogging.