Preface
The Prototype Pattern
creates duplicate objects while keeping performance
in mind. It’s a part of the creational patterns and provides one of the best
ways to create an object.
In the mainstream languages (like C# and JAVA), it requires implementing a prototype interface which tells to create a clone of the current object. It is used when creation of object directly is costly.
For instance, an object is to be created after a costly database operation. We can cache the object, returns its clone on next request and update the database as and when needed thus reducing database calls.
Purpose
- Specify the kind of objects to create using a prototypical instance, and create new objects by copying this prototype.
Design Pattern Diagram
Prototype
declares an interface for cloning itselfConcretePrototype
implements an operation for cloning itselfClient
creates a new object by asking a prototype to clone itself
Implementation
In Golang, the pattern is applicable only in situation that we want to customize how the object is cloned. We will explore two examples regarding both situations.
Lets build a system that generates a different configuration files depending on
our needs. In first place, we have a struct Config
that looks like:
package configurer
// Config provides a configuration of microservice
type Config struct {
workDir string
user string
}
// NewConfig create a new config
func NewConfig(user string, workDir string) Config {
return Config{
user: user,
workDir: workDir,
}
}
// WithWorkDir creates a copy of Config with the provided working directory
func (c Config) WithWorkDir(dir string) Config {
c.workDir = dir
return c
}
// WithUser creates a copy of Config with the provided user
func (c Config) WithUser(user string) Config {
c.user = user
return c
}
We want to be able to mutate the object without affecting its initial instance. The goal is to be able to generate different configuration files without loosing the flexibility of customizing them without mutation of the initial default configuration.
As you can see the functions WithWorkDir
, WithUser
are declared for the
struct Config
(not for *Config
). At the time, when they are called the
object is copied by the Golang
runtime. This allows us to modify it
without affecting the original object.
Lets see it’s usage in action:
config := configurer.NewConfig("guest", "/home/guest")
rootConfig := config.WithUser("root").WithWorkDir("/root")
fmt.Println("Guest Config", config)
fmt.Println("Root Config", rootConfig)
Now lets explore the classic implementation of The Prototype Design Pattern
.
We will assume that we are developing again document object model for a custom
document format. The core object is an Element
structure which has parent and
children.
// Element represents an element in document object model
type Element struct {
text string
parent Node
children []Node
}
// NewElement makes a new element
func NewElement(text string) *Element {
return &Element{
text: text,
parent: nil,
children: make([]Node, 0),
}
}
// Parent returns the element parent
func (e *Element) Parent() Node {
return e.parent
}
// SetParent sets the element parent
func (e *Element) SetParent(node Node) {
e.parent = node
}
// Children returns the element children elements
func (e *Element) Children() []Node {
return e.children
}
// AddChild adds a child element
func (e *Element) AddChild(child Node) {
copy := child.Clone()
copy.SetParent(e)
e.children = append(e.children, copy)
}
// Clone makes a copy of particular element. Note that the element becomes a
// root of new orphan tree
func (e *Element) Clone() Node {
copy := &Element{
text: e.text,
parent: nil,
children: make([]Node, 0),
}
for _, child := range e.children {
copy.AddChild(child)
}
return copy
}
// String returns string representation of element
func (e *Element) String() string {
buffer := bytes.NewBufferString(e.text)
for _, c := range e.Children() {
text := c.String()
fmt.Fprintf(buffer, "\n %s", text)
}
return buffer.String()
}
The contract that exposes the Clone
funcion is the Node
interface:
// Node a document object model node
type Node interface {
// Strings returns nodes text representation
String() string
// Parent returns the node parent
Parent() Node
// SetParent sets the node parent
SetParent(node Node)
// Children returns the node children nodes
Children() []Node
// AddChild adds a child node
AddChild(child Node)
// Clone clones a node
Clone() Node
}
We want to extract a particular subtree of concrete element hierary. We want to use it as independent document object model. In order to do that, we should use the clone function:
directorNode := dom.NewElement("Director of Engineering")
engManagerNode := dom.NewElement("Engineering Manager")
engManagerNode.AddChild(dom.NewElement("Lead Software Engineer"))
directorNode.AddChild(engManagerNode)
directorNode.AddChild(engManagerNode)
officeManagerNode := dom.NewElement("Office Manager")
directorNode.AddChild(officeManagerNode)
fmt.Println("")
fmt.Println("# Company Hierarchy")
fmt.Print(directorNode)
fmt.Println("")
fmt.Println("# Team Hiearachy")
fmt.Print(engManagerNode.Clone())
The sample above creates a tree from the subtree pointed by engManagerNode
variable.
You can get the full source code from github.
Verdict
One of the disadvantages of this pattern is that the process of copying an object can be complicated. In addition, structs that have circular references to other classes are difficult to clone. Its overuse could affect performance, as the prototype object itself would need to be instantiated if you use a registry of prototypes.
In the context of Golang
, I don’t see any particular reason to adopt it.
Golang
already provides builtin mechanism for cloning objects.