Skip to Content
Course content

105: The Abstract Factory Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen in almost every mid-sized project I've joined. A developer starts with a simple factory to handle object creation, and for a while, it works great. But then the requirements grow, and suddenly that factory becomes a bloated "God Class" that everyone is afraid to touch. Let's look at a scenario where this usually falls apart.

The Switch-Statement Nightmare

Imagine you're building a game with different biomes: Forest and Desert. Each biome needs its own set of objects—like a Tree and a Creature. At first, you might write a factory like this:

public class GameElementFactory 
{
    public ITree CreateTree(string biomeType) 
    {
        return biomeType switch 
        {
            "Forest" => new ForestTree(),
            "Desert" => new DesertTree(),
            _ => throw new ArgumentException("Invalid biome")
        };
    }

    public ICreature CreateCreature(string biomeType) 
    {
        return biomeType switch 
        {
            "Forest" => new ForestCreature(),
            "Desert" => new DesertCreature(),
            _ => throw new ArgumentException("Invalid biome")
        };
    }
}

On the surface, this looks fine. But here is where the "bug" manifests—not as a compiler error, but as a maintenance disaster. First, there's no way to guarantee consistency. A developer could accidentally call CreateTree("Forest") and then CreateCreature("Desert") in the same scene, creating a weird hybrid world that breaks your game logic. Second, every time you add a new biome (like "Tundra" or "Volcano"), you have to open this class and modify every single method. You're violating the Open/Closed Principle in the worst way possible.

Grouping Families with Abstract Factories

The fix isn't just to add more logic to the existing factory; it's to change the level of abstraction. Instead of one factory that knows about every single object in the game, we create a factory that knows how to create a family of related objects.

First, we define the interface for the factory itself. This interface doesn't care about "Forest" or "Desert"; it only cares that any biome factory must be able to produce a tree and a creature:

public interface IBiomeFactory 
{
    ITree CreateTree();
    ICreature CreateCreature();
}

Now, we implement concrete factories for each biome. I like this approach because the logic for "Forest" is now entirely encapsulated in one place. If you need to change how a Forest creature is instantiated, you don't have to scroll through a 500-line switch statement in a global factory class.

public class ForestBiomeFactory : IBiomeFactory 
{
    public ITree CreateTree() => new ForestTree();
    public ICreature CreateCreature() => new ForestCreature();
}

public class DesertBiomeFactory : IBiomeFactory 
{
    public ITree CreateTree() => new DesertTree();
    public ICreature CreateCreature() => new DesertCreature();
}

Enforcing Consistency in the Client

The real magic happens when you use these factories in your game engine. Your engine no longer asks for a "Forest" tree; it just asks the current factory to give it a tree. It doesn't even need to know which biome it's currently in.

public class GameWorld 
{
    private readonly ITree _tree;
    private readonly ICreature _creature;

    // The world doesn't know if it's Forest or Desert.
    // It just knows it has a factory that provides the right parts.
    public GameWorld(IBiomeFactory factory) 
    {
        _tree = factory.CreateTree();
        _creature = factory.CreateCreature();
    }
}

By passing the IBiomeFactory into the GameWorld, you've completely eliminated the possibility of mixing objects from different biomes. If you pass in a DesertBiomeFactory, the world is guaranteed to be consistently "Desert." If you want to add a "Tundra" biome tomorrow, you just create a TundraBiomeFactory and a couple of Tundra objects. You don't have to change a single line of existing code in your GameWorld or your other factories. That's the power of the Abstract Factory pattern: it moves the decision of which family to use to the very top level of your application, leaving the rest of your code clean and agnostic.




📋 Practical Task

Implementing a Cross-Platform UI Theme Engine

You are building a dashboard that must support two different visual styles: Light Mode and Dark Mode. Each mode requires a specific set of UI components: a Button and a TextBox.

Your task is to implement the Abstract Factory pattern to ensure that the dashboard never accidentally mixes Light Mode buttons with Dark Mode textboxes.

  • Create interfaces for IButton and ITextBox.
  • Implement concrete classes for LightButton, LightTextBox, DarkButton, and DarkTextBox.
  • Define an IThemeFactory interface with methods to create buttons and textboxes.
  • Implement LightThemeFactory and DarkThemeFactory.
  • Create a Dashboard class that accepts an IThemeFactory in its constructor and uses it to initialize its components.

Validation: In your Main method, instantiate a Dashboard using a DarkThemeFactory and verify that both the button and the textbox produced are the "Dark" versions.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.