Design Patterns

81. Factory Method Pattern

Factory methods decouple object creation from clients — they can return subtype instances, cache instances, or vary the concrete type — overcoming the limitations of constructors (List.of(), Optional.of() are classic examples).

Java 21 (core language)

Explanation

A factory method is a static (or instance) method that creates and returns an object, instead of calling new directly. Advantages over constructors: they have a meaningful name (valueOf, of, from, getInstance, create); they can return a subtype (hiding the concrete class); they can return cached instances (like Integer.valueOf(127)); they can fail more expressibly.

Static factory method pattern (Effective Java Item 1): use this when a class might want to cache instances, when the return type might change, or when a more descriptive name helps readability. Downsides: the class cannot be subclassed if all constructors are private, and they're harder to discover than constructors (not in Javadoc's constructor summary).

The Factory Method design pattern (GoF): defines an interface for creating an object but lets subclasses decide which class to instantiate. More heavyweight — use static factory methods first; reach for GoF Factory Method only when you need polymorphic creation.

Example

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class FactoryMethodPattern {
    // Static factory methods: named, can return subtypes, can cache
    interface Shape {
        double area();
        String name();

        // Static factory on the interface (Java 8+)
        static Shape circle(double radius) { return new Circle(radius); }
        static Shape rectangle(double w, double h) { return new Rect(w, h); }
    }

    private record Circle(double radius) implements Shape {
        @Override public double area() { return Math.PI * radius * radius; }
        @Override public String name() { return "Circle(r=" + radius + ")"; }
    }
    private record Rect(double w, double h) implements Shape {
        @Override public double area() { return w * h; }
        @Override public String name() { return "Rect(" + w + "x" + h + ")"; }
    }

    // Caching factory (like Integer.valueOf for -128..127)
    static class ConnectionPool {
        private static final Map<String, ConnectionPool> cache = new HashMap<>();
        private final String url;
        private ConnectionPool(String url) { this.url = url; }

        public static ConnectionPool forUrl(String url) {
            return cache.computeIfAbsent(url, ConnectionPool::new);
        }
        @Override public String toString() { return "Pool[" + url + "]"; }
    }

    public static void main(String[] args) {
        // Shape factory — clients don't know the concrete class
        Shape c = Shape.circle(5);
        Shape r = Shape.rectangle(3, 4);
        System.out.printf("%s area = %.2f%n", c.name(), c.area());
        System.out.printf("%s area = %.2f%n", r.name(), r.area());

        // Standard library factory methods
        List<String>    list    = List.of("a", "b", "c");
        Optional<String> opt    = Optional.of("value");
        Optional<String> empty  = Optional.empty();
        Map<String, Integer> map = Map.of("x", 1, "y", 2);

        System.out.println("List: " + list);
        System.out.println("Optional: " + opt);
        System.out.println("Empty: " + empty);

        // Caching factory: same URL -> same instance
        var pool1 = ConnectionPool.forUrl("jdbc:postgresql://localhost/db");
        var pool2 = ConnectionPool.forUrl("jdbc:postgresql://localhost/db");
        System.out.println(pool1 == pool2); // true — cached
    }
}

Sources