4.4. Abstract Factory

  • Provide an interface for creating families of related objects

  • Factory Method is a method

  • Abstract Factory is an abstraction (interface)

  • Used for theme support (which generates buttons, inputs etc)

The Abstract Factory design pattern is a creational design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes.

4.4.1. Problem

>>> import sys
>>> from dataclasses import dataclass
>>>
>>>
>>> # iOS Components
>>> @dataclass
... class iOSLabel:
...     name: str
...     def render(self):
...         return f'iOS Label'
>>>
>>> @dataclass
... class iOSButton:
...     name: str
...     def render(self):
...         return f'iOS Button'
>>>
>>>
>>> # Android Components
... @dataclass
... class AndroidLabel:
...     name: str
...     def render(self):
...         return f'Android Label'
>>>
>>> @dataclass
... class AndroidButton:
...     name: str
...     def render(self):
...         return f'Android Button'
>>>
>>>
>>> # Main
>>> def main():
...     match sys.platform:
...         case 'ios':
...             iOSLabel('username')
...             iOSLabel('password')
...             iOSButton('login')
...         case 'android':
...             AndroidLabel('username')
...             AndroidLabel('password')
...             AndroidButton('login')

4.4.2. Solution

>>> import sys
>>> from abc import ABC, abstractmethod
>>> from dataclasses import dataclass
>>>
>>>
>>> # iOS Components
>>> @dataclass
... class iOSLabel:
...     name: str
...     def render(self):
...         return f'iOS Label'
>>>
>>> @dataclass
... class iOSButton:
...     name: str
...     def render(self):
...         return f'iOS Button'
>>>
>>>
>>> # Android Components
>>> @dataclass
... class AndroidLabel:
...     name: str
...     def render(self):
...         return f'Android Label'
>>>
>>> @dataclass
... class AndroidButton:
...     name: str
...     def render(self):
...         return f'Android Button'
>>>
>>>
>>> # Factories
>>> class AbstractFactory(ABC):
...     @abstractmethod
...     def create_label(self, name):
...         raise NotImplementedError
...
...     @abstractmethod
...     def create_button(self, name):
...         raise NotImplementedError
>>>
>>> class iOSFactory(AbstractFactory):
...     def create_label(self, name):
...         return iOSLabel(name)
...
...     def create_button(self, name):
...         return iOSButton(name)
>>>
>>> class AndroidFactory(AbstractFactory):
...     def create_label(self, name):
...         return AndroidLabel(name)
...
...     def create_button(self, name):
...         return AndroidButton(name)
>>>
>>>
>>> # Main
>>> def main():
...     match sys.platform:
...         case 'ios': factory = iOSFactory()
...         case 'android': factory = AndroidFactory()
...         case _: raise ValueError(f'Unknown platform: {sys.platform}')
...
...     factory.create_label('username')
...     factory.create_label('password')
...     factory.create_button('login')

4.4.3. Case Study

  • Violates Open/Close Principle

  • Hard to add a new theme

  • Easy to accidentally use Material widget inside of Flat theme block

Problem:

from dataclasses import dataclass
import sys


# iOS Components

@dataclass
class iOSLabel:
    name: str
    def render(self):
        return 'iOS Label'

@dataclass
class iOSButton:
    name: str
    def render(self):
        return 'iOS Button'


# Android Components

@dataclass
class AndroidLabel:
    name: str
    def render(self):
        return 'Android Label'

@dataclass
class AndroidButton:
    name: str
    def render(self):
        return 'Android Button'


# Main

if __name__ == '__main__':

    if sys.platform == 'ios':
        iOSLabel('username')
        iOSLabel('password')
        iOSButton('login')
    elif sys.platform == 'android':
        AndroidLabel('username')
        AndroidLabel('password')
        AndroidButton('login')

Solution:

from dataclasses import dataclass
import sys


# iOS Components (the same as before)

@dataclass
class iOSLabel:
    name: str
    def render(self):
        return 'iOS Label'

@dataclass
class iOSButton:
    name: str
    def render(self):
        return 'iOS Button'


# Android Components (the same as before)

@dataclass
class AndroidLabel:
    name: str
    def render(self):
        return 'Android Label'

@dataclass
class AndroidButton:
    name: str
    def render(self):
        return 'Android Button'


