The idea: control how objects are made
Most objects are made with new and a constructor. Two classic creational
patterns change that. A Builder lets you put a complicated object together one
part at a time and only creates it when every part is ready. A Singleton makes sure a
class has only one object, and hands that same object to everyone who asks.
Builder: one part at a time
Java's HttpRequest has a uri, a method, headers, a timeout and a body. Most are optional.
Instead of one constructor with all of them, you call HttpRequest.newBuilder() and then one
method per part: uri(…), header(…), timeout(…),
POST(…). Each call stores one field on the builder object. No request
exists yet. (Demo: build a request step by step)
Every setter ends with return this, so the calls can be written as one chain:
HttpRequest.newBuilder().uri(u).timeout(t).build(). That is called a
fluent interface. The page writes each call on its own line so you can follow it.
build() checks once, then creates
build() is the one place where the parts are checked together. A request without a uri makes
no sense, so Java's build() throws IllegalStateException and creates nothing.
You never get a half-made object that fails later. The builder keeps its fields: set the uri and call
build() again. (Demo: build() refuses a missing uri)
When the check passes, build() calls the product's constructor once, with the whole builder.
The new HttpRequest copies the values into final fields and has no setters, so
it is immutable. Immutable objects can be shared between threads and cached without
defensive copies. The builder can be reused to make HttpRequest@2.
Why not just a constructor?
Without a builder, every combination of optional parts needs its own constructor, or one constructor with
many positional arguments and null for the ones you skip. That is the
telescoping constructor. The call site no longer says what each value means, and two
arguments of the same type can be swapped without any error.
| Way to create | Optional parts | Checks | Result |
|---|---|---|---|
| Telescoping constructor | one overload per combination, or null | in the constructor | can be immutable, hard to read |
| Empty constructor + setters | call the setters you need | none: the object exists half set | mutable |
| Builder | call the builder methods you need | once, in build() | immutable and complete |
Singleton: one instance for everyone
AppConfig has a private constructor, so no other class can write
new AppConfig(). It keeps its one object in a static instance field.
getInstance() creates the object on the first call (lazy initialization) and
returns the same one on every later call. On the page, new AppConfig() runs once however many
services ask. (Demo: one shared instance)
The pitfall: hidden global state
A singleton is a global variable with a class around it. In Demo: hidden global state,
EmailService gets the config and expects the default timeout of 30 s. Then
OrderService calls setTimeout(5 s) for its own needs. Next time
EmailService reads the timeout it gets 5 s. It never changed anything, and nothing in its
constructor says it depends on AppConfig: the call AppConfig.getInstance() is
buried inside its methods.
The same thing breaks tests. One test changes the singleton, the next test inherits the change, and the result depends on the order the tests run in. You also cannot hand a test a fake config, because the code fetches the real one itself.
Thread safety: the lazy-init race
The lazy getInstance() is a check-then-act: test instance == null, then create.
If two threads run it at the same moment, both can see null before either assigns the field.
Both run new AppConfig(), the second overwrites instance, and the two services
now hold different objects. (Demo: race, lazy init without a lock)
Making getInstance() synchronized fixes it: the second thread waits for the
lock, then sees the object is there. (Demo: race fixed with synchronized) The cost is a lock on
every call. Common alternatives: create the object eagerly in a static final field, the
holder class idiom (the JVM initializes a nested class once, thread-safely, on first
use), a one-element enum, or double-checked locking with a volatile field.
Using one instance without a Singleton
Often you want one object without the global access. Create it once at startup and pass it into the constructors of the classes that need it: dependency injection. Spring beans are single instances by default in exactly this way. The dependency is now visible in the constructor, and a test can pass in its own config. A config object that is immutable, like the builder's product, removes the hidden-state problem too.
See also Strategy and Factory Method Patterns, another way to keep
new in one place, SOLID Principles for dependency inversion, and
Decorator, Proxy and Adapter, whose virtual proxy also creates an
object lazily.
What the page leaves out
The Gang of Four Builder also has a Director that runs a fixed sequence of builder calls, and
builders for different representations of the same steps. Builders generated by tools (Lombok's
@Builder), step builders that force a call order with types, and Kotlin or Python named
arguments, which remove much of the need, are left out. For Singleton, the page leaves out
serialization and reflection creating a second object, one instance per class loader, and memory
visibility without volatile.