8.4. Abstract Factory

  • EN: Abstract Factory

  • PL: Fabryka Abstrakcyjna

  • Type: object

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.

Here's a simple example of the Abstract Factory pattern in Python:

>>> class AbstractFactory:
...     def create_product_a(self):
...         pass
...
...     def create_product_b(self):
...         pass
...
>>> class ConcreteFactory1(AbstractFactory):
...     def create_product_a(self):
...         return ConcreteProductA1()
...
...     def create_product_b(self):
...         return ConcreteProductB1()
...
>>> class ConcreteFactory2(AbstractFactory):
...     def create_product_a(self):
...         return ConcreteProductA2()
...
...     def create_product_b(self):
...         return ConcreteProductB2()
...
>>> class AbstractProductA:
...     pass
...
>>> class ConcreteProductA1(AbstractProductA):
...     pass
...
>>> class ConcreteProductA2(AbstractProductA):
...     pass
...
>>> class AbstractProductB:
...     pass
...
>>> class ConcreteProductB1(AbstractProductB):
...     pass
...
>>> class ConcreteProductB2(AbstractProductB):
...     pass
...
>>> factory1 = ConcreteFactory1()
>>> product_a1 = factory1.create_product_a()
>>> product_b1 = factory1.create_product_b()
>>> factory2 = ConcreteFactory2()
>>> product_a2 = factory2.create_product_a()
>>> product_b2 = factory2.create_product_b()

In this example, AbstractFactory is an interface for creating objects in a super factory which creates other factories. This factory is also called as factory of factories. This type of design pattern comes under creational pattern as this pattern provides one of the best ways to create an object. ConcreteFactory1 and ConcreteFactory2 are concrete classes that implement the AbstractFactory interface and define the create_product_a and create_product_b methods.

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.56. 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)