diff --git a/README.md b/README.md index a37d1f4..5f59f22 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,4 @@ - [Factory Method](creational/factory-method/README.md) - [Abstract Factory](creational/abstract-factory/README.md) - [Builder](creational/builder/README.md) + - [Prototype](creational/prototype/README.md) diff --git a/creational/README.md b/creational/README.md index d719ad8..3a3b7e0 100644 --- a/creational/README.md +++ b/creational/README.md @@ -3,3 +3,4 @@ - [Factory Method](factory-method/README.md) - [Abstract Factory](abstract-factory/README.md) - [Builder](builder/README.md) +- [Prototype](prototype/README.md) diff --git a/creational/prototype/README.md b/creational/prototype/README.md new file mode 100644 index 0000000..42842c7 --- /dev/null +++ b/creational/prototype/README.md @@ -0,0 +1,35 @@ +# Prototype + +Prototype is a creational design pattern that allows cloning objects, even complex ones, without coupling to their specific classes. + +## Summary + +Prototype is a creational design pattern that lets you copy existing objects without making your code dependent on their classes. + +## Problem + +Say you have an object, and you want to create an exact copy of it. How would you do it? First, you have to create a new object of the same class. Then you have to go through all the fields of the original object and copy their values over to the new object. + +Nice! But there’s a catch. Not all objects can be copied that way because some of the object’s fields may be private and not visible from outside of the object itself. + +There’s one more problem with the direct approach. Since you have to know the object’s class to create a duplicate, your code becomes dependent on that class. If the extra dependency doesn’t scare you, there’s another catch. Sometimes you only know the interface that the object follows, but not its concrete class, when, for example, a parameter in a method accepts any objects that follow some interface. + +## Solution + +The Prototype pattern delegates the cloning process to the actual objects that are being cloned. The pattern declares a common interface for all objects that support cloning. This interface lets you clone an object without coupling your code to the class of that object. Usually, such an interface contains just a single `clone` method. + +## How to Implement + +1. Create the prototype interface and declare the clone method in it. Or just add the method to all classes of an existing class hierarchy, if you have one. + +1. A prototype class must define the alternative constructor that accepts an object of that class as an argument. The constructor must copy the values of all fields defined in the class from the passed object into the newly created instance. If you’re changing a subclass, you must call the parent constructor to let the superclass handle the cloning of its private fields. + +If your programming language doesn’t support method overloading, you may define a special method for copying the object data. The constructor is a more convenient place to do this because it delivers the resulting object right after you call the new operator. + +1. The cloning method usually consists of just one line: running a new operator with the prototypical version of the constructor. Note, that every class must explicitly override the cloning method and use its own class name along with the new operator. Otherwise, the cloning method may produce an object of a parent class. + +1. Optionally, create a centralized prototype registry to store a catalog of frequently used prototypes. + +You can implement the registry as a new factory class or put it in the base prototype class with a static method for fetching the prototype. This method should search for a prototype based on search criteria that the client code passes to the method. The criteria might either be a simple string tag or a complex set of search parameters. After the appropriate prototype is found, the registry should clone it and return the copy to the client. + +Finally, replace the direct calls to the subclasses’ constructors with calls to the factory method of the prototype registry. diff --git a/creational/prototype/__init__.py b/creational/prototype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/creational/prototype/components.py b/creational/prototype/components.py new file mode 100644 index 0000000..d5f6d09 --- /dev/null +++ b/creational/prototype/components.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import copy +from dataclasses import dataclass +from typing import Any, List + + +class SelfReferencingEntity: + def __init__(self): + self.parent = None + + def set_parent(self, parent): + self.parent = parent + + +@dataclass +class SomeComponent: + """ + Python provides its own interface of Prototype via `copy.copy` and + `copy.deepcopy` functions. And any class that wants to implement custom + implementations have to override `__copy__` and `__deepcopy__` member + functions. + """ + some_int: int + some_list_of_objects: List + some_circular_ref: Any + + def __copy__(self) -> SomeComponent: + """ + Create a shallow copy. This method will be called whenever someone calls + `copy.copy` with this object and the returned value is returned as the + new shallow copy. + """ + + # First, let's create copies of the nested objects. + some_list_of_objects = copy.copy(self.some_list_of_objects) + some_circular_ref = copy.copy(self.some_circular_ref) + + # Then, let's clone the object itself, using the prepared clones of the + # nested objects. + new = self.__class__( + self.some_int, some_list_of_objects, some_circular_ref) + new.__dict__.update(self.__dict__) + + return new + + def __deepcopy__(self, memo={}) -> SomeComponent: + """ + Create a deep copy. This method will be called whenever someone calls + `copy.deepcopy` with this object and the returned value is returned as + the new deep copy. + + What is the use of the argument `memo`? Memo is the dictionary that is + used by the `deepcopy` library to prevent infinite recursive copies in + instances of circular references. Pass it to all the `deepcopy` calls + you make in the `__deepcopy__` implementation to prevent infinite + recursions. + """ + + # First, let's create copies of the nested objects. + some_list_of_objects = copy.deepcopy(self.some_list_of_objects, memo) + some_circular_ref = copy.deepcopy(self.some_circular_ref, memo) + + # Then, let's clone the object itself, using the prepared clones of the + # nested objects. + new = self.__class__( + self.some_int, some_list_of_objects, some_circular_ref) + + new.__dict__ = copy.deepcopy(self.__dict__, memo) + + return new diff --git a/creational/prototype/main.py b/creational/prototype/main.py new file mode 100644 index 0000000..5fb2226 --- /dev/null +++ b/creational/prototype/main.py @@ -0,0 +1,84 @@ +import copy + +from components import SelfReferencingEntity, SomeComponent + +list_of_objects = [1, {1, 2, 3}, [1, 2, 3]] +circular_ref = SelfReferencingEntity() +component = SomeComponent(23, list_of_objects, circular_ref) +circular_ref.set_parent(component) + +shallow_copied_component = copy.copy(component) + +# Let's change the list in shallow_copied_component and see if it changes in component. +shallow_copied_component.some_list_of_objects.append("another object") +if component.some_list_of_objects[-1] == "another object": + print( + """Adding elements to `shallow_copied_component`'s some_list_of_objects + adds it to `component`'s some_list_of_objects.""" + ) +else: + print( + """Adding elements to `shallow_copied_component`'s some_list_of_objects + doesn't add it to `component`'s some_list_of_objects.""" + ) + +# Let's change the set in the list of objects. +component.some_list_of_objects[1].add(4) +if 4 in shallow_copied_component.some_list_of_objects[1]: + print( + "Changing objects in the `component`'s some_list_of_objects " + "changes that object in `shallow_copied_component`'s " + "some_list_of_objects." + ) +else: + print( + "Changing objects in the `component`'s some_list_of_objects " + "doesn't change that object in `shallow_copied_component`'s " + "some_list_of_objects." + ) + +deep_copied_component = copy.deepcopy(component) + +# Let's change the list in deep_copied_component and see if it changes in +# component. +deep_copied_component.some_list_of_objects.append("one more object") +if component.some_list_of_objects[-1] == "one more object": + print( + "Adding elements to `deep_copied_component`'s " + "some_list_of_objects adds it to `component`'s " + "some_list_of_objects." + ) +else: + print( + "Adding elements to `deep_copied_component`'s " + "some_list_of_objects doesn't add it to `component`'s " + "some_list_of_objects." + ) + +# Let's change the set in the list of objects. +component.some_list_of_objects[1].add(10) +if 10 in deep_copied_component.some_list_of_objects[1]: + print( + "Changing objects in the `component`'s some_list_of_objects " + "changes that object in `deep_copied_component`'s " + "some_list_of_objects." + ) +else: + print( + "Changing objects in the `component`'s some_list_of_objects " + "doesn't change that object in `deep_copied_component`'s " + "some_list_of_objects." + ) + +print( + f"id(deep_copied_component.some_circular_ref.parent): " + f"{id(deep_copied_component.some_circular_ref.parent)}" +) +print( + f"id(deep_copied_component.some_circular_ref.parent.some_circular_ref.parent): " + f"{id(deep_copied_component.some_circular_ref.parent.some_circular_ref.parent)}" +) +print( + "^^ This shows that deepcopied objects contain same reference, they " + "are not cloned repeatedly." +)