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)