Factory Pattern in Python

Factory Pattern in Python

Get stuff loosely coupled

ยท

5 min read

Factory Pattern aka Factory Method, Simple Factory is one of the most common Design Patterns out there. the Factory Pattern separates the creation of objects from its use case, in this pattern you have a factory object which holds the logic behind the object instantiation and returns it at the runtime.

Factory Pattern also has another variation, usually known as Abstract Factory Pattern aka Factory Pattern and the difference is that Abstract Factory adds a layer of abstraction where factories are used by another main factory.

Factory Pattern ๐Ÿญ

Let's create a localization feature by having an abstract class called Localizer that will be extended later by the different languages we want to support.

Python docs

This module provides the infrastructure for defining abstract base classes (ABCs) in Python

from abc import ABC, abstractmethod


class Localizer(ABC):
    @abstractmethod
    def localize(self, msg):
        """ Translate message """
        pass

We will add 3 different languages: English, German and Arabic.

Each of them extends the Localizer class and config a dict of translations in the constructor as well as overrides the localize function

starting with the German

class GermanLocalizer(Localizer):
    "German version"

    def __init__(self):
        """ Setup translations """
        self.translations = {"main.header": "Ich spreache deutsch"}

    def localize(self, msg):
        """Returns msg in German"""
        return self.translations.get(msg, msg)

then we add the others, and the code becomes

from abc import ABC, abstractmethod


class Localizer(ABC):
    @abstractmethod
    def localize(self, msg):
        """ Translate message """
        pass


class GermanLocalizer(Localizer):
    "German version"

    def __init__(self):
        """ Setup translations """
        self.translations = {"main.header": "Ich spreache deutsch"}

    def localize(self, msg):
        """Returns msg in German"""
        return self.translations.get(msg, msg)


class ArabicLocalizer(Localizer):
    """Arabic version"""

    def __init__(self):
        """ Setup translations """
        self.translations = {"main.header": "ุงุชูƒู„ู… ุงู„ุนุฑุจูŠุฉ"}

    def localize(self, msg):
        """Returns msg in Arabic"""
        return self.translations.get(msg, msg)


class EnglishLocalizer(Localizer):
    """English version"""

    def __init__(self):
        """ Setup translations """
        self.translations = {"main.header": "I speak English"}

    def localize(self, msg):
        """Returns msg in English"""
        return self.translations.get(msg, msg)

The Problem ๐Ÿšฉ

Now if we DON'T want to use the Factory pattern our main function will be something like

def main():

    print("Please select your language:")
    print("1. English")
    print("2. German")
    print("3. Arabic")

    option = input("Please select an option then hit enter:")
    local = EnglishLocalizer()
    if option == '1':
        local = EnglishLocalizer()

    if option == '2':
        local = GermanLocalizer()

    if option == '3':
        local = ArabicLocalizer()

    msg = "main.header"

    print(local.localize(msg))

Each time we want to add some message on the shell, we will have to add those ugly if statements! Imagine how bad would it be if we decided to add a new language?!

The Solution ๐Ÿ–

We add the Factory method!

class Translate:

    @staticmethod
    def get_localizer(option="1") -> Localizer:
        """Factory Method"""
        localizers = {
            "1": EnglishLocalizer,
            "2": GermanLocalizer,
            "3": ArabicLocalizer,
        }

        return localizers[option]()

So main becomes

def main():

    print("Please select your language:")
    print("1. English")
    print("2. German")
    print("3. Arabic")

    option = input("Please select an option then hit enter:")

    local = Translate.get_localizer(option)

    msg = "main.header"
    print(local.localize(msg))

Output

image.png

By having that Factory method, all the logic that handles localization is now in one function, whenever we want to change anything regarding the logic, there is only one function to modify get_localizer.

Abstract Factory Pattern ๐Ÿญ

You are developing a cross-platform application, where buttons, sliders, and dialogs will be unique on each OS.

each UI component has two variations, either Windows or MacOS

class IButton(ABC):
    @abstractmethod
    def onClick(func: function):
        pass


class ISlider(ABC):
    @abstractmethod
    def onSlide(func: function):
        pass


class WinButton(IButton):
    """Button of Windows design system"""
    def onClick(func):
        func()


class MacButton(IButton):
    """Button of macOS design system"""
    def onClick(func):
        func()


class WinSlider(ISlider):
    """Slider of Windows design system"""
    def onSlide(func):
        func()


class MacSlider(ISlider):
    """Slider of macOS design system"""
    def onSlide(func):
        func()

The Problem ๐Ÿšฉ

On each page of your application, you have to do something like

def main():
    is_windows = os.name == 'nt'
    button = WinButton() if is_windows else MacButton()
    slider = WinSlider() if is_windows else MacSlider()

The Solution ๐Ÿ–

To solve this, we will create a factory interface, and both MacUiFactory and WindowsUiFactory will extend it

class IGUI(ABC):

    @abstractmethod
    def create_button() -> IButton:
        pass

    @abstractmethod
    def create_slider() -> ISlider:
        pass


class WinUiFactory(IGUI):
    def create_button():
        return WinButton()

    def create_slider():
        return WinSlider()


class MacUiFactory(IGUI):
    def create_button():
        return MacButton()

    def create_slider():
        return MacSlider()

We will add another class UI which will compose IGUI factory

class UI:
    """Config the current factory"""
    def __init__(self) -> None:
        is_windows = os.name == 'nt'
        self.factory = WinUiFactory() if is_windows else MacUiFactory

And now, we can easily use the correct UI elements without repeating our code, main becomes:

def main():
    ui = UI()
    button = ui.factory.create_button()
    slider = ui.factory.create_slider()

    print(button) # <__main__.MacButton object at 0x7f11599515b0>
    print(slider) # <__main__.MacSlider object at 0x7f11599515e0>

You won't now have to change the whole code base if you decided to support a new OS.

When to use it? ๐Ÿค”

  • Factory Pattern: Use it whenever you have a complex logic behind the creation of a single object.

  • Abstract Factory Pattern: Use it when you have different variations of objects that are unknown at the definition time.

Pros and Cons ๐Ÿ˜‡

Pros โœ…

  • it supports the Single Responsibility Principle (SOLID); by separating the logic of creation out of the use case
  • it supports Open/Closed Principle (SOLID); by separating the logic of creation out of the use case which will allow adding more variants or objects without breaking the client's code.

Cons โ›”

  • The code might get complex because it introduces many more interfaces and abstract classes

Code โŒจ

You can review the whole code that was used on this GitHub repo

Conclusion

If your code is repetitive with the same if statements, you probably need something like the Factory Pattern, but always keep in mind that overusing the Factory Pattern will result in a more complex, hard-to-read code.

You might notice that in this pattern the subclass decides what class to be instantiated and that would be decided in the runtime.

I'm planning to cover the most common design patterns. if you are interested, you can follow me to get notified of any new articles that I publish in the future or you can check the below list of my published articles about design patterns:

ย