Skip to Content
Course content

147: The Factory Pattern

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

I've seen this exact pattern in dozens of junior-level pull requests: a service class that knows way too much about how its dependencies are created. It usually starts small, but it quickly turns into a maintenance nightmare. Let me show you what I mean.

public class NotificationService {
    public void sendNotification(String type, String message) {
        Notification notification;

        if (type.equals("EMAIL")) {
            notification = new EmailNotification("smtp.provider.com", 587);
        } else if (type.equals("SMS")) {
            notification = new SmsNotification("api.sms-gateway.net", "secret-key");
        } else if (type.equals("PUSH")) {
            notification = new PushNotification("fcm.google.com");
        } else {
            throw new IllegalArgumentException("Unknown notification type");
        }

        notification.send(message);
    }
}

The Fragility of Hard-Coded Instantiation

At first glance, this code "works." But look at the NotificationService. Its primary job should be sending notifications, yet it's currently burdened with the knowledge of how to construct every single type of notification. It knows the SMTP server for emails, the API key for SMS, and the endpoint for push notifications.

Here is where it breaks: imagine your boss tells you we need to add WhatsApp notifications. You now have to go into this service class and add another else if block. Then, you realize you need this same logic in a AlertManager class and a UserWelcomeService class. Suddenly, you're copying and pasting that switch statement across your entire codebase. If the SMS API key changes, you're hunting through five different files to update a string. This is a violation of the Single Responsibility Principle, and it makes your code incredibly brittle.

Abstracting Creation with a Factory

The fix is to move the "decision-making" logic about which object to create into its own dedicated class. We call this the Factory Pattern. Instead of the service asking "Which class should I instantiate?", it simply asks the factory for a Notification object and doesn't care how it's made.

First, we ensure all notifications follow a common interface:

public interface Notification {
    void send(String message);
}

Now, we build the Factory. I usually make the factory method static for simplicity unless the factory itself needs to be injected with dependencies:

public class NotificationFactory {
    public static Notification createNotification(String type) {
        return switch (type.toUpperCase()) {
            case "EMAIL" -> new EmailNotification("smtp.provider.com", 587);
            case "SMS" -> new SmsNotification("api.sms-gateway.net", "secret-key");
            case "PUSH" -> new PushNotification("fcm.google.com");
            default -> throw new IllegalArgumentException("Unknown type: " + type);
        };
    }
}

Now, look how clean the NotificationService becomes:

public class NotificationService {
    public void sendNotification(String type, String message) {
        // The service no longer knows about SMTP servers or API keys
        Notification notification = NotificationFactory.createNotification(type);
        notification.send(message);
    }
}

When to Actually Use This

I'll be honest with you: don't over-engineer this. If you have exactly one implementation of an interface and that will never change, a factory is just extra boilerplate. You're adding a new class and a new layer of indirection for no reason.

Use a Factory when the object creation logic is complex, or when you have multiple implementations of an interface and the specific one you need is determined at runtime (like from a config file or a user input). It keeps your business logic pure and your instantiation logic centralized. If you add a new notification type tomorrow, you change exactly one line in the factory, and the rest of your system just works.




📋 Practical Task

Exercise: Implementing a Document Parser Factory

You are building a data import tool that needs to handle different file formats: CSV, JSON, and XML. Currently, the code is using a messy if/else chain inside the main processing loop.

Your Task:

  • Create a DocumentParser interface with a method String parse(String content).
  • Create three concrete implementation classes: CsvParser, JsonParser, and XmlParser. Each should return a string indicating how it parsed the data (e.g., "Parsing CSV content...").
  • Create a ParserFactory class with a method getParser(String format) that returns the appropriate DocumentParser based on the input string.
  • In a Main class, demonstrate the factory by requesting a "JSON" parser and a "CSV" parser and calling the parse method on each.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.