Published on
· 10 min read

Abstract Factory Pattern in Java: Creating Families of Related Objects

Authors
  • avatar
    Name
    Nguyễn Tạ Minh Trung
    Twitter
Table of Contents

Introduction to Abstract Factory Pattern

The Abstract Factory Pattern is one of the most important creational design patterns in software engineering. It provides a way to encapsulate a group of individual factories that have a common theme without specifying their concrete classes.

What is the Abstract Factory Pattern?

The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. It's like having a factory that produces different types of products, but all products from the same factory are designed to work together.

Key Characteristics:

  • 🏭 Factory of Factories - Creates other factories
  • 🔗 Related Objects - Products are designed to work together
  • 🎭 Interface-based - Clients work with abstractions, not concrete classes
  • 🔄 Interchangeable - Easy to switch between different product families

Components of Abstract Factory Pattern

1. Abstract Factory

Declares the interface for operations that create abstract products.

2. Concrete Factory

Implements the operations to create concrete product objects.

3. Abstract Product

Declares an interface for a type of product object.

4. Concrete Product

Defines a product object to be created by the corresponding concrete factory.

5. Client

Uses only interfaces declared by Abstract Factory and Abstract Product classes.

Abstract Factory UML Diagram

abstract-factory-diagram

Real-World Use Cases

1. Cross-Platform GUI Applications

  • Creating UI components (buttons, checkboxes, menus) for different operating systems
  • Each OS has its own look and feel, but the same functionality
  • Example: Windows, macOS, Linux GUI components

2. Database Connectivity

  • Different database drivers (MySQL, PostgreSQL, Oracle)
  • Each database has specific connection, statement, and result set implementations
  • Client code remains database-agnostic

3. Game Development

  • Different themes or environments (Medieval, SciFi, Fantasy)
  • Each theme has its own set of characters, weapons, and environments
  • Consistent visual style within each theme

4. Document Processing

  • Different document formats (PDF, Word, HTML)
  • Each format has specific parsers, formatters, and exporters
  • Unified interface for document operations

5. E-commerce Platforms

  • Different payment gateways (PayPal, Stripe, Square)
  • Each gateway has specific payment, refund, and validation implementations
  • Consistent payment processing interface

Programing in Java

Imagine a UI framework that supports multiple platforms: MacOS, Windows. Each platform has its own style of widgets like Button, Checkbox.

To keep the UI consistent, we should only use components from the same "family" (e.g., Mac-style Button with Mac-style Checkbox, not a Windows-style one). The Abstract Factory ensures we get the right components as a group.

Frameworks like Swing, SWT, or Android Themes follow similar abstract factory patterns for rendering views/components.

Programing UML Diagram

os-creational-abstract-factory

Step 1: Define Abstract Products

// Abstract Product A - Button
public interface Button {

    void render();

    void onClick();
}

// Abstract Product B - Checkbox
public interface Checkbox {

    void render();

    void toggle();
}

Step 2: Create Concrete Products for Windows

// Windows Button Implementation
public class WindowsButton implements Button {

    @Override
    public void render() {
        System.out.println("Rendering windows button");
    }

    @Override
    public void onClick() {
        System.out.println("Clicked on windows button");
    }
}

// Windows Checkbox Implementation
public class WindowsCheckbox implements Checkbox {

    @Override
    public void render() {
        System.out.println("Rendering windows checkbox");
    }

    @Override
    public void toggle() {
        System.out.println("Windows button toggled");
    }
}

Step 3: Create Concrete Products for macOS

// macOS Button Implementation
public class MacButton implements Button {

    @Override
    public void render() {
        System.out.println("Rendering Mac button");
    }

    @Override
    public void onClick() {
        System.out.println("Mac button clicked");
    }
}

// macOS Checkbox Implementation
public class MacCheckbox implements Checkbox {

    @Override
    public void render() {
        System.out.println("Rendering Mac checkbox");
    }

    @Override
    public void toggle() {
        System.out.println("Mac button toggled");
    }
}

