The Factory Method Chronicles: When Subclasses Decide
The bug report glowed red on Alex’s monitor—*“Enemy spawns incorrectly on Switch but works fine on PS5.”*—a familiar symphony of platform-specific chaos echoing through the open-plan office of Nexus Interactive. Three months into *Chrono Rift*, their ambitious action RPG, the codebase had morphed into a labyrinth of `#ifdef SWITCH` and `#ifdef PLAYSTATION` directives, each conditional a tiny time bomb. Maya, the lead engineer on the combat team, slammed her coffee mug down, the sound cutting through the hum of ventilators and mechanical keyboards. “We’re patching holes in a sinking ship,” she muttered, her gaze fixed on a spaghetti bowl of `EnemyFactory.cpp` where spawning logic for the Void Stalker was duplicated with slight variations for each console. “Every new enemy type means touching this nightmare twice. Last week, the Neon Wraith had a different hitbox on Switch due to a memory alignment quirk, and our `createEnemy()` function grew another twenty lines of preprocessor garbage.”
Alex swiveled in his chair, the ergonomic mesh sighing under his weight. He’d seen this movie before—twenty years of it, in fact. The studio’s youthful energy was a double-edged sword; they built fast, but technical debt accumulated like interest on a high-risk loan. He walked over to Maya’s desk, his worn Nexus hoodie a relic from a past project. “Show me the `Void Stalker` creation,” he said, leaning over her shoulder.
```cpp // EnemyFactory.cpp (The "Before" State - A Fragile Monolith) Enemy* createVoidStalker(Platform platform) { Enemy* enemy = new Enemy(); enemy->setTexture(platform == PLAYSTATION ? TextureCache::get("stalker_ps5.png") : TextureCache::get("stalker_switch.png"));
if (platform == SWITCH) { enemy->setCollisionBox(AABB(-0.5f, -1.0f, 0.5f, 1.0f)); // Switch-specific enemy->setLODDistance(15.0f); } else { enemy->setCollisionBox(AABB(-0.6f, -1.2f, 0.6f, 1.2f)); // PS5/Xbox enemy->setLODDistance(25.0f); }
// Platform-specific AI behavior if (platform == SWITCH) { enemy->setAI(new SwitchStalkerAI()); } else { enemy->setAI(new StandardStalkerAI()); } return enemy; } ```
“It’s not just the duplication,” Maya explained, pointing at the nested conditionals. “It’s the *knowledge*. This function knows about Switch texture compression, PS5 LOD strategies, and Xbox’s different threading model. It violates the Single Responsibility Principle at a galactic scale. The `Enemy` class itself is polluted with platform checks.” She opened `Enemy.cpp`, scrolling past a dozen `#ifdef`s.
Alex nodded slowly. “You’re describing a system where *object creation is tightly coupled to the client code*—in this case, the level loader or spawner. Every time a new platform emerges, or we optimize for a new GPU, we have to dive into this core logic. It’s brittle. It’s the antithesis of open/closed.” He paused, letting the weight of the principle settle. “What you need is a way to *delegate* the ‘how’ of instantiation to the pieces of code that *actually know* about the platform specifics.”
“A factory?” Leo, a junior developer with bright, eager eyes, piped up from the next desk. “Like, a separate class that just makes enemies?”
“A step in the right direction,” Alex said, “but a *simple* factory—a single class with a giant `switch(platform)`—just moves the problem. You still have one monolithic function that must change for every new enemy *and* every new platform. That’s O(n*m) complexity in your codebase. You want O(n + m).” He drew a quick diagram on the whiteboard:
```mermaid graph TD A[Client Code<br/>Level Spawner] -->|calls| B[Factory Interface<br/>IEnemyFactory] B -->|implemented by| C[SwitchEnemyFactory] B -->|implemented by| D[PS5EnemyFactory] C -->|creates| E[VoidStalker_Switch] C -->|creates| F[NeonWraith_Switch] D -->|creates| G[VoidStalker_PS5] D -->|creates| H[NeonWraith_PS5] ```
“This,” Alex tapped the whiteboard, “is the essence of the Factory Method pattern. The *client* depends on an *abstraction*—an interface, say `IEnemyFactory`. The *subclasses*—`SwitchEnemyFactory`, `PS5EnemyFactory`—contain the platform-specific knowledge. The crucial twist: the *factory itself* doesn’t decide *what* concrete `Enemy` subclass to instantiate. It delegates that decision to its *own* subclasses via a factory method—often a `createEnemy(EnemyType type)` method. But wait, there’s a deeper layer.”
He erased part of the diagram and redrew:
```mermaid graph TD A[Client Code<br/>Level Spawner] -->|depends on| B[Abstract Creator<br/>EnemySpawner] B -->|defines| C[Factory Method<br/>createEnemy(EnemyType)] B -->|may use| C D[Concrete Creator<br/>SwitchEnemySpawner] -->|overrides| C D -->|instantiates| E[VoidStalker_Switch] E -->|uses| F[SwitchStalkerAI] G[Concrete Creator<br/>PS5EnemySpawner] -->|overrides| C G -->|instantiates| H[VoidStalker_PS5] H -->|uses| I[StandardStalkerAI] ```
“Here, the *Creator* hierarchy (`EnemySpawner` and its subclasses) owns the factory method. The client (`LevelSpawner`) works with the abstract `EnemySpawner`. The *Concrete Creator* (`SwitchEnemySpawner`) overrides `createEnemy()` to return a `VoidStalker_Switch`, which internally uses `SwitchStalkerAI`. The key is: instantiation is deferred to subclasses.”
Leo frowned. “But why not just have `SwitchEnemyFactory::create(EnemyType)`? Why the extra `EnemySpawner` layer?”
“Excellent question,” Alex smiled. “It’s about *who owns the variation*. In the first diagram, the factory is a *utility*—the client asks it for an enemy. In the second, the *spawner itself* is the variation point. The factory method is *protected* or *private* within the creator. The client doesn’t call `factory.create()`; it calls `spawner.spawnEnemy(EnemyType)`, and the spawner’s overridden method does the actual `new`. This encapsulates the creation logic *within* the object that needs the product. It’s more cohesive. For *Chrono Rift*, we might have different spawner behaviors per platform—maybe Switch uses object pooling aggressively, while PS5 uses direct allocation. The Factory Method pattern lets us vary *both* the product *and* the creation strategy together.”
The room had gone quiet. Maya was typing, her expression shifting from frustration to focused experimentation. “So… we’d have a base `EnemySpawner` with a virtual `Enemy* createEnemy(EnemyType)` method. Then `SwitchEnemySpawner` overrides it to return `new VoidStalker_Switch()`, and `PS5EnemySpawner` returns `new VoidStalker_PS5()`.”
“Exactly. And `VoidStalker_Switch` and `VoidStalker_PS5` would be *subclasses* of `Enemy`, each overriding `setCollisionBox()` and `setAI()` with their platform-optimal defaults. No more conditionals in the spawner or the enemy base class.”
“But wait,” Leo chimed in, “doesn’t that mean we need a separate subclass for *every* enemy on *every* platform? That’s a combinatorial explosion! `VoidStalker_Switch`, `VoidStalker_PS5`, `NeonWraith_Switch`, `NeonWraith_PS5`… that’s more classes!”
Alex’s eyes gleamed. This was the moment he lived for—the pivot from theory to pragmatic design. “You’ve hit on the classic *over-engineering* trap. The Factory Method’s power is in *varying families* of products. If the only difference between `VoidStalker` on Switch and PS5 is texture and collision box, maybe we don’t need separate subclasses. Maybe we need a *configurable* `VoidStalker` that takes platform parameters in its constructor. The factory method then becomes the place that *assembles* the right configuration.”
He sketched another diagram:
```mermaid graph LR A[Client] --> B[Abstract Creator<br/>EnemySpawner] B --> C[Factory Method<br/>createEnemy(type)] D[Concrete Creator<br/>SwitchSpawner] -->|override| C D --> E[configured Enemy<br/>VoidStalker(switchParams)] F[Concrete Creator<br/>PS5Spawner] -->|