# Abstract Factories

class iOSFactory:
    def create_label(self, name):
        return iOSLabel(name)

    def create_button(self, name):
        return iOSButton(name)


class AndroidFactory:
    def create_label(self, name):
        return AndroidLabel(name)

    def create_button(self, name):
        return AndroidButton(name)


# Main

if __name__ == '__main__':

    if sys.platform == 'ios':
        factory = iOSFactory()
    elif sys.platform == 'android':
        factory = AndroidFactory()

    factory.create_label('username')
    factory.create_label('password')
    factory.create_button('login')

4.4.4. Use Case 1

>>> from abc import ABC, abstractmethod
>>>
>>>
>>> # Abstract Products
>>> class User(ABC):
...     @abstractmethod
...     def login(self):
...         pass
>>>
>>> class Admin(ABC):
...     @abstractmethod
...     def login(self):
...         pass
>>>
>>>
>>> # Concrete Products
>>> class RegularUser(User):
...     def __init__(self, username):
...         self.username = username
...
...     def login(self):
...         return f"Regular user {self.username} logged in"
>>>
>>> class SuperUser(User):
...     def __init__(self, username):
...         self.username = username
...
...     def login(self):
...         return f"Super user {self.username} logged in"
>>>
>>> class SystemAdmin(Admin):
...     def __init__(self, username):
...         self.username = username
...
...     def login(self):
...         return f"System admin {self.username} logged in"
>>>
>>> class NetworkAdmin(Admin):
...     def __init__(self, username):
...         self.username = username
...
...     def login(self):
...         return f"Network admin {self.username} logged in"
>>>
>>> # Abstract Factory
>>> class AccountFactory(ABC):
...     @abstractmethod
...     def create_user(self, username):
...         pass
...
...     @abstractmethod
...     def create_admin(self, username):
...         pass
>>>
>>> # Concrete Factories
>>> class StandardAccountFactory(AccountFactory):
...     def create_user(self, username):
...         return RegularUser(username)
...
...     def create_admin(self, username):
...         return SystemAdmin(username)
>>>
>>> class EnterpriseAccountFactory(AccountFactory):
...     def create_user(self, username):
...         return SuperUser(username)
...
...     def create_admin(self, username):
...         return NetworkAdmin(username)
>>>
>>>
>>> # Main
>>> def main():
...     # Create a standard account factory
...     standard_factory = StandardAccountFactory()
...
...     # Create alice as a regular user
...     alice = standard_factory.create_user("alice")
...     print(alice.login())  # Regular user alice logged in
...
...     # Create an enterprise account factory
...     enterprise_factory = EnterpriseAccountFactory()
...
...     # Create bob as a network admin
...     bob = enterprise_factory.create_admin("bob")
...     print(bob.login())  # Network admin bob logged in

4.4.5. Assignments

# FIXME: Poprawić sekcję Types

# %% About
# - Name: DesignPatterns Creational AbstractFactory
# - Difficulty: easy
# - Lines: 70
# - Minutes: 21

# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% English
# 1. Implement Abstract Factory pattern
# 2. Run doctests - all must succeed

# %% Polish
# 1. Zaimplementuj wzorzec Abstract Factory
# 2. Uruchom doctesty - wszystkie muszą się powieść

# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from pprint import pprint

>>> main(Platform.iOS)
iOS Textbox username
iOS Textbox password
iOS Button submit

>>> main(Platform.Android)
Android Textbox username
Android Textbox password
Android Button submit
"""

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% Imports
from dataclasses import dataclass
from enum import Enum
from abc import ABC, abstractmethod

# %% Types

# %% Data

# %% Result
class Platform(Enum):
    iOS = 'iOS'
    Android = 'Android'


@dataclass
class Button:
    name: str

    def render(self, platform: Platform):
        if platform is platform.iOS:
            print(f'iOS Button {self.name}')
        elif platform is platform.Android:
            print(f'Android Button {self.name}')

@dataclass
class Textbox:
    name: str

    def render(self, platform: Platform):
        if platform is platform.iOS:
            print(f'iOS Textbox {self.name}')
        elif platform is platform.Android:
            print(f'Android Textbox {self.name}')


def main(platform: Platform):
    Textbox('username').render(platform)
    Textbox('password').render(platform)
    Button('submit').render(platform)