Step 4: Define Abstract Factory

// Abstract Factory Interface
public interface GUIFactory {

    Button createButton();

    Checkbox createCheckbox();
}

Step 5: Implement Concrete Factories

// Windows Factory Implementation
public class WindowsFactory extends GUIFactory {

    @Override
    public Button createButton() {
        return new WindowsButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new WindowsCheckbox();
    }
}

// macOS Factory Implementation
public class MacFactory extends GUIFactory {

    @Override
    public Button createButton() {
        return new MacButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new MacCheckbox();
    }
}

Step 6: Client Implementation

// Application class that uses the Abstract Factory
public class Application {

    private final Button button;
    private final Checkbox checkbox;

    public Application(GUIFactory factory) {
        button = factory.createButton();
        checkbox = factory.createCheckbox();
    }

    public void render() {
        button.render();
        checkbox.render();
    }

    public void onClick() {
        button.onClick();
    }

    public void onToggle() {
        checkbox.toggle();
    }
}

Step 7: Demo Application

public class OSDetector {

    public static String detectOS(String osName) {
        if (osName.contains("win")) {
            return "windows";
        } else if (osName.contains("mac")) {
            return "mac";
        } else {
            return "Unsupported OS";
        }
    }
}

public class GUIFactoryResolver {

    public static GUIFactory getFactory(String osName) {
        switch (osName) {
            case "mac":
                return new MacFactory();
            case "windows":
                return new WindowsFactory();
            default:
                throw new UnsupportedOperationException("Unsupported OS");
        }
    }
}
public class Main {

  public static void main(String[] args) {
      String osName = OSDetector.detectOS(System.getProperty("os.name").toLowerCase());
      GUIFactory factory = GUIFactoryResolver.getFactory(osName);

      Application app = new Application(factory);
      app.render();
      app.onClick();
      app.onToggle();
  }
}

Advanced Implementation Patterns

1. Factory Registry Pattern

public class FactoryRegistry {
    private static final Map<String, Supplier<GUIFactory>> factories = new HashMap<>();

    static {
        registerFactory("windows", WindowsFactory::new);
        registerFactory("macos", MacFactory::new);
    }

    public static void registerFactory(String type, Supplier<GUIFactory> factorySupplier) {
        factories.put(type.toLowerCase(), factorySupplier);
    }

    public static GUIFactory getFactory(String type) {
        Supplier<GUIFactory> factorySupplier = factories.get(type.toLowerCase());
        if (factorySupplier == null) {
            throw new IllegalArgumentException("No factory registered for type: " + type);
        }
        return factorySupplier.get();
    }

    public static Set<String> getSupportedTypes() {
        return Collections.unmodifiableSet(factories.keySet());
    }
}

Benefits and Advantages

Advantages

1. Consistency

  • Ensures that products from the same family work together
  • Maintains consistent look and feel across the application

2. Flexibility

  • Easy to switch between different product families
  • New product families can be added without changing existing code

3. Separation of Concerns

  • Client code is decoupled from concrete classes
  • Product creation logic is centralized in factories

4. Single Responsibility Principle

  • Each factory handles creation of one product family
  • Clear separation between creation and business logic

