Let's suppose you are a developer working on an eCommerce, and you're given the responsibility for this feature:
In the
Cart
screen, user should be able to add more of an existing product there, and change its color.
One intuitive solution in this context could be instantiating a new product object, and set all attributes to be equal the original product. But this seems too verbose, and you also have to know all the internals of the product code, which seems like breaking the encapsulation.
Prototype to the rescue!
From refactoring.guru:
Prototype is a creational design pattern that lets you copy existing objects without making your code dependent on their classes.
That means you don't need to use the new
operator and worry about how to setup a perfect copy instance. Just use the built-in clone
operator and PHP will deal with everything.
Sample application
You check the final code on Github:
fabiothiroki / php-design-patterns
A collection of design patterns written in PHP
The Cart
class doesn't make part of the design pattern but it's here just to demonstrate how it fits in a real world application:
final class Cart
{
/**
* @var ProductPrototype[]
*/
private array $products;
public function addProduct(ProductPrototype $product): void
{
$this->products[] = $product;
}
/**
* @return ProductPrototype[]
*/
public function getProducts(): array
{
return $this->products;
}
}
As you can see, we are able to add a new product and see which products are already there.
abstract class ProductPrototype
{
protected int $id;
protected string $name;
protected string $color;
public function getId(): int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function getColor(): string
{
return $this->color;
}
public function setColor(string $color): void
{
$this->color = $color;
}
}
PHP already implements the Prototype
design pattern natively because every object already has a built-in clone
operator support. In this example, I created the ProductPrototype
class just to illustrate a real use case: for Cart
class is doesn't matter which specific kind of product it has, as long as it has the attributes id
, name
and color
.
Just to simplify this example, I will add only two types of product: Smartphone
and Laptop
:
final class Smartphone extends ProductPrototype
{
public function __construct()
{
$this->id = 1;
$this->name = 'Smartphone';
$this->color = 'Default color';
}
}
final class Laptop extends ProductPrototype
{
public function __construct()
{
$this->id = 2;
$this->name = 'Smartphone';
$this->color = 'Default color';
}
}
Explaining Prototype with Unit test
What does the clone
operator really do? It creates a copy of an instance with the same attributes. So in our example the cloned product will contain the same id
, name
and color
as the original.
public function testSmartphoneClone(): void
{
$smartphone = new Smartphone();
$clonedSmartphone = clone $smartphone;
self::assertEquals($clonedSmartphone->getId(), $smartphone->getId());
self::assertEquals($clonedSmartphone->getName(), $smartphone->getName());
self::assertEquals($clonedSmartphone->getColor(), $smartphone->getColor());
self::assertEquals($clonedSmartphone, $smartphone);
self::assertNotSame($clonedSmartphone, $smartphone);
}
As you can see, if we were to manually clone an object, we would have to know all the properties to be copied, and how to set them.
Please notice this test shouldn't be written in a real application because we can assume the clone
method already works since its provided by PHP itself.
Back to our use case
We finally can enable the Cart
to increase the quantity of an existing product and change its color. Cart
will be even happier because it doesn't matter if it's an Laptop
or Smartphone
.
Just make use of clone
and add the new cloned product:
public function testCartCanAddClonedProducts(): void
{
$laptop = new Laptop();
$cart = new Cart();
$cart->addProduct($laptop);
$clonedLaptop = clone $laptop;
$clonedLaptop->setColor('White');
$cart->addProduct($clonedLaptop);
self::assertCount(2, $cart->getProducts());
self::assertEquals($cart->getProducts()[1]->getId(), $cart->getProducts()[0]->getId());
self::assertEquals($cart->getProducts()[1]->getName(), $cart->getProducts()[0]->getName());
self::assertEquals('White', $cart->getProducts()[1]->getColor());
self::assertEquals('Default color', $cart->getProducts()[0]->getColor());
}
Thanks for reading and hope you could get a better picture of this design pattern and will remember to use it whenever the opportunity appears!