Factory Pattern in Python
Thomas J. Kennedy
1 Overview
Our first Factory Pattern will be based on a Python example. Counter to intuition… the implementations of the ShapeFactory
(class or struct) and shape_factory
(module) are quite similar between Python and Rust.
The only real differences come down to mechanics.
Once we have established how the Python version is implemented… we can use that to evaluate the Rust version.
2 The HashMap (Dictionary)
The core of the Shape Factory is its lookup table. In Python it is a dictionary.
_KNOWN_SHAPES = {
"Triangle": Triangle,
"Right Triangle": RightTriangle,
"Equilateral Triangle": EquilateralTriangle,
"Square": Square,
"Circle": Circle
}
The key is the name of a shape. The corresponding value is the function that will be used to create that shape. At the risk of getting lost in type hints…
_KNOWN_SHAPES: dict[str, Callable[[None | list[float]], Shape]] = {
# ...
}
The type hint indicates that…
-
the key is an
str
-
the value is a function (or something that behaves like a function) that accepts either no arguments (
None
) or multiple floats (list[float]
).
Keep in mind that the callables in this example are constructors for Shape
subtypes.
3 Creating Shapes
There are two functions that can be used to create shapes…
-
Create a shape with all of its dimensions set to one.
def create(name: str) -> Optional[Shape]: """ Create a Shape Args: name: the shape to be created Returns: A shape with the specified name or null if no matching shape is found """ if name in _KNOWN_SHAPES: return _KNOWN_SHAPES[name]() return None
-
Create a shape with specified dimensions.
def create_from_dimensions(name: str, values: list[float]) -> Optional[Shape]: """ Create a Shape Args: name: the shape to be created values: dictionary of values corresponding to the data needed to inialize a shape Returns: A shape with the specified name or null if no matching shape is found """ if name in _KNOWN_SHAPES: return _KNOWN_SHAPES[name](*values) return None
The optional return type (Optional[Shape]
) indicates that, for both functions, it is possible to get nothing (None
) instead of a Shape
.
This can occur for both functions if the shape name is not known (i.e., not a key in _KNOWN_SHAPES
).
4 Convenience
There are three (3) convenience functions in the Shape Factory.
def is_known(name: str) -> bool:
"""
Determine whether a given shape is known
Args:
name: the shape for which to query
"""
return name in _KNOWN_SHAPES
def list_known() -> str:
"""
Print a list of known Shapes
"""
return "\n".join((f" {name:}" for name in _KNOWN_SHAPES))
def number_known() -> int:
"""
Determine the number of known Shapes
"""
return len(_KNOWN_SHAPES)
This allows the client code to…
-
determine whether a shape name is known (which is useful when implementing parser logic).
-
obtain a printable list of supported shape names.
-
determine how many shapes are supported by the Shape Factory.
None of these functions are necessary in the Factory Pattern… However, they do, in my opinion, fall under interface completeness.
5 How About a Definition?
The Factory Pattern allows us to keep track of what we can create.
In the Shape Factory Example we are…
-
keeping track of which
class
is used for each Shape -
keeping track of which shapes we can actually represent