how to implement factory design pattern

Understanding the Factory Design Pattern

Welcome back! Let's peel back the layers of one of the most practical creational patterns. As we discussed, the Factory Pattern isn't just about making objects; it's about managing complexity.

1 Intuition: Why we need a creator

Imagine you're writing code that needs a Button object. Right now, you might write button = new Button() directly wherever you need one.

That seems simple, but what happens when creating a Button becomes more complex? Maybe you need to choose between a WindowsButton and a MacButton, or you need to configure it with a theme before it's usable.

Every place you wrote new Button() now needs to know these details. You'd have to change many lines of code just to switch how buttons are made.

Professor Pixel's Insight: A creator—the factory—solves this. It's a dedicated function whose only job is to handle the new keyword. The rest of your code simply asks the factory for a button.

Visualizing the Refactor

Client Code (Your App)
// ❌ Hardcoded logic
if (os === 'win') btn = new WindowsBtn()
// ❌ Hardcoded logic
if (os === 'mac') btn = new MacBtn()
The Factory
(Currently unused)

2 Common pitfall: Assuming the factory does more than create

Here's where beginners often stumble: they think a factory should also contain business logic. For example, a createButton() method that not only instantiates the button but also attaches event listeners, applies validation rules, or logs analytics.

That turns the factory into a Swiss Army knife—it's no longer just about creation. This violates the pattern's intent.

Remember: The factory's responsibility is solely to return a ready-to-use object. If you find yourself writing createX() methods that do more than construct and configure the object's internal state, you're mixing concerns.

3 The core idea in simple terms

The factory method pattern is a single, well-defined entry point for object creation. It encapsulates the new operator and any immediate configuration, so the rest of your code depends on abstraction ("give me a button") rather than concretion (new WindowsButton()).

❌ Instead of this scattered everywhere:
const button = new Button({ theme: 'dark' });
✅ You centralize it:
const button = ButtonFactory.create('dark');
// Inside ButtonFactory.create(), the 'new' and configuration happen.

The power comes from this indirection: you can change what gets created (subclasses, different configurations) without changing where it's requested. That's loose coupling in action—your high-level code says what it needs, and the factory decides how to build it.

The Role of Object Creation in OOP Design Patterns

We've established that the Factory Pattern exists to help us create objects. But why is object creation such a big deal in Object-Oriented Programming (OOP)? Let's zoom out and look at the bigger picture.

1 Intuition: Objects are systems, not just things

Think of an object not as a single brick, but as a complex system. A Car isn't just new Car(). It's an assembly of an Engine, Wheels, and a Battery.

If you build cars manually, every mechanic (your code) needs to know how to source parts and assemble them. A factory changes this: it becomes the assembly plant. You don't ask for bolts; you ask for a car.

Visualizing the Assembly Process

Client Code (Your App)
// ❌ The client knows too much
const engine = new Engine("V8");
const wheels = [new Wheel(), ...];
const car = new Car(engine, wheels);
🏗️ Assembly Logic
Scattered across codebase

2 Misconception: Constructors are enough

It's tempting to think a standard constructor (new Car()) can handle everything. But constructors are for initializing state, not orchestrating creation.

Imagine if your Car constructor had to decide whether to build an ElectricEngine or a CombustionEngine based on a parameter, or if it needed to check a database for available parts first.

Professor Pixel's Rule: Constructors should be simple. If the logic to get an object ready involves decisions, caching, or validation, that logic belongs in a Factory, not the constructor.

3 Why we encapsulate creation

By moving the "how" into a factory, we gain four major architectural superpowers:

1. Single Responsibility

The factory owns the "how" of building. Your business logic only cares about the "what".

2. Loose Coupling

Callers depend on abstract requests ("Give me a car") rather than concrete new calls.

3. Testability

In tests, you can swap the factory with a mock that returns fake objects without touching your real code.

4. Centralized Config

Default themes, logging, and validation rules live in one place, not duplicated everywhere.

4 The shift in mindset

The factory doesn't just hide the new keyword—it hides the decisions. Here is the transformation in code:

❌ Rigid: Client knows internal details
function renderDashboard() {
  // Client must know specific theme/size details
  const btn = new Button({ theme: 'dark', size: 'large' });
  // ... use button
}
✅ Flexible: Client asks for intent
function renderDashboard() {
  // Factory decides theme/size based on context
  const btn = ButtonFactory.create('dashboard');
  // ... use button
}

