Prototype design pattern

The last pattern we will see in this chapter is the Prototype pattern. Like all creational patterns, this too comes in handy when creating objects, and it is very common to see the Prototype pattern surrounded by more patterns.

While with the Builder pattern, we are dealing with repetitive building algorithms and with the factories we are simplifying the creation of many types of objects; with the Prototype pattern, we will use an already created instance of some type to clone it and complete it with the particular needs of each context. Let's see it in detail.

Description

The aim of the Prototype pattern is to have an object or a set of objects that is already created at compilation time, but which you can clone as many times as you want at runtime. This is useful, for example, as a default template for a user who has just registered with your webpage or a default pricing plan in some service. The key difference between this and a Builder pattern is that objects are cloned for the user instead of building them at runtime. You can also build a cache-like solution, storing information using a prototype.

Objective

The main objective for the Prototype design pattern is to avoid repetitive object creation. Imagine a default object composed of dozens of fields and embedded types. We don't want to write everything needed by this type every time that we use the object, especially if we can mess it up by creating instances with different foundations:

  • Maintain a set of objects that will be cloned to create new instances
  • Provide a default value of some type to start working on top of it
  • Free CPU of complex object initialization to take more memory resources

Example

We will build a small component of an imaginary customized shirts shop that will have a few shirts with their default colors and prices. Each shirt will also have a Stock Keeping Unit (SKU), a system to identify items stored at a specific location) that will need an update.

Acceptance criteria

To achieve what is described in the example, we will use a prototype of shirts. Each time we need a new shirt we will take this prototype, clone it and work with it. In particular, those are the acceptance criteria for using the Prototype pattern design method in this example:

  • To have a shirt-cloner object and interface to ask for different types of shirts (white, black, and blue at 15.00, 16.00, and 17.00 dollars respectively)
  • When you ask for a white shirt, a clone of the white shirt must be made, and the new instance must be different from the original one
  • The SKU of the created object shouldn't affect new object creation
  • An info method must give me all the information available on the instance fields, including the updated SKU

Unit test

First, we will need a ShirtCloner interface and an object that implements it. Also, we need a package-level function called GetShirtsCloner to retrieve a new instance of the cloner:

type ShirtCloner interface { 
    GetClone(s int) (ItemInfoGetter, error) 
} 
 
const ( 
    White = 1 
    Black = 2 
    Blue  = 3 
) 
 
func GetShirtsCloner() ShirtCloner { 
    return nil 
} 
 
type ShirtsCache struct {} 
func (s *ShirtsCache)GetClone(s int) (ItemInfoGetter, error) { 
    return nil, errors.New("Not implemented yet") 
} 

Now we need an object struct to clone, which implements an interface to retrieve the information of its fields. We will call the object Shirt and the ItemInfoGetter interface:

type ItemInfoGetter interface { 
    GetInfo() string 
} 
 
type ShirtColor byte 
 
type Shirt struct { 
    Price float32 
    SKU   string 
    Color ShirtColor 
} 
func (s *Shirt) GetInfo()string { 
    return "" 
} 
 
func GetShirtsCloner() ShirtCloner { 
    return nil 
} 
 
var whitePrototype *Shirt = &Shirt{ 
    Price: 15.00, 
    SKU:   "empty", 
    Color: White, 
} 
 
func (i *Shirt) GetPrice() float32 { 
    return i.Price 
} 

Tip

Have you realized that the type called ShirtColor that we defined is just a byte type? Maybe you are wondering why we haven't simply used the byte type. We could, but this way we created an easily readable struct, which we can upgrade with some methods in the future if required. For example, we could write a String() method that returns the color in the string format (White for type 1, Black for type 2, and Blue for type 3).

With this code, we can already write our first tests:

