Simplifying Design Patterns: Abstract Factory

Introduction Design patterns are proven solutions to common software design problems. In this article, we'll simplify the Abstract Factory pattern—explaining how it creates object families, when to use it, and how to implement it with kitchen-themed examples. Inspired by Refactoring Guru and Dive Into Design Patterns (Alexander Shvets). How Does the Abstract Factory Work? The Abstract Factory is a creational design pattern that produces families of related objects without specifying their concrete classes. Key Characteristics Creates multiple product types (e.g., Pizza + Pasta + Burger) Ensures created objects are compatible (all Italian or all Mexican) Uses composition over inheritance (factories are injected) Example Scenario: Restaurant Kitchen Imagine a kitchen that needs: Multiple dish types (Pizzas, Pastas, Burgers) Multiple cuisines (Italian, American, Mexican) Instead of: // ❌ Hardcoded kitchen (brittle) class Kitchen { Pizza makeItalianPizza() {...} Pasta makeAmericanPasta() {...} // Mixed cuisines! } We use: // ✅ Abstract Factory solution interface CuisineFactory { Pizza createPizza(); Pasta createPasta(); } class ItalianFactory implements CuisineFactory { Pizza createPizza() { return new MargheritaPizza(); } Pasta createPasta() { return new AlfredoPasta(); } } When to Use Abstract Factory? Use this pattern when: Working with object families – Your system needs groups of related products (e.g., GUI components, cuisine sets) Enforcing compatibility – Objects must work together (no "Italian Pizza + Mexican Pasta") Switching configurations – You need to change entire product families at runtime How to Implement Abstract Factory Step-by-Step (Restaurant Example) Define Product Interfaces interface Pizza { void bake(); } interface Pasta { void boil(); } Create Concrete Products class MargheritaPizza implements Pizza { @Override void bake() { System.out.println("Baking thin-crust pizza"); } } Declare Abstract Factory interface CuisineFactory { Pizza createPizza(); Pasta createPasta(); } Implement Factories per Cuisine class MexicanFactory implements CuisineFactory { @Override Pizza createPizza() { return new TacoPizza(); } @Override Pasta createPasta() { return new ChipotlePasta(); } } Use in Client Code class Kitchen { private final CuisineFactory factory; Kitchen(CuisineFactory factory) { this.factory = factory; } void prepareMeal() { Pizza p = factory.createPizza(); p.bake(); } } // Usage: Kitchen italianKitchen = new Kitchen(new ItalianFactory()); Extend with Variants (Optional) Pizza createPizza(PizzaStyle style) { return switch(style) { case NEAPOLITAN -> new NeapolitanPizza(); case SICILIAN -> new SicilianPizza(); }; } Why Avoid Hardcoding Factories? // ❌ Problematic approach class Kitchen { Pizza makeItalianPizza() {...} Pasta makeMexicanPasta() {...} Burger makeAmericanBurger() {...} } Issues: Mixed cuisines create incompatible meals Adding new cuisines requires modifying Kitchen Violates Single Responsibility Principle Diagram The client (Kitchen) works with factories and products only through interfaces. Key Takeaways ✅ Ensures compatibility – All objects belong to the same family ✅ Simplifies swapping – Change entire themes by switching factories ✅ Follows Open/Closed – Add new families without modifying code Use Abstract Factory when you need coordinated object families with clean separation of concerns!

Apr 9, 2025 - 00:51
 0
Simplifying Design Patterns: Abstract Factory

Introduction

Design patterns are proven solutions to common software design problems. In this article, we'll simplify the Abstract Factory pattern—explaining how it creates object families, when to use it, and how to implement it with kitchen-themed examples. Inspired by Refactoring Guru and Dive Into Design Patterns (Alexander Shvets).

How Does the Abstract Factory Work?

The Abstract Factory is a creational design pattern that produces families of related objects without specifying their concrete classes.

Key Characteristics

  • Creates multiple product types (e.g., Pizza + Pasta + Burger)
  • Ensures created objects are compatible (all Italian or all Mexican)
  • Uses composition over inheritance (factories are injected)

Example Scenario: Restaurant Kitchen

Imagine a kitchen that needs:

  • Multiple dish types (Pizzas, Pastas, Burgers)
  • Multiple cuisines (Italian, American, Mexican)

Instead of:

// ❌ Hardcoded kitchen (brittle)  
class Kitchen {  
    Pizza makeItalianPizza() {...}  
    Pasta makeAmericanPasta() {...} // Mixed cuisines!  
}  

We use:

// ✅ Abstract Factory solution  
interface CuisineFactory {  
    Pizza createPizza();  
    Pasta createPasta();  
}  

class ItalianFactory implements CuisineFactory {  
    Pizza createPizza() { return new MargheritaPizza(); }  
    Pasta createPasta() { return new AlfredoPasta(); }  
}  

When to Use Abstract Factory?

Use this pattern when:

  1. Working with object families – Your system needs groups of related products (e.g., GUI components, cuisine sets)
  2. Enforcing compatibility – Objects must work together (no "Italian Pizza + Mexican Pasta")
  3. Switching configurations – You need to change entire product families at runtime

How to Implement Abstract Factory

Step-by-Step (Restaurant Example)

  1. Define Product Interfaces
   interface Pizza { void bake(); }  
   interface Pasta { void boil(); }  
  1. Create Concrete Products
   class MargheritaPizza implements Pizza {  
       @Override void bake() {  
           System.out.println("Baking thin-crust pizza");  
       }  
   }  
  1. Declare Abstract Factory
   interface CuisineFactory {  
       Pizza createPizza();  
       Pasta createPasta();  
   }  
  1. Implement Factories per Cuisine
   class MexicanFactory implements CuisineFactory {  
       @Override  
       Pizza createPizza() { return new TacoPizza(); }  

       @Override  
       Pasta createPasta() { return new ChipotlePasta(); }  
   }  
  1. Use in Client Code
   class Kitchen {  
       private final CuisineFactory factory;  

       Kitchen(CuisineFactory factory) {  
           this.factory = factory;  
       }  

       void prepareMeal() {  
           Pizza p = factory.createPizza();  
           p.bake();  
       }  
   }  

   // Usage:  
   Kitchen italianKitchen = new Kitchen(new ItalianFactory());  
  1. Extend with Variants (Optional)
   Pizza createPizza(PizzaStyle style) {  
       return switch(style) {  
           case NEAPOLITAN -> new NeapolitanPizza();  
           case SICILIAN -> new SicilianPizza();  
       };  
   }  

Why Avoid Hardcoding Factories?

// ❌ Problematic approach  
class Kitchen {  
    Pizza makeItalianPizza() {...}  
    Pasta makeMexicanPasta() {...}  
    Burger makeAmericanBurger() {...}  
}  

Issues:

  • Mixed cuisines create incompatible meals
  • Adding new cuisines requires modifying Kitchen
  • Violates Single Responsibility Principle

Diagram

Abstract Factory Structure
The client (Kitchen) works with factories and products only through interfaces.

Key Takeaways

Ensures compatibility – All objects belong to the same family

Simplifies swapping – Change entire themes by switching factories

Follows Open/Closed – Add new families without modifying code

Use Abstract Factory when you need coordinated object families with clean separation of concerns!