Skip to Content
Course content

368: The Factory Pattern

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

I remember a project a few years back where I was building a notification system for a growing startup. At first, it was simple: we only sent emails. A single class, a few methods, and we were gold. Then the product manager decided we needed SMS. Then Slack. Then PagerDuty for the engineering team. Before I knew it, my main service class had a massive, 200-line if/elif/else block just to decide which provider class to instantiate. Every time we added a new service, I had to touch the core business logic, and I inevitably broke the email flow while trying to get Slack working. It was a fragile mess.

That's when I realized I was fighting the code because I was forcing the "client" (the part of the app sending the notification) to know exactly how to create every single provider. The Factory Pattern is the solution to this specific headache. It allows you to decouple the creation of an object from the code that actually uses that object.

Moving Instantiation Out of the Business Logic

In Python, we don't have the rigid interfaces of Java, but the principle remains the same. Instead of your main logic worrying about which class to call, you delegate that responsibility to a "Factory." The factory is essentially a specialized object (or even just a function) whose only job is to look at the input and return the correct object.

Take a look at how we can clean up that payment or notification mess. Instead of a giant conditional block scattered throughout your app, you encapsulate that logic in one place:

class EmailProvider:
    def send(self, message):
        print(f"Sending Email: {message}")

class SMSProvider:
    def send(self, message):
        print(f"Sending SMS: {message}")

class SlackProvider:
    def send(self, message):
        print(f"Posting to Slack: {message}")

class NotificationFactory:
    @staticmethod
    def get_provider(provider_type):
        providers = {
            "email": EmailProvider,
            "sms": SMSProvider,
            "slack": SlackProvider
        }
        
        provider_class = providers.get(provider_type.lower())
        if provider_class:
            return provider_class()
        raise ValueError(f"Unknown provider type: {provider_type}")

# Now, the main logic doesn't care which class it's using
def notify_user(user_pref, message):
    provider = NotificationFactory.get_provider(user_pref)
    provider.send(message)

notify_user("slack", "Your build failed!")

The Registry Pattern for Open-Closed Design

The example above is a great start, but if you look closely, you still have to edit the NotificationFactory every time you add a new provider. If you're working on a massive plugin-based system, that's still a bottleneck. To truly follow the "Open-Closed Principle"—where code is open for extension but closed for modification—you can implement a registry.

I often prefer this approach for larger projects. By allowing providers to "register" themselves with the factory, you can add new functionality without ever touching the factory's source code. It's a subtle shift, but it makes your system incredibly modular.

class ProviderRegistry:
    _providers = {}

    @classmethod
    def register(cls, name, provider_class):
        cls._providers[name] = provider_class

    @classmethod
    def create(cls, name):
        provider_class = cls._providers.get(name)
        if not provider_class:
            raise ValueError(f"No provider registered for {name}")
        return provider_class()

# New providers can be defined in entirely different files
class DiscordProvider:
    def send(self, message):
        print(f"Sending Discord message: {message}")

# Registering the new provider without changing the Registry class
ProviderRegistry.register("discord", DiscordProvider)

# Usage
client = ProviderRegistry.create("discord")
client.send("Hello from the registry!")

Knowing When to Over-Engineer

Here is a piece of advice from the trenches: don't use the Factory Pattern for everything. I've seen developers turn every single class instantiation into a factory, and it creates a nightmare of abstraction where you can't find where an object is actually being created. If you only have two classes and you know for a fact you'll never add a third, a simple if/else is perfectly fine. Use a factory when the types of objects you need to create are dynamic, depend on external configuration, or are growing in number over time.




📋 Practical Task

Exercise: Building a Multi-Format Document Exporter

You are building a reporting tool that needs to export data in various formats (JSON, XML, and CSV). Currently, the code uses a series of if statements that are becoming hard to manage.

Your task is to implement a DocumentExporterFactory that handles the creation of these exporters. Follow these requirements:

  • Create three classes: JSONExporter, XMLExporter, and CSVExporter. Each should have a method export(self, data) that prints "Exporting [data] to [Format]".
  • Implement a DocumentExporterFactory class with a method get_exporter(format_type).
  • The factory should use a dictionary to map format strings (e.g., "json", "xml", "csv") to their respective classes.
  • Ensure the factory raises a ValueError if an unsupported format is requested.
  • Write a small script that iterates through a list of formats ["json", "csv", "pdf"], attempts to get the exporter from the factory, and calls the export method with some sample data. Handle the ValueError gracefully when "pdf" is encountered.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.