func TestClone(t *testing.T) { 
    shirtCache := GetShirtsCloner() 
    if shirtCache == nil { 
        t.Fatal("Received cache was nil") 
    } 
 
    item1, err := shirtCache.GetClone(White) 
    if err != nil { 
        t.Error(err) 
} 

//more code continues here... 

We will cover the first case of our scenario, where we need a cloner object that we can use to ask for different shirt colors.

For the second case, we will take the original object (which we can access because we are in the scope of the package), and we will compare it with our shirt1 instance.

if item1 == whitePrototype { 
    t.Error("item1 cannot be equal to the white prototype"); 
} 

Now, for the third case. First, we will type assert item1 to a shirt so that we can set an SKU. We will create a second shirt, also white, and we will type assert it too to check that the SKUs are different:

shirt1, ok := item1.(*Shirt) 
if !ok { 
    t.Fatal("Type assertion for shirt1 couldn't be done successfully") 
} 
shirt1.SKU = "abbcc" 
 
item2, err := shirtCache.GetClone(White) 
if err != nil { 
    t.Fatal(err) 
} 
 
shirt2, ok := item2.(*Shirt) 
if !ok { 
    t.Fatal("Type assertion for shirt1 couldn't be done successfully") 
} 
 
if shirt1.SKU == shirt2.SKU { 
    t.Error("SKU's of shirt1 and shirt2 must be different") 
} 
 
if shirt1 == shirt2 { 
    t.Error("Shirt 1 cannot be equal to Shirt 2") 
} 

Finally, for the fourth case, we log the info of the first and second shirts:

t.Logf("LOG: %s", shirt1.GetInfo()) 
t.Logf("LOG: %s", shirt2.GetInfo()) 

We will be printing the memory positions of both shirts, so we make this assertion at a more physical level:

t.Logf("LOG: The memory positions of the shirts are different %p != %p 

", &shirt1, &shirt2) 
Finally, we run the tests so we can check that it fails:
go test -run=TestClone . 
--- FAIL: TestClone (0.00s) 
prototype_test.go:10: Not implemented yet 
FAIL 
FAIL

We have to stop there so that the tests don't panic if we try to use a nil object that is returned by the GetShirtsCloner function.

Implementation

We will start with the GetClone method. This method should return an item of the specified type and we have three type: white, black and blue:

var whitePrototype *Shirt = &Shirt{ 
    Price: 15.00, 
    SKU:   "empty", 
    Color: White, 
} 
 
var blackPrototype *Shirt = &Shirt{ 
    Price: 16.00, 
    SKU:   "empty", 
    Color: Black, 
} 
 
var bluePrototype *Shirt = &Shirt{ 
    Price: 17.00, 
    SKU:   "empty", 
    Color: Blue, 
} 

So now that we have the three prototypes to work over we can implement GetClone(s int) method:

type ShirtsCache struct {} 
func (s *ShirtsCache)GetClone(s int) (ItemInfoGetter, error) { 
    switch m { 
        case White: 
            newItem := *whitePrototype 
            return &newItem, nil 
        case Black: 
            newItem := *blackPrototype 
            return &newItem, nil 
        case Blue: 
            newItem := *bluePrototype 
            return &newItem, nil 
        default: 
            return nil, errors.New("Shirt model not recognized") 
    } 
} 

The Shirt structure also needs a GetInfo implementation to print the contents of the instances.

type ShirtColor byte 
 
type Shirt struct { 
    Price float32 
    SKU   string 
    Color ShirtColor 
} 

func (s *Shirt) GetInfo() string { 
    return fmt.Sprintf("Shirt with SKU '%s' and Color id %d that costs %f
", s.SKU, s.Color, s.Price) 
} 

Finally, let's run the tests to see that everything is now working:

go test -run=TestClone -v . 
=== RUN   TestClone 
--- PASS: TestClone (0.00s) 
prototype_test.go:41: LOG: Shirt with SKU 'abbcc' and Color id 1 that costs 15.000000 
prototype_test.go:42: LOG: Shirt with SKU 'empty' and Color id 1 that costs 15.000000 
prototype_test.go:44: LOG: The memory positions of the shirts are different 0xc42002c038 != 0xc42002c040  
 
PASS 
ok

In the log, (remember to set the -v flag when running the tests) you can check that shirt1 and shirt2 have different SKUs. Also, we can see the memory positions of both objects. Take into account that the positions shown on your computer will probably be different.

What we learned about the Prototype design pattern

The Prototype pattern is a powerful tool to build caches and default objects. You have probably realized too that some patterns can overlap a bit, but they have small differences that make them more appropriate in some cases and not so much in others.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.180.193