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

../../_images/designpatterns-prototype-pattern.png

8.6.2. Problem

  • Violates Open/Close Principle

../../_images/designpatterns-prototype-problem.png

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

../../_images/designpatterns-prototype-solution.png

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

Code 8.60. Solution
"""
* 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


Code 8.61. Solution
"""
* 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


Code 8.62. Solution
"""
* 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:
    ...


Code 8.63. Solution
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?
"""