Note: Much improved versions of this post have been published to JAXenter.de and JAXenter.com:
Erzeugungsmuster mit Java 8 Lambdas
Creational patterns with Java 8 lambdas
I hope you enjoy reading them as much as I enjoyed writing them!
Sometimes I’m under the impression that one of my favorite DI frameworks, Spring, is the butt of a gazillion jokes regarding factories. For example it has a SimpleBeanFactoryAwareAspectInstanceFactory.
The name is actually well chosen: You’ll get a factory that produces instances of aspects by using another factory. It must be a factory, as you want to have polymorphism:
“In class-based programming, the factory method pattern is a creational pattern that uses factory methods to deal with the problem of creating objects without having to specify the exact class of the object that will be created.”
“Define an interface for creating an object, but let subclasses decide which class to instantiate. The Factory method lets a class defer instantiation it uses to subclasses.”
Though I find factories useful, I’m under the impression that they are most useful in frameworks, providing groundworks for other stuff, for example for decorators through aspects.
Application wise I often need builders:
The intent of the Builder design pattern is to separate the construction of a complex object from its representation. By doing so the same construction process can create different representations.
Sometimes it’s actually correct, to end up with a “MyClassBuilderGenerationFactory” (though I’d probably call it MyClassFactoryBuilder, saying that it is a builder to create factories producing instances of MyClass):
@VolkerGoebbels Solange dabei keine MyClassBuilderGenerationFactory rauskommt: Yup. @rotnroll666 @spfeiffr @gerricom @takipid
— Simon Praetorius (@s2bproject) July 5, 2016
So, what’s my favorite way of implementing builders using Java? Well, I pretty much have the idea from the great Venkat Subramaniam (if you have the chance to see one of his talks, go!). The example comes from a real world project (our temperature monitor) and builds a sensor:
import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.TimeZone; import java.util.function.Consumer; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; public class Sensor { // (1) private final DocumentBuilder documentBuilder; private final XPathExpression xPathExpression; private final DateTimeFormatter dateTimeFormatter; private final String url; // (2) Sensor(Builder builder) { try { this.documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); this.xPathExpression = XPathFactory.newInstance().newXPath().compile("/wx/temperature/current"); } catch (ParserConfigurationException | XPathExpressionException ex) { throw new RuntimeException("Could not create DocumentBuilder or XPath", ex); } this.dateTimeFormatter = DateTimeFormatter .ofPattern(builder.dateTimePattern, builder.locale) .withZone(builder.timeZone.toZoneId()); this.url = builder.url; } // (3) public static Sensor forUrl(final String url, final Consumer<Builder> cfg) { final Builder builder = new Builder(url); cfg.accept(builder); return new Sensor(builder); } public static class Builder { private String url; // (4) private String dateTimePattern = "dd.MM.yyyy (HH:mm)"; private Locale locale = Locale.GERMAN; private TimeZone timeZone = TimeZone.getTimeZone("Europe/Berlin"); // (5) private Builder(String url) { this.url = url; } public Builder withUrl(String url) { // (6) if (url == null || url.isEmpty()) { throw new IllegalArgumentException("URL may not be empty!"); } this.url = url; return this; } public Builder withDateTimePattern(String dateTimePattern) { this.dateTimePattern = dateTimePattern; return this; } public Builder withLocale(Locale locale) { this.locale = locale; return this; } public Builder withTimeZone(TimeZone timeZone) { this.timeZone = timeZone; return this; } } public static void main(String... a) { // (7) final Sensor sensor = Sensor.forUrl("http://wetter-aachen-brand.de/mwlive.xml", cfg -> cfg .withTimeZone(TimeZone.getTimeZone("Europe/Berlin")) .withLocale(Locale.GERMAN) ); } } |
What do we have here?
- An immutable sensor object, all values are final, including the ones that take more than one information to configure (i.e. the dateTimeFormatter).
- A sensor cannot be build without the builder, so you won’t end up with invalid objects. Also, you won’t end up with a telescoping constructor (adding more and more arguments or overloading constructors). As you can see I don’t mind some intelligence in a constructor, but you could move the instantiation of the stuff that throws exceptions into the builder
- Several things going on here:
A “speaking” method: I’ll always want to have a sensor for a given URL. Also note that I want to make clear which values are obligatory. A sensor without an URL is useless. If you add Project Lombok you can get rid of the manuell checks in (6)
The creational method takes aConsumer
as an argument. Together with the private Builder constructor in (5) this leads to the fact that the builder cannot be instantiated without the context of a Sensor. This clarifies the fact, the this builder here should be used once and only once. The creational method does the build, not the external caller. - Decouple the builder properties from the actual needed properties. The builder knows a format pattern, the resulting object the formatter.
- Don’t allow the builder to be used standalone, only in the context of (3).
- Check for required arguments in the builder methods.
- Finally: A fluent API to configure your objects in a very concise way.
But can we do better than that? What if the creation of stuff depends on an order? It’s actually pretty easy to implement and you’ll get a very clean interface, without the need for purely functional languages by just using Java 8 idioms:
import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.TimeZone; import java.util.function.Function; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; public class Sensor { private final DocumentBuilder documentBuilder; private final XPathExpression xPathExpression; private final DateTimeFormatter dateTimeFormatter; private final String url; // (1) Sensor(BuilderStep3 builder) { this.documentBuilder = builder.documentBuilder; this.xPathExpression = builder.xPathExpression; this.dateTimeFormatter = builder.dateTimeFormatter; this.url = builder.url; } public static class Builder { private String url; private Builder(String url) { this.url = url; } // (2) public BuilderStep2 withDateTimePattern(final String dateTimePattern) { return new BuilderStep2(url, dateTimePattern); } } public static class BuilderStep2 { private final String url; private String dateTimePattern = "dd.MM.yyyy (HH:mm)"; private Locale locale = Locale.GERMAN; private TimeZone timeZone = TimeZone.getTimeZone("Europe/Berlin"); public BuilderStep2(final String url, final String dateTimePattern) { this.url = url; this.dateTimePattern = dateTimePattern; } public BuilderStep2 withDateTimePattern(String dateTimePattern) { this.dateTimePattern = dateTimePattern; return this; } public BuilderStep2 withLocale(Locale locale) { this.locale = locale; return this; } public BuilderStep2 withTimeZone(TimeZone timeZone) { this.timeZone = timeZone; return this; } // (3) public BuilderStep3 withPath(final String path) { return new BuilderStep3(url, DateTimeFormatter .ofPattern(this.dateTimePattern, this.locale) .withZone(this.timeZone.toZoneId()), path); } } // (4) public static class BuilderStep3 { private final String url; private final DateTimeFormatter dateTimeFormatter; private final DocumentBuilder documentBuilder; private final XPathExpression xPathExpression; public BuilderStep3(String url, DateTimeFormatter dateTimeFormatter, String path) { this.url = url; this.dateTimeFormatter = dateTimeFormatter; try { this.documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); this.xPathExpression = XPathFactory.newInstance().newXPath().compile(path); } catch (ParserConfigurationException | XPathExpressionException ex) { throw new RuntimeException("Could not create DocumentBuilder or XPath", ex); } } } // (5) public static Sensor forUrl(final String url, final Function<Builder, BuilderStep3> cfg) { return cfg.andThen(Sensor::new).apply(new Builder(url)); } public static void main(String... a) { // (6) final Sensor sensor = Sensor.forUrl("http://wetter-aachen-brand.de/mwlive.xml", cfg -> cfg .withDateTimePattern("dd.MM.yyyy") .withLocale(Locale.GERMAN) .withPath("/wx/temperature/current") ); } } |
- The sensor now takes a
BuilderStep3
argument. Also take note that I have moved the construction of complex objects into the builders as well - The first builder returns a
BuilderStep2
as the context changes from an url to datetime stuff - Context changes again from creating the actual DateTimeFormatter to XML and XPath stuff
BuilderStep3
is the last step and returns no other builder (and also has nobuild
method!)- Here is the “fun”: The creational function now takes a
Function
fromBuilder
to aBuilderStep3
as argument and no simple consumer. This way I am in control over creating the first builder and then I can guide the user of my builders through the right order of configuring stuff. The final usage in (6) doesn’t look much different but it’s now the only way to use that set of builders
Also take note how easily you can chain function calls in Java 8 - Final usage: Configuration can only be done in the correct order
I think my last suggestion can be very nice integrated into the Step builder pattern as described by Marco.
2 comments
Just as an addition, I also like the step builder pattern using inner interfaces, as described here:
https://www.javacodegeeks.com/2013/05/building-smart-builders.html
http://rdafbn.blogspot.de/2012.....rn_28.html
http://blog.crisp.se/2013/10/0.....n-for-java
Stefan, Danke für die Links! Ich hab insbesondere den von Marco mal verlinkt.
Post a Comment