8.6. Prototype¶
EN: Prototype
PL: Prototyp
Type: object
The Prototype design pattern is a creational design pattern that allows cloning objects, even complex ones, without coupling to their specific classes. All prototype classes have a common interface that makes it possible to clone objects.
Here's a simple example of the Prototype pattern in Python:
>>> import copy
...
>>> class Prototype:
... def clone(self):
... return copy.deepcopy(self)
...
>>> class ConcretePrototype1(Prototype):
... pass
...
>>> class ConcretePrototype2(Prototype):
... pass
...
>>> prototype1 = ConcretePrototype1()
>>> prototype2 = ConcretePrototype2()
>>> clone1 = prototype1.clone()
>>> clone2 = prototype2.clone()
In this example, Prototype is an abstract class that declares the clone method that returns a copy of the current object. ConcretePrototype1 and ConcretePrototype2 are concrete classes that implement the Prototype interface. The clone method uses the copy.deepcopy function to create a deep copy of the current object.
8.6.1. Pattern¶
Create new object by copying an existing object
8.6.2. Problem¶
Violates Open/Close Principle
from abc import ABC, abstractmethod
from dataclasses import dataclass
class Component(ABC):
@abstractmethod
def render(self) -> None: ...
@dataclass
class Circle(Component):
radius: int | None = None
color: str | None = None
def render(self) -> None:
print('Rendering circle')
if __name__ == '__main__':
a = Circle(radius=3, color='red')
b = Circle(radius=a.radius, color=a.color)
print(f'A: radius={a.radius}, color={a.color}')
print(f'B: radius={b.radius}, color={b.color}')
# A: radius=3, color=red
# B: radius=3, color=red
8.6.3. Solution¶
from dataclasses import dataclass
from typing import Self
from abc import ABC, abstractmethod
class Component(ABC):
@abstractmethod
def render(self) -> None: ...
@abstractmethod
def clone(self) -> Self: ...
@dataclass
class Circle(Component):
radius: int | None = None
color: str | None = None
def clone(self) -> Self:
new = Circle()
new.radius = self.radius
new.color = self.color
return new
def render(self) -> None:
print('Rendering circle')
if __name__ == '__main__':
a = Circle(radius=3, color='red')
b = a.clone()
print(f'A: radius={a.radius}, color={a.color}')
print(f'B: radius={b.radius}, color={b.color}')
# A: radius=3, color=red
# B: radius=3, color=red
8.6.4. Use Case - 0x01¶
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal
@dataclass
class User:
firstname: str
lastname: str
username: str
password: str
email: str
last_login: datetime | None
role: Literal['admin', 'user', 'guest']
groups: list[str] = field(default_factory=list)
def clone(self):
return User(
firstname = self.firstname,
lastname = self.lastname,
username = self.username,
password = self.password,
email = self.email,
last_login = self.last_login,
role = self.role,
groups = self.groups)
mark = User(
firstname='Mark',
lastname='Watney',
username='mwatney',
password='Ares3',
email='mwatney@nasa.gov',
last_login=None,
role='admin',
groups=['admins', 'users'],
)
melissa = mark.clone()
print(melissa)
# User(firstname='Mark', lastname='Watney', username='mwatney', password='Ares3', email='mwatney@nasa.gov', last_login=None, role='admin', groups=['admins', 'users'])
melissa.firstname = 'Melissa'
melissa.lastname = 'Lewis'
melissa.username = 'mlewis'
melissa.email = 'mlewis@nasa.gov'
print(melissa)
# User(firstname='Melissa', lastname='Lewis', username='mlewis', password='Ares3', email='mlewis@nasa.gov', last_login=None, role='admin', groups=['admins', 'users'])
8.6.5. Use Case - 0x02¶
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal
@dataclass
class User:
firstname: str
lastname: str
username: str
password: str
email: str
last_login: datetime | None
role: Literal['admin', 'user', 'guest']
groups: list[str] = field(default_factory=list)
def clone(self):
return User(**vars(self))
mark = User(
firstname='Mark',
lastname='Watney',
username='mwatney',
password='Ares3',
email='mwatney@nasa.gov',
last_login=None,
role='admin',
groups=['admins', 'users'],
)
melissa = mark.clone()
print(melissa)
# User(firstname='Mark', lastname='Watney', username='mwatney', password='Ares3', email='mwatney@nasa.gov', last_login=None, role='admin', groups=['admins', 'users'])
melissa.firstname = 'Melissa'
melissa.lastname = 'Lewis'
melissa.username = 'mlewis'
melissa.email = 'mlewis@nasa.gov'
print(melissa)
# User(firstname='Melissa', lastname='Lewis', username='mlewis', password='Ares3', email='mlewis@nasa.gov', last_login=None, role='admin', groups=['admins', 'users'])
8.6.6. Use Case - 0x03¶
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal
@dataclass
class User:
firstname: str
lastname: str
username: str
password: str
email: str
last_login: datetime | None
role: Literal['admin', 'user', 'guest']
groups: list[str] = field(default_factory=list)
def clone(self, **kwargs):
values = vars(self) | kwargs
return User(**values)
mark = User(
firstname='Mark',
lastname='Watney',
username='mwatney',
password='Ares3',
email='mwatney@nasa.gov',
last_login=None,
role='admin',
groups=['admins', 'users'],
)
melissa = mark.clone(
firstname='Melissa',
lastname='Lewis',
username='mlewis',
email='mlewis@nasa.gov',
)
print(melissa)
# User(firstname='Melissa', lastname='Lewis', username='mwatney', password='Ares3', email='mwatney@nasa.gov', last_login=None, role='admin', groups=['admins', 'users'])
8.6.7. Use Case - 0x04¶
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal
@dataclass
class User:
firstname: str
lastname: str
username: str
password: str
email: str
last_login: datetime | None
role: Literal['admin', 'user', 'guest']
groups: list[str] = field(default_factory=list)
def clone(self, **kwargs):
values = vars(self) | kwargs
cls = self.__class__
return cls(**values)
@dataclass
class Admin:
pass
mark = Admin(
firstname='Mark',
lastname='Watney',
username='mwatney',
password='Ares3',
email='mwatney@nasa.gov',
last_login=None,
role='admin',
groups=['admins', 'users'],
)
melissa = mark.clone(
firstname='Melissa',
lastname='Lewis',
username='mlewis',
email='mlewis@nasa.gov',
)
print(melissa)
# Admin(firstname='Melissa', lastname='Lewis', username='mwatney', password='Ares3', email='mwatney@nasa.gov', last_login=None, role='admin', groups=['admins', 'users'])
8.6.8. Assignments¶
"""
* Assignment: DesignPatterns Creational PrototypeDate
* Complexity: easy
* Lines of code: 5 lines
* Time: 3 min
English:
1. Create class `Date` with:
a. `year: int`
b. `month: int`
c. `day: int`
d. method `.clone()`
2. Method `.clone()` returns another `Date` with the same values
3. Run doctests - all must succeed
Polish:
TODO: Polish translation
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from pprint import pprint
>>> date = Date(1969, 7, 21)
>>> result = date.clone()
>>> result.year
1969
>>> result.month
7
>>> result.day
21
"""
from dataclasses import dataclass
@dataclass
class Date:
year: int
month: int
day: int
"""
* Assignment: DesignPatterns Creational PrototypeTime
* Complexity: easy
* Lines of code: 2 lines
* Time: 3 min
English:
1. Create class `Time` with:
a. `hour: int`
b. `minute: int`
c. `second: int`
d. `microsecond: int`
e. method `.clone()`
2. Method `.clone()` returns another `Time` with the same values
3. Use `vars(self)`
4. Run doctests - all must succeed
Polish:
1. Stwórz klasę `Time` z:
a. `hour: int`
b. `minute: int`
c. `second: int`
d. `microsecond: int`
e. metodą `.clone()`
2. Metoda `.clone()` zwraca kolejny `Time` z tymi samymi wartościami
3. Użyj `vars(self)`
4. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from pprint import pprint
>>> time = Time(2, 56, 15)
>>> result = time.clone()
>>> result.hour
2
>>> result.minute
56
>>> result.second
15
>>> result.microsecond
0
"""
from dataclasses import dataclass
@dataclass
class Time:
hour: int = 0
minute: int = 0
second: int = 0
microsecond: int = 0
"""
* Assignment: DesignPatterns Creational PrototypeDragon
* Complexity: easy
* Lines of code: 6 lines
* Time: 8 min
English:
1. Create class `Dragon`
2. Dragon has attributes:
a. `name: str`
b. `position: tuple[int,int]` default `(0, 0)`
c. `health: int` random from 50 to 100
d. `gold: int` random from 1 to 100
e. method `.clone()`
3. Method `.clone()` returns another `Dragon` with the same values
4. Use `random.randint()` to generate pseudorandom numbers
5. Run doctests - all must succeed
Polish:
TODO: Polish translation
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from pprint import pprint
>>> from random import seed
>>> seed(0)
>>> dragon = Dragon('Wawelski')
>>> result = dragon.clone()
>>> result.name
'Wawelski'
>>> result.health
74
>>> result.gold
98
>>> result.position
(0, 0)
"""
from dataclasses import dataclass, field
from random import randint
@dataclass
class Dragon:
...
TODO = """
You're building a video editor similar to Adobe Premier.
The editor contains a timeline of various types of components
such as text, clips, audio, and so on. The user should be able
to duplicate any component. The duplicated component should be
added to the timeline. Look at the implementation of the ContextMenu
class in the prototype package of the Exercises project. What are
the problems in the current implementation?Refactor the code using
the prototype pattern. What have you achieved?
"""