5. Open/Closed Principle

  • Open for extension (new factories and products)
  • Closed for modification (existing code doesn't change)

Disadvantages

1. Complexity

  • Increases the number of classes and interfaces
  • Can be overkill for simple applications

2. Rigid Structure

  • Adding new product types requires changes to all factories
  • Interface changes propagate to all implementations

3. Memory Overhead

  • Multiple factory instances may consume more memory
  • Caching strategies needed for better performance

When to Use Abstract Factory Pattern

Use When:

  1. Multiple Product Families

    • System needs to work with multiple families of related products
    • Products are designed to work together
  2. Platform Independence

    • Application should run on different platforms
    • Each platform has specific implementations
  3. Consistent Interface

    • Need to enforce consistent interface across product families
    • Want to ensure compatibility between related products
  4. Runtime Decision

    • Product family selection happens at runtime
    • Configuration-driven object creation

Avoid When:

  1. Simple Applications

    • Only one product family exists
    • Unlikely to add new product families
  2. Frequently Changing Products

    • Product types change frequently
    • Interface modifications are common
  3. Performance Critical

    • Object creation is performance-critical
    • Overhead of abstraction is significant

Comparison with Other Patterns

Abstract Factory vs Factory Method

AspectAbstract FactoryFactory Method
PurposeCreates families of related objectsCreates single objects
ComplexityHigher complexityLower complexity
FlexibilityMultiple product typesSingle product type
Use CasePlatform-specific componentsObject creation with subclass control

Abstract Factory vs Builder

AspectAbstract FactoryBuilder
FocusFamily creationComplex object construction
ProcessOne-step creationMulti-step construction
CustomizationFamily-levelObject-level
ImmutabilityNot enforcedOften creates immutable objects

Abstract Factory vs Prototype

AspectAbstract FactoryPrototype
Creation MethodConstructor-basedCloning-based
PerformanceMay be slowerFaster for complex objects
InitializationFrom scratchFrom existing instance
Use CaseDifferent familiesSimilar objects with variations

Common Pitfalls and Solutions

1. Over-Engineering

// ❌ Pitfall: Creating factory for simple cases
public class SimpleButtonFactory extends GUIFactory {
    @Override
    public Button createButton() {
        return new SimpleButton(); // Only one implementation
    }

    // Unnecessary complexity for single implementation
}

// ✅ Solution: Use direct instantiation for simple cases
public class SimpleUI {
    private Button button = new SimpleButton(); // Direct creation
}

2. Rigid Interface

// ❌ Pitfall: Interface that's hard to extend
public abstract class RigidFactory {
    public abstract ComponentA createA();
    public abstract ComponentB createB();
    // Adding ComponentC requires changing all implementations
}

// ✅ Solution: Flexible interface design
public abstract class FlexibleFactory {
    public abstract <T> T createComponent(Class<T> componentType);

    // Or use a registry approach
    private final Map<Class<?>, Supplier<?>> creators = new HashMap<>();

    protected <T> void registerCreator(Class<T> type, Supplier<T> creator) {
        creators.put(type, creator);
    }

    @SuppressWarnings("unchecked")
    public <T> T create(Class<T> type) {
        Supplier<T> creator = (Supplier<T>) creators.get(type);
        if (creator == null) {
            throw new IllegalArgumentException("No creator for type: " + type);
        }
        return creator.get();
    }
}

3. Memory Leaks with Caching

// ❌ Pitfall: Unbounded cache
public class LeakyFactory {
    private static final Map<String, GUIFactory> cache = new HashMap<>();

    public static GUIFactory getFactory(String type) {
        return cache.computeIfAbsent(type, k -> createFactory(k));
        // Cache grows indefinitely!
    }
}

// ✅ Solution: Bounded cache with eviction
public class BoundedCacheFactory {
    private static final Map<String, GUIFactory> cache =
        Collections.synchronizedMap(new LinkedHashMap<String, GUIFactory>(16, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, GUIFactory> eldest) {
                return size() > 10; // Limit cache size
            }
        });

    public static GUIFactory getFactory(String type) {
        return cache.computeIfAbsent(type, k -> createFactory(k));
    }
}

Conclusion

The Abstract Factory pattern is a powerful creational design pattern that provides a robust solution for creating families of related objects. It excels in scenarios where:

Key Takeaways:

  • Use Abstract Factory when you need to create families of related objects
  • Ensures consistency across product families
  • Provides flexibility to switch between implementations
  • Promotes loose coupling between client and concrete classes
  • Follows SOLID principles for maintainable code

The Abstract Factory pattern is particularly valuable in enterprise applications, cross-platform software, and any system that needs to support multiple implementations of related components. When implemented correctly, it provides a solid foundation for scalable and maintainable software architecture.


References