8.4. Abstract Factory

  • EN: Abstract Factory

  • PL: Fabryka Abstrakcyjna

  • Type: object

The Abstract Factory pattern differs from the Factory Pattern in that it returns Factories, rather than objects of concrete class.

8.4.1. Pattern

  • 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)

../../_images/designpatterns-abstractfactory-pattern.png

8.4.2. Problem

  • Violates Open/Close Principle

  • Hard to add a new theme

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

../../_images/designpatterns-abstractfactory-problem.png

from abc import ABC, abstractmethod
from enum import Enum


#%% Interfaces
class Widget(ABC):
    @abstractmethod
    def render(self) -> None:
        raise NotImplementedError

class Button(Widget):
    pass

class Textbox(Widget):
    pass


#%% Material Theme
class MaterialButton(Button):
    def render(self) -> None:
        print('Material Button')

class MaterialTextbox(Textbox):
    def render(self) -> None:
        print('Material Textbox')


#%% Flat Theme
class FlatButton(Button):
    def render(self) -> None:
        print('Flat Button')

class FlatTextbox(Textbox):
    def render(self) -> None:
        print('Flat Textbox')


#%% Main
class Theme(Enum):
    MATERIAL = 1
    FLAT = 2


class ContactForm:
    def render(self, theme: Theme) -> None:
        match theme:
            case Theme.MATERIAL:
                MaterialTextbox().render()
                MaterialButton().render()
            case Theme.FLAT:
                FlatTextbox().render()
                FlatButton().render()


if __name__ == '__main__':

    ContactForm().render(Theme.FLAT)
    # Flat Textbox
    # Flat Button

    ContactForm().render(Theme.MATERIAL)
    # Material Textbox
    # Material Button

8.4.3. Solution

design-patterns/creational/img/designpatterns-abstractfactory-solution.png

#%% Interfaces
from abc import ABC, abstractmethod


class Widget(ABC):
    @abstractmethod
    def render(self) -> None:
        raise NotImplementedError

class Button(Widget):
    pass

class Textbox(Widget):
    pass

class Theme(ABC):
    @abstractmethod
    def create_button(self) -> Button:
        raise NotImplementedError

    @abstractmethod
    def create_textbox(self) -> Textbox:
        raise NotImplementedError


#%% Material Theme
class MaterialButton(Button):
    def render(self) -> None:
        print('Material Button')

class MaterialTextbox(Textbox):
    def render(self) -> None:
        print('Material Textbox')

class MaterialTheme(Theme):
    def create_button(self) -> Button:
        return MaterialButton()

    def create_textbox(self) -> Textbox:
        return MaterialTextbox()


#%% Flat Theme
class FlatButton(Button):
    def render(self) -> None:
        print('Flat Button')

class FlatTextbox(Textbox):
    def render(self) -> None:
        print('Flat Textbox')

class FlatTheme(Theme):
    def create_button(self) -> Button:
        return FlatButton()

    def create_textbox(self) -> Textbox:
        return FlatTextbox()


#%% Main
class ContactForm:
    def render(self, theme: Theme) -> None:
        theme.create_textbox().render()
        theme.create_button().render()


if __name__ == '__main__':

    theme = FlatTheme()
    ContactForm().render(theme)
    # Flat Textbox
    # Flat Button

    theme = MaterialTheme()
    ContactForm().render(theme)
    # Material Textbox
    # Material Button

8.4.4. Assignments

Code 8.43. Solution
"""
* Assignment: DesignPatterns Creational AbstractFactory
* Complexity: easy
* Lines of code: 70 lines
* Time: 21 min

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ść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> 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
"""
from dataclasses import dataclass
from enum import Enum


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)