The design patterns differ in their usage and implementation, and each one of them provide a solution for many common problems with more efficient and flexible methods, one of the famous patterns is Prototype and it belongs to the creational patterns group. The prototype as our reference book “Design Patterns: Elements of Reusable Object-Oriented Software” defines it aims to “Ensure a class only has one instance, and provide a global point of access to it.”, to get deep understanding of this definition we can take an example of how the prototype concept is applied.
Lets say we have a report editor that provides many reports items objects and the client can choose one of them to generate the needed report, each report consists of title, texts and images so in the normal case we can define :
1- Report item prototype interface
2- Text , title and image items classes
Imagine that if we need to add new text and image items but with different styles , too much of subclasses right !!!, in addition to that if the report editor (client) needs to use an item multiple times in a report then there are two options; creating a new item object each time which will be a waste of time and resources, or copying each time a fixed item object by going through all the item’s fields and copy their values. Note here there is an extra dependency because the client must know the item class to create a right copy.
These are the problems. So what is the solutions ?! , we can change the structure by:
1- Adjusting text and images constructors to accept different styles arguments
2- Adding clone method to each item that returns a copy from it
3- Adding an initialize method to each item class
Therefore now we can create the main report items and whenever we needed an item with different style we will copy the main one and initialize it with the new style, also we can duplicate the item in reports whenever its required by calling its clone method. The original items objects that are used for cloning are called prototypes, we use these prototypes to create different copies by calling their clone methods. Additionally we can create a prototype registry to track the available prototypes then the client just needs to use this registry operations to add new prototypes under a key and retrieve them, this registry is called a prototype manager.
Lets say our new structure in the following diagram
The new structure describes how the prototype pattern works, it consists of prototype, concrete prototypes and prototype manager
Prototype (Report Item Prototype) : declares an interface for cloning itself
Concrete prototype (Text, Image): implements an operation for cloning itself.
Prototype Manager: an associative store that returns the prototype
matching a given key (the prototype manager is an optional component).
By using the prototype pattern we reduce the subclasses, have the ability to create unlimited number of objects copies with different styles, less resources and without needing any dependency in the client code, but on the other hand cloning objects can be difficult when their internals include objects that don’t support copying or having circular references.
Implementation:
Before going to the code I need to list some notes to consider them through the implementation:
– Each prototype class must explicitly implement the clone operation.
– The clone method returns a new prototypical version of the constructor
– If the client needs to initialize some objects with different values, you can define an Initialize operation that takes initialization parameters as arguments and sets the clone’s internal state accordingly.
the following code illustrates the report editor example using Kotlin language:
Note :The Prototype pattern is available in Kotlin with a Data class which has a copy method that gives the chance to clone the whole objects and optionally change some of the new object’s properties. But for more clarification the below example was written without using data class.
interface ReportItemPrototype { fun clone(): ReportItemPrototype }
class TextItem : ReportItemPrototype { var text: String? = null var color: String? = null constructor() {} constructor(source: TextItem) { this.text = source.text this.color = source.color } fun initializeText(newText: String, newColor: String) { this.text = newText this.color = newColor } override fun clone(): TextItem { return TextItem(this) } }
class ImageItem : ReportItemPrototype { var url: String? = null var width: Int? = null constructor() {} constructor(source: ImageItem) { this.url = source.url this.width = source.width } override fun clone(): ImageItem { return ImageItem(this) } }
class Report { fun printReport(reportItems: MutableMap<String, ReportItemPrototype>) { reportItems.forEach { if (it.value is TextItem) { console.log( "${it.key} TextItem" ) } else if (it.value is ImageItem) { console.log( "${it.key} ImageITem" ) } } } }
fun main() { val report = Report() val reportItems: MutableMap<String, ReportItemPrototype> = mutableMapOf() val text = TextItem() text.text = "Normal text" text.color = "Black" reportItems["Normal text"] = text val image = ImageItem() image.url = "/image/url" image.width = 100 reportItems["Normal image"] = image report.printReport(reportItems) /////////////////////////////////////////////// val redText = text.clone() redText.initializeText("Red text", "Red") reportItems["Red text"] = redText val secondImage = image.clone() reportItems["Second image"] = secondImage console.log("\n \n Updated report") report.printReport(reportItems) }
Output:
///////
Normal text TextItem
Normal image ImageITem
Updated report
Normal text TextItem
Normal image ImageITem
Red text TextItem
Second image ImageITem
///////
With prototype manager(or registry or cache):
class ReportItemsCache { private val reportItems: MutableMap<String, ReportItemPrototype> = mutableMapOf() init { val text = TextItem() text.text = "Normal text" text.color = "Black" reportItems["item 1"] = text val image = ImageItem() image.url = "/image/url" image.width = 100 reportItems["item 2"] = image } fun addItem(key: String, newItem: ReportItemPrototype) { reportItems[key] = newItem } fun getItem(key: String): ReportItemPrototype { return reportItems[key]?.clone() ?: throw Exception("no item with this key") } }
class Report { val reportItemsCache = ReportItemsCache() fun printReport() { val firstItem = reportItemsCache.getItem("item 1") val secondItem = reportItemsCache.getItem("item 2") if (firstItem is TextItem) { console.log( "The first item is text" ) } else { console.log("Something wrong !") } if (secondItem is ImageItem) { console.log( "The second item is image" ) } else { console.log("Something wrong !") } } }
fun main() { val report = Report() report.printReport() }
Output:
//////
The first item is text
The second item is image
/////
Finally you can always refer to our reference book “Design Patterns: Elements of Reusable Object-Oriented Software” by the Gang of Four to get more details.