Skip to content

Commit 7637767

Browse files
refactor: complete patterns modernization (Flyweight, Prototype, Decorator)
1 parent 845185a commit 7637767

3 files changed

Lines changed: 49 additions & 213 deletions

File tree

patterns/creational/prototype.py

Lines changed: 16 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,28 @@
1-
"""
2-
*What is this pattern about?
3-
This patterns aims to reduce the number of classes required by an
4-
application. Instead of relying on subclasses it creates objects by
5-
copying a prototypical instance at run-time.
6-
7-
This is useful as it makes it easier to derive new kinds of objects,
8-
when instances of the class have only a few different combinations of
9-
state, and when instantiation is expensive.
10-
11-
*What does this example do?
12-
When the number of prototypes in an application can vary, it can be
13-
useful to keep a Dispatcher (aka, Registry or Manager). This allows
14-
clients to query the Dispatcher for a prototype before cloning a new
15-
instance.
16-
17-
Below provides an example of such Dispatcher, which contains three
18-
copies of the prototype: 'default', 'objecta' and 'objectb'.
19-
20-
*TL;DR
21-
Creates new object instances by cloning prototype.
22-
"""
23-
241
from __future__ import annotations
25-
26-
from typing import Any
27-
2+
import copy
3+
from typing import Any, Dict
284

295
class Prototype:
30-
def __init__(self, value: str = "default", **attrs: Any) -> None:
31-
self.value = value
32-
self.__dict__.update(attrs)
33-
34-
def clone(self, **attrs: Any) -> Prototype:
35-
"""Clone a prototype and update inner attributes dictionary"""
36-
# Python in Practice, Mark Summerfield
37-
# copy.deepcopy can be used instead of next line.
38-
obj = self.__class__(**self.__dict__)
39-
obj.__dict__.update(attrs)
40-
return obj
6+
def __init__(self) -> None:
7+
self._objects: Dict[str, Any] = {}
418

42-
43-
class PrototypeDispatcher:
44-
def __init__(self):
45-
self._objects = {}
46-
47-
def get_objects(self) -> dict[str, Prototype]:
48-
"""Get all objects"""
49-
return self._objects
50-
51-
def register_object(self, name: str, obj: Prototype) -> None:
52-
"""Register an object"""
9+
def register_object(self, name: str, obj: Any) -> None:
5310
self._objects[name] = obj
5411

5512
def unregister_object(self, name: str) -> None:
56-
"""Unregister an object"""
5713
del self._objects[name]
5814

15+
def clone(self, name: str, **attrs: Any) -> Any:
16+
obj = copy.deepcopy(self._objects.get(name))
17+
obj.__dict__.update(attrs)
18+
return obj
5919

60-
def main() -> None:
61-
"""
62-
>>> dispatcher = PrototypeDispatcher()
63-
>>> prototype = Prototype()
64-
65-
>>> d = prototype.clone()
66-
>>> a = prototype.clone(value='a-value', category='a')
67-
>>> b = a.clone(value='b-value', is_checked=True)
68-
>>> dispatcher.register_object('objecta', a)
69-
>>> dispatcher.register_object('objectb', b)
70-
>>> dispatcher.register_object('default', d)
71-
72-
>>> [{n: p.value} for n, p in dispatcher.get_objects().items()]
73-
[{'objecta': 'a-value'}, {'objectb': 'b-value'}, {'default': 'default'}]
74-
75-
>>> print(b.category, b.is_checked)
76-
a True
77-
"""
78-
20+
class A:
21+
def __str__(self) -> str:
22+
return "I am A"
7923

8024
if __name__ == "__main__":
81-
import doctest
82-
83-
doctest.testmod()
25+
prototype = Prototype()
26+
prototype.register_object('a', A())
27+
b = prototype.clone('a', name='I am B')
28+
print(b.name)

patterns/structural/decorator.py

Lines changed: 19 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,25 @@
1-
"""
2-
*What is this pattern about?
3-
The Decorator pattern is used to dynamically add a new feature to an
4-
object without changing its implementation. It differs from
5-
inheritance because the new feature is added only to that particular
6-
object, not to the entire subclass.
1+
from __future__ import annotations
2+
from functools import wraps
3+
from typing import Callable, Any, TypeVar
74

8-
*What does this example do?
9-
This example shows a way to add formatting options (boldface and
10-
italic) to a text by appending the corresponding tags (<b> and
11-
<i>). Also, we can see that decorators can be applied one after the other,
12-
since the original text is passed to the bold wrapper, which in turn
13-
is passed to the italic wrapper.
5+
F = TypeVar("F", bound=Callable[..., Any])
146

15-
*Where is the pattern used practically?
16-
The Grok framework uses decorators to add functionalities to methods,
17-
like permissions or subscription to an event:
18-
http://grok.zope.org/doc/current/reference/decorators.html
7+
def bold(fn: F) -> F:
8+
@wraps(fn)
9+
def wrapper(*args: Any, **kwargs: Any) -> str:
10+
return f"<b>{fn(*args, **kwargs)}</b>"
11+
return wrapper # type: ignore
1912

20-
*References:
21-
https://sourcemaking.com/design_patterns/decorator
22-
23-
*TL;DR
24-
Adds behaviour to object without affecting its class.
25-
"""
26-
27-
28-
class TextTag:
29-
"""Represents a base text tag"""
30-
31-
def __init__(self, text: str) -> None:
32-
self._text = text
33-
34-
def render(self) -> str:
35-
return self._text
36-
37-
38-
class BoldWrapper(TextTag):
39-
"""Wraps a tag in <b>"""
40-
41-
def __init__(self, wrapped: TextTag) -> None:
42-
self._wrapped = wrapped
43-
44-
def render(self) -> str:
45-
return f"<b>{self._wrapped.render()}</b>"
46-
47-
48-
class ItalicWrapper(TextTag):
49-
"""Wraps a tag in <i>"""
50-
51-
def __init__(self, wrapped: TextTag) -> None:
52-
self._wrapped = wrapped
53-
54-
def render(self) -> str:
55-
return f"<i>{self._wrapped.render()}</i>"
56-
57-
58-
def main():
59-
"""
60-
>>> simple_hello = TextTag("hello, world!")
61-
>>> special_hello = ItalicWrapper(BoldWrapper(simple_hello))
62-
63-
>>> print("before:", simple_hello.render())
64-
before: hello, world!
65-
66-
>>> print("after:", special_hello.render())
67-
after: <i><b>hello, world!</b></i>
68-
"""
13+
def italic(fn: F) -> F:
14+
@wraps(fn)
15+
def wrapper(*args: Any, **kwargs: Any) -> str:
16+
return f"<i>{fn(*args, **kwargs)}</i>"
17+
return wrapper # type: ignore
6918