This is why object creation becomes a first-class concern in OOP. By treating creation as a distinct process managed by a Factory, you stop fighting with complexity and start focusing on behavior.

Factory Method vs. Simple Constructor

Now that we understand the "what" and "why", let's address the most common question beginners ask: "Why not just use new everywhere?"

1 Intuition: Cooking vs. Ordering

A

Constructor (Cooking Yourself)

You are in the kitchen. To get a meal, you must know the exact recipe, find the ingredients (dependencies), and use the tools.

  • You handle the new keyword.
  • You decide the configuration.
  • If the recipe changes, you must cook again.
B

Factory Method (Ordering)

You are at a restaurant. You simply tell the waiter what you want. The kitchen (factory) handles the ingredients and tools.

  • You specify the intent (e.g., "I need a burger").
  • The factory handles the implementation.
  • If the kitchen changes suppliers, you don't care.

Visualizing the Responsibility Shift

Your Code (The Client)
// ❌ You know too much
if (os === 'win') {
  new WindowsButton(...);
} else {
  new MacButton(...);
}

Logic is scattered. Changing logic breaks your app.

The System (Hidden)
🏗️ Assembly logic is scattered
across your entire codebase.

2 Misconception: "Factories replace everything"

A common trap is thinking you must wrap every new call in a factory. This is over-engineering.

Constructors are perfectly fine for simple, stable objects. If you are creating a Point(x, y) or a simple User object that doesn't need complex setup, just use the constructor. It's clearer and simpler.

Professor Pixel's Rule: Start with a constructor. Introduce a factory method only when you feel the "pain" of duplicated logic or rigid coupling.

3 When to prefer a Factory Method

You should reach for the Factory pattern when you encounter these specific scenarios:

1
Complex Setup

If creation involves validation, multiple steps, or assembling many objects, a factory keeps your client clean.

2
Runtime Decisions

When you need to choose between subclasses (e.g., WindowsButton vs MacButton) based on user input or config.

3
Decoupling

When your code should depend on an abstraction (like ILogger) rather than a concrete class (like FileLogger).

4
Lifecycle Management

If you need to return cached instances, singletons, or pooled objects instead of always creating new ones.

4 The Code Transformation

Here is the concrete shift in code structure. Notice how the "if/else" logic disappears from the client and moves into the factory.

❌ Without Factory: Logic is exposed
function setupUI(os) {
  let button;
  // Client must know the details
  if (os === 'windows') {
    button = new WindowsButton({ theme: 'light' });
  } else {
    button = new MacButton({ theme: 'dark' });
  }
  // ... use button
}
✅ With Factory: Logic is abstracted
function setupUI(os) {
  // Client only asks for the intent
  const button = ButtonFactory.create(os);
  // ... use button
}

The Key Distinction: A constructor is a language tool for initializing a specific class. A factory method is a design decision to control how and which class gets created. Use the constructor for simple objects; use the factory when creation becomes a complex process.

Implementing a Basic Factory Method

Now that we understand the theory, let's roll up our sleeves and build one. We are going to translate the "Assembly Line" analogy into actual code.

1 Intuition: The Assembly Line Visualizer

Think of the factory method as a single, standardized station. You don't weld the car yourself; you just place an order. The station handles the complexity behind the scenes.

The Assembly Station

Client Request
Waiting for order...
(User just asks for a "Car")
Factory Logic
Waiting for input...
Returned Object
None
(Ready to use)

2 Misconception: Ignoring the need for an interface

A beginner's first factory often looks like this. It works, but it's fragile:

// ❌ The "Bad" Factory
class ButtonFactory {
  static create(type) {
    if (type === 'windows') return new WindowsButton();
    if (type === 'mac') return new MacButton();
  }
}

This ties your caller directly to the concrete factory class. If you later want to swap the entire creation strategy (e.g., for testing or a new platform), you have to change every place that references ButtonFactory.

Professor Pixel's Rule: The pattern's real power comes from depending on an abstraction for the factory itself. You should code to an interface (like IFactory), not a concrete class. This allows you to swap in a MockButtonFactory for tests without touching your main code.

3 Structure: The Three Moving Parts

A proper implementation isn't just a function; it's a structure of three distinct roles.

1. The Product Interface

This is what your code actually uses. It defines the contract (e.g., render()) without saying how it's built.

class Button {
  render() {
    throw new Error();
  }
}
2. Concrete Products

These are the real implementations. They contain the platform-specific logic.

class WindowsButton extends Button {
  render() {
    console.log('Win UI');
  }
}
3. The Creator (Factory)

