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

8.3.2. Problem¶
Tightly coupled with MatchaEngine
What if we have better templating engine

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¶

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¶
"""
* 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:
...