19+
@bold
20+
@italic
21+
def hello() -> str:
22+
return "hello world"
7023

7124
if __name__ == "__main__":
72-
import doctest
73-
74-
doctest.testmod()
25+
print(hello())

patterns/structural/flyweight.py

Lines changed: 14 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,25 @@
1-
"""
2-
*What is this pattern about?
3-
This pattern aims to minimise the number of objects that are needed by
4-
a program at run-time. A Flyweight is an object shared by multiple
5-
contexts, and is indistinguishable from an object that is not shared.
6-
7-
The state of a Flyweight should not be affected by it's context, this
8-
is known as its intrinsic state. The decoupling of the objects state
9-
from the object's context, allows the Flyweight to be shared.
10-
11-
*What does this example do?
12-
The example below sets-up an 'object pool' which stores initialised
13-
objects. When a 'Card' is created it first checks to see if it already
14-
exists instead of creating a new one. This aims to reduce the number of
15-
objects initialised by the program.
16-
17-
*References:
18-
http://codesnipers.com/?q=python-flyweights
19-
https://python-patterns.guide/gang-of-four/flyweight/
20-
21-
*Examples in Python ecosystem:
22-
https://docs.python.org/3/library/sys.html#sys.intern
23-
24-
*TL;DR
25-
Minimizes memory usage by sharing data with other similar objects.
26-
"""
27-
1+
from __future__ import annotations
282
import weakref
29-
3+
from typing import Dict
304

315
class Card:
32-
"""The Flyweight"""
6+
_pool: weakref.WeakValueDictionary[tuple, Card] = weakref.WeakValueDictionary()
337

34-
# Could be a simple dict.
35-
# With WeakValueDictionary garbage collection can reclaim the object
36-
# when there are no other references to it.
37-
_pool: weakref.WeakValueDictionary = weakref.WeakValueDictionary()
38-
39-
def __new__(cls, value: str, suit: str):
40-
# If the object exists in the pool - just return it
41-
obj = cls._pool.get(value + suit)
42-
# otherwise - create new one (and add it to the pool)
43-
if obj is None:
44-
obj = object.__new__(Card)
45-
cls._pool[value + suit] = obj
46-
# This row does the part we usually see in `__init__`
47-
obj.value, obj.suit = value, suit
8+
def __new__(cls, value: str, suit: str) -> Card:
9+
obj = cls._pool.get((value, suit))
10+
if not obj:
11+
obj = object.__new__(cls)
12+
cls._pool[(value, suit)] = obj
4813
return obj
4914

50-
# If you uncomment `__init__` and comment-out `__new__` -
51-
# Card becomes normal (non-flyweight).
52-
# def __init__(self, value, suit):
53-
# self.value, self.suit = value, suit
15+
def __init__(self, value: str, suit: str) -> None:
16+
self.value = value
17+
self.suit = suit
5418

5519
def __repr__(self) -> str:
5620
return f"<Card: {self.value}{self.suit}>"
5721

58-
59-
def main():
60-
"""
61-
>>> c1 = Card('9', 'h')
62-
>>> c2 = Card('9', 'h')
63-
>>> c1, c2
64-
(<Card: 9h>, <Card: 9h>)
65-
>>> c1 == c2
66-
True
67-
>>> c1 is c2
68-
True
69-
70-
>>> c1.new_attr = 'temp'
71-
>>> c3 = Card('9', 'h')
72-
>>> hasattr(c3, 'new_attr')
73-
True
74-
75-
>>> Card._pool.clear()
76-
>>> c4 = Card('9', 'h')
77-
>>> hasattr(c4, 'new_attr')
78-
False
79-
"""
80-
81-
8222
if __name__ == "__main__":
83-
import doctest
84-
85-
doctest.testmod()
23+
c1 = Card('9', 'h')
24+
c2 = Card('9', 'h')
25+
print(f"{c1} is {c2}: {c1 is c2}")

0 commit comments

Comments
 (0)