The assembly station. It has one public method that returns the Product Interface.

class ButtonFactory {
  static create(type) {
    if (type === 'win') return new WindowsButton();
    return new MacButton();
  }
}

Why this structure matters: Your application code now depends only on the Button abstraction. It knows nothing about WindowsButton. If you add a LinuxButton later, you only change the Factory. The callers don't change. This is loose coupling.

Common Mistakes When Using the Factory Pattern

Congratulations on learning the pattern! But now comes the hard part: applying it correctly. Many beginners fall into the trap of thinking "Factory = Good" and wrap everything in one. This leads to over-engineering. Let's look at the typical errors so you can avoid them.

1 Intuition: The "Swiss Army Knife" Trap

A factory should be a specialized tool, not a Swiss Army Knife. A common mistake is stuffing business logic (like validation, logging, or API calls) inside the factory's create() method.

Inspect the Factory Code

✅ Correct Approach
// Focus: Creation only
function createButton(type) {
  if (type === 'win') {
    return new WindowsButton();
  }
  return new MacButton();
}

Simple. Predictable. Does exactly what it says.

2 Misconception: The Factory is not a Manager

A factory's sole job is to encapsulate object creation. It is not a manager, a validator, or a controller.

Business Logic

Don't put logic like "if button is red, log an alert" inside the factory. That belongs in the object's methods or the controller.

State Management

Don't manage the object's lifecycle after creation (e.g., starting timers, attaching listeners). The factory's job ends once the object is returned.

When to Create

The factory doesn't decide when to create objects (e.g., "every 5 seconds"). That is a higher-level concern (like a Scheduler or Controller).

Replacing Constructors

Don't use a factory for simple data holders like Point(x, y). If there's no complexity, use the constructor directly.

3 Checklist: Avoiding Pitfalls

Before finalizing your factory, run through this checklist. Click the items to reveal the "Why".

4 The Litmus Test

Ask yourself this final question:

"If the creation process changed (e.g., new subclass, extra config step), would I only touch this factory file?"

If YES: You've encapsulated correctly. The complexity is hidden.
If NO: You've leaked creation details outward. Your client code knows too much.

Advanced: Factory Pattern with Parameters

Welcome back! So far, we've looked at factories that just pick a product (like a "Windows Button"). But what if you need to customize that product?

Imagine you are at a restaurant. You don't just want "a burger"; you want a burger that is well-done, with no onions, on a gluten-free bun. The kitchen (your factory) still handles the assembly, but it uses your specifications to customize the result.

1 Intuition: Dynamic creation based on input

This is where the Factory becomes powerful. Instead of Factory.create() always returning the same thing, you pass arguments.

Visualizing the Custom Order

Client Request
Waiting for order...
Factory Logic
Waiting for input...
Returned Object
None

2 Misconception: The "Fancy Constructor" Trap

Just because you can add parameters doesn't mean you should. A common mistake is creating a factory that just passes every single parameter straight to the constructor without making any decisions.

!

The "Pass-Through" (Bad)

If the factory just forwards arguments, it's adding indirection without adding value.

function createButton(size, color) {
  // Just forwarding...
  return new Button(size, color);
}

The "Decision Maker" (Good)

The factory uses parameters to make a choice or configure dependencies.

function createButton(type) {
  // Making a decision
  if (type === 'admin') {
    return new AdminButton();
  }
  return new StandardButton();
}

Professor Pixel's Rule: Ask yourself: Is the factory using the parameter to make a decision (which class? which strategy?), or is it just forwarding data? If it's the latter, you haven't gained anything.

3 Managing Dependencies (The Composition Root)

The real superpower of a parameterized factory is Dependency Injection. Instead of the caller gathering all the pieces (Themes, Loggers, Rules), the factory does it internally.

The Composition Root

The Client (Simple)
// Client just passes the User
const button =
ButtonFactory.create(user);
The Factory (Complexity)
Waiting for user object...

Notice the shift: The caller only passes the intent (the User object). The factory translates that into the actual dependencies (Theme, Logger, Rules). This keeps your application code clean and decoupled from the underlying object graph.

Testing Factory Methods

You've built your factory. It looks clean. It works when you run the app. But have you asked the most important question: "What if I break it tomorrow?"

Testing factories is often overlooked because they seem "too simple" to fail. But remember: the factory is the gatekeeper of your application's objects. If the gatekeeper fails, everything that walks through the door is broken.

1 Intuition: The Vending Machine Analogy

Think of your factory as a vending machine. You press a button (call create('windows')), and you expect a specific snack (a WindowsButton) to drop out.

Testing the factory is like checking that the machine actually gives you the right snack every time. If the machine is broken—giving you a MacButton when you asked for Windows, or returning a button with missing properties—your whole application will fail.

Factory Testing Simulator

Test Input
Waiting...
Factory Logic
Running Unit Tests...
Test Result
None
(Assertion Check)

2 Misconception: "Factories are too simple to test"

A beginner might think: "The factory just calls new and returns the object. There's no logic to test—it's too simple to break."

This is dangerous. Factories often contain decision logic: conditionally choosing a subclass, applying configuration, or managing dependencies. That logic can have bugs.

Professor Pixel's Rule: If your factory contains if statements, it has logic. If it has logic, it needs tests.

// ❌ The Bug: A typo in the condition
static create(os) {
  // Typo here: 'windwos' instead of 'windows'
  if (os === 'windwos') return new WindowsButton();
  return new MacButton(); // Falls through!
}

Without a test, you might not realize that create('windows') is returning a MacButton instead of a WindowsButton. A simple test catches this immediately.

3 Strategies for Unit Testing Factories

Testing a factory means verifying two things: (1) the correct concrete type is returned for given inputs, and (2) the object is properly configured.

4 Why this matters

Testing your factory guarantees that the one place responsible for object creation actually does its job correctly.

Because all your application code depends on this single entry point, a well-tested factory gives you confidence that the objects flowing through your system are properly formed. It also protects you when you later modify the factory's internal logic—your tests will catch if you break existing behavior.

Real-world example: GUI toolkit button creation

Let's step out of theory and into a scenario you will likely face as a developer: building a desktop application that runs on Windows, macOS, and Linux.

1 Intuition: The "Scattered Logic" Trap

Imagine you are building a SaveDialog. You need a button. If you don't use a factory, your code looks like this:

Simulating Client Code (Without Factory)

Client Code (Your App)
Select an OS to see the logic...
Resulting Object
None
(Concrete Class Instantiated)

2 The Solution: The Button Factory

Now, we introduce the factory. The client code no longer knows how to build the button. It only knows what it needs.

The Factory (Hidden Logic)
class ButtonFactory {
  static create(os) {
    if (os === 'windows') {
      return new WindowsButton();
    }
    return new MacButton();
  }
}
The Client (Clean Logic)
function renderDialog(os) {
  // Client just asks for the intent
  const btn = ButtonFactory.create(os);
  btn.render();
}

Professor Pixel's Insight: Notice how the if/else logic has completely vanished from the client code. The client is now "platform-agnostic."

3 Misconception: "One Factory Per Product"

A common beginner mistake is thinking that because you have WindowsButton and MacButton, you need separate factories for them.

The Wrong Way

Creating WindowsButtonFactory and MacButtonFactory. This forces the client to know which factory to use, defeating the purpose.

The Right Way

One ButtonFactory that decides internally. The factory is the decision-maker, not the product itself.

4 Benefits in UI Frameworks

1. Consistent Families

Guarantees that if you use a Windows Button, you also use Windows TextFields. No mixing platforms.

2. Easy Extensibility

Adding Linux support only requires updating the Factory. Your 50+ view files remain untouched.

3. Centralized Config

If Windows buttons need a light theme, you set it once in the factory. Not in every single view.

4. Testability

In tests, inject a MockFactory that returns simple dummy objects. No UI rendering needed.

5 The Code Transformation

Here is the final transformation. Notice how the client code depends on the Factory Interface, not the concrete class.

✅ The Clean Client (Dependency Injection)
class SaveDialog {
  constructor(buttonFactory, os) {
    // The factory is passed in, not imported directly
    this.saveButton = buttonFactory.create(os);
  }
  render() {
    this.saveButton.render();
  }
}
Production vs. Test Usage
// Production: Real Factory
const dialog = new SaveDialog(ButtonFactory, 'windows');

// Test: Mock Factory (Returns a dummy object)
const mockFactory = { create: () => ({ render: () => {} }) };
const testDialog = new SaveDialog(mockFactory, 'windows');

Frequently Asked Questions

Welcome back! You've mastered the Factory Pattern, but as you dive deeper into your projects, you might run into specific scenarios. Let's tackle the most common questions I get from my students, from "Why is this failing?" to "Is this over-engineering?"

Post a Comment

Previous Post Next Post