8.3. Factory Method

  • EN: Factory Method

  • PL: Metoda wytwórcza

  • Type: class

8.3.1. Pattern

  • Defer the creation of an object to subclasses

  • Relays on inheritance and polymorphism

  • Adds flexibility to the design

../../_images/designpatterns-factorymethod-pattern.png

8.3.2. Problem

  • Tightly coupled with MatchaEngine

  • What if we have better templating engine

../../_images/designpatterns-factorymethod-problem.png

from abc import ABC, abstractmethod
from typing import Any


TEMPLATE = """

<h1>Products</h1>

{% for product in products %}
    <p>{{ product.title }}</p>

"""

class ViewEngine(ABC):
    @abstractmethod
    def render(self, view_name: str, context: dict[str, Any]): ...


class MatchaViewEngine(ViewEngine):
    def render(self, view_name: str, context: dict[str, Any]) -> str:
        return 'View rendered by Matcha'


class Controller:
    def render(self, view_name: str, context: dict[str, Any], engine: ViewEngine) -> None:
        html = engine.render(view_name, context)
        print(html)


class ProductsController(Controller):
    def list_products(self) -> None:
        context: dict[str, Any] = {}
        # get products from a database
        # context[products] = products
        self.render('products.html', context, MatchaViewEngine())

8.3.3. Solution

../../_images/designpatterns-factorymethod-solution.png

from abc import ABC, abstractmethod
from typing import Any


TEMPLATE = """

<h1>Products</h1>

{% for product in products %}
    <p>{{ product.title }}</p>

"""

class ViewEngine(ABC):
    @abstractmethod
    def render(self, view_name: str, context: dict[str, Any]): ...



class MatchaViewEngine(ViewEngine):
    def render(self, view_name: str, context: dict[str, Any]) -> str:
        return 'View rendered by Matcha'

class Controller:
    def _create_view_engine(self) -> ViewEngine:
        return MatchaViewEngine()

    def render(self, view_name: str, context: dict[str, Any]) -> None:
        engine = self._create_view_engine()
        html = engine.render(view_name, context)
        print(html)



class SharpViewEngine(ViewEngine):
    def render(self, view_name: str, context: dict[str, Any]):
        return 'View rendered by Sharp'

class SharpController(Controller):
    def _create_view_engine(self) -> ViewEngine:
        return SharpViewEngine()



class ProductsController(SharpController):
    def list_products(self) -> None:
        context: dict[str, Any] = {}
        # get products from a database
        # context[products] = products
        self.render('products.html', context)


if __name__ == '__main__':
    ProductsController().list_products()

8.3.4. Use Case - 0x01

class Setosa:
    pass

class Versicolor:
    pass

class Virginica:
    pass


def iris_factory(species):
    if species == 'setosa':
        return Setosa
    elif species == 'versicolor':
        return Versicolor
    elif species == 'virginica':
        return Virginica
    else:
        raise NotImplementedError


if __name__ == '__main__':
    iris = iris_factory('setosa')
    print(iris)
    # <class '__main__.Setosa'>

    iris = iris_factory('virginica')
    print(iris)
    # <class '__main__.Virginica'>

    iris = iris_factory('arctica')
    print(iris)
    # Traceback (most recent call last):
    # NotImplementedError

8.3.5. Use Case - 0x02

class Setosa:
    pass

class Versicolor:
    pass

class Virginica:
    pass


def iris_factory(species):
    cls = {
        'setosa': Setosa,
        'versicolor': Versicolor,
        'virginica': Virginica,
    }.get(species, None)

    if not cls:
        raise NotImplementedError
    else:
        return cls


if __name__ == '__main__':
    iris = iris_factory('setosa')
    print(iris)
    # <class '__main__.Setosa'>

    iris = iris_factory('virginica')
    print(iris)
    # <class '__main__.Virginica'>

    iris = iris_factory('arctica')
    print(iris)
    # Traceback (most recent call last):
    # NotImplementedError

8.3.6. Use Case - 0x03

class Setosa:
    pass

class Virginica:
    pass

class Versicolor:
    pass


def iris_factory(species):
    try:
        classname = species.capitalize()
        return globals()[classname]
    except KeyError:
        raise NotImplementedError


if __name__ == '__main__':
    iris = iris_factory('setosa')
    print(iris)
    # <class '__main__.Setosa'>

    iris = iris_factory('virginica')
    print(iris)
    # <class '__main__.Virginica'>

    iris = iris_factory('arctica')
    print(iris)
    # Traceback (most recent call last):
    # NotImplementedError

8.3.7. Use Case - 0x04

class PDF:
    pass

class TXT:
    pass


class File:
    def __new__(cls, *args, **kwargs):
        filename, extension = args[0].split('.')
        if extension == 'pdf':
            return PDF()
        elif extension == 'txt':
            return TXT()


if __name__ == '__main__':
    file = File('myfile.pdf')
    print(file)
    # <__main__.PDF object at 0x...>

    file = File('myfile.txt')
    print(file)
    # <__main__.TXT object at 0x...>

8.3.8. Use Case - 0x05

from abc import ABC, abstractproperty


class Document(ABC):
    @abstractproperty
    @property
    def extension(self):
        return

    def __new__(cls, filename, *args, **kwargs):
        name, extension = filename.split('.')
        for cls in Document.__subclasses__():
            if cls.extension == extension:
                return super().__new__(cls)
        else:
            raise NotImplementedError('File format unknown')


class PDF(Document):
    extension = 'pdf'

class Txt(Document):
    extension = 'txt'

class Word(Document):
    extension = 'docx'


if __name__ == '__main__':
    file = Document('myfile.txt')
    print(type(file))
    # <class '__main__.Txt'>

    file = Document('myfile.pdf')
    print(type(file))
    # <class '__main__.PDF'>

8.3.9. Use Case - 0x06

from abc import ABC, abstractproperty, abstractmethod
from dataclasses import dataclass


@dataclass
class ConfigParser(ABC):
    filename: str

    @abstractproperty
    @property
    def extension(self):
        pass

    def show(self):
        content = self.read()
        return self.parse(content)

    @abstractmethod
    def parse(self, content: str) -> dict:
        return NotImplementedError

    def read(self):
        with open(self.filename) as file:
            return file.read()

    def __new__(cls, filename, *args, **kwargs):
        _, extension = filename.split('.')
        for parser in cls.__subclasses__():
            if parser.extension == extension:
                instance = super().__new__(parser)
                instance.__init__(filename)
                return instance
        else:
            raise NotImplementedError('Parser for given file type not found')


class ConfigParserINI(ConfigParser):
    extension = 'ini'

    def parse(self, content: str) -> dict:
        print('Parsing INI file')


class ConfigParserCSV(ConfigParser):
    extension = 'csv'

    def parse(self, content: str) -> dict:
        print('Parsing CSV file')


class ConfigParserYAML(ConfigParser):
    extension = 'yaml'

    def parse(self, content: str) -> dict:
        print('Parsing YAML file')


class ConfigFileJSON(ConfigParser):
    extension = 'json'

    def parse(self, content: str) -> dict:
        print('Parsing JSON file')


class ConfigFileXML(ConfigParser):
    extension = 'xml'

    def parse(self, content: str) -> dict:
        print('Parsing XML file')


if __name__ == '__main__':
    # iris.csv or *.csv, *.json *.yaml...
    # filename = input('Type filename: ')
    config = ConfigParser('/tmp/myfile.json')
    config.show()

8.3.10. Use Case - 0x07

import os


class HttpClientInterface:
    def GET(self):
        raise NotImplementedError

    def POST(self):
        raise NotImplementedError


class GatewayLive(HttpClientInterface):
    def GET(self):
        print('Execute GET request over network')
        return ...

    def POST(self):
        print('Execute POST request over network')
        return ...


class GatewayStub(HttpClientInterface):
    def GET(self):
        print('Returning stub GET')
        return {'firstname': 'Mark', 'lastname': 'Watney'}

    def POST(self):
        print('Returning stub POST')
        return {'status': 200, 'reason': 'OK'}


class HttpGatewayFactory:
    def __new__(cls, *args, **kwargs):
        if os.getenv('ENVIRONMENT') == 'production':
            return GatewayLive()
        else:
            return GatewayStub()


if __name__ == '__main__':
    os.environ['ENVIRONMENT'] = 'testing'

    client = HttpGatewayFactory()
    result = client.GET()
    # Returning stub GET
    result = client.POST()
    # Returning stub POST

    os.environ['ENVIRONMENT'] = 'production'

    client = HttpGatewayFactory()
    result = client.GET()
    # Execute GET request over network
    result = client.POST()
    # Execute POST request over network

8.3.11. Use Case - 0x08

from abc import ABC, abstractmethod


class Path(ABC):
    def __new__(cls, path, *args, **kwargs):
        if path.startswith(r'C:\Users'):
            instance = object.__new__(WindowsPath)
        if path.startswith('/home'):
            return object.__new__(LinuxPath)
        if path.startswith('/Users'):
            return object.__new__(macOSPath)
        instance.__init__(path)
        return instance

    def __init__(self, filename):
        self.filename = filename

    @abstractmethod
    def dir_create(self): pass

    @abstractmethod
    def dir_list(self): pass

    @abstractmethod
    def dir_remove(self): pass


class WindowsPath(Path):
    def dir_create(self):
        print('create directory on ')

    def dir_list(self):
        print('list directory on ')

    def dir_remove(self):
        print('remove directory on ')


class LinuxPath(Path):
    def dir_create(self):
        print('create directory on ')

    def dir_list(self):
        print('list directory on ')

    def dir_remove(self):
        print('remove directory on ')


class macOSPath(Path):
    def dir_create(self):
        print('create directory on ')

    def dir_list(self):
        print('list directory on ')

    def dir_remove(self):
        print('remove directory on ')


if __name__ == '__main__':
    file = Path(r'C:\Users\MWatney\myfile.txt')
    print(type(file))
    # <class '__main__.WindowsPath'>

    file = Path(r'/home/mwatney/myfile.txt')
    print(type(file))
    # <class '__main__.LinuxPath'>

    file = Path(r'/Users/mwatney/myfile.txt')
    print(type(file))
    # <class '__main__.macOSPath'>

8.3.12. Assignments

Code 8.42. Solution
"""
* Assignment: DesignPatterns Creational FactoryMethod
* Complexity: easy
* Lines of code: 6 lines
* Time: 8 min

English:
    1. Create abstract factory `iris` producing instances of `Iris`
    2. Separate `values` from `species` in each row
    3. Create instances of:
        a. class `Setosa` if `species` is "setosa"
        b. class `Versicolor` if `species` is "versicolor"
        c. class `Virginica` if `species` is "virginica"
    4. Initialize instances with `values`
    5. Run doctests - all must succeed

Polish:
    1. Stwórz fabrykę abstrakcyjną `iris` tworzącą instancje klasy `Iris`
    2. Odseparuj `values` od `species` w każdym wierszu
    3. Stwórz instancje:
        a. klasy `Setosa` jeżeli `species` to "setosa"
        b. klasy `Versicolor` jeżeli `species` to "versicolor"
        c. klasy `Virginica` jeżeli `species` to "virginica"
    4. Instancje inicjalizuj danymi z `values`
    5. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `globals()[classname]`

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from pprint import pprint

    >>> result = map(iris, DATA[1:])
    >>> pprint(list(result), width=120)
    [Virginica(sepal_length=5.8, sepal_width=2.7, petal_length=5.1, petal_width=1.9),
     Setosa(sepal_length=5.1, sepal_width=3.5, petal_length=1.4, petal_width=0.2),
     Versicolor(sepal_length=5.7, sepal_width=2.8, petal_length=4.1, petal_width=1.3),
     Virginica(sepal_length=6.3, sepal_width=2.9, petal_length=5.6, petal_width=1.8),
     Versicolor(sepal_length=6.4, sepal_width=3.2, petal_length=4.5, petal_width=1.5),
     Setosa(sepal_length=4.7, sepal_width=3.2, petal_length=1.3, petal_width=0.2)]
"""

from dataclasses import dataclass


DATA = [
    ('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'),
    (5.8, 2.7, 5.1, 1.9, 'virginica'),
    (5.1, 3.5, 1.4, 0.2, 'setosa'),
    (5.7, 2.8, 4.1, 1.3, 'versicolor'),
    (6.3, 2.9, 5.6, 1.8, 'virginica'),
    (6.4, 3.2, 4.5, 1.5, 'versicolor'),
    (4.7, 3.2, 1.3, 0.2, 'setosa'),
]


@dataclass
class Iris:
    sepal_length: float
    sepal_width: float
    petal_length: float
    petal_width: float

class Setosa(Iris):
    pass

class Versicolor(Iris):
    pass

class Virginica(Iris):
    pass


# Skip header and separate `values` from `species` in each row
# Create instances of: `Setosa`, `Versicolor`, `Virginica` based on `species`
# Initialize instances with `values`
# type: Callable[[tuple[float,float,float,float,str], Iris]]
def iris(row: tuple[float,float,float,float,str]) -> Iris:
    ...