Prototype Design Pattern
The Prototype Design Pattern is a creational pattern used when the type of objects to be created is determined by a prototypical instance. Instead of creating new instances directly, you clone an existing object. This pattern is useful when the cost of creating a new object is expensive or when you want to avoid the overhead of creating a large number of similar objects.
Key Concepts:
Prototype Interface: Declares a clone() method that is used to clone the object.
Concrete Prototype: Implements the clone() method, allowing the object to be duplicated.
We will use the Prototype pattern to explain Geofencing.
Geofencing is a location-based service used in various applications where virtual boundaries, known as "geofences," are defined around specific geographic areas. When a device enters, exits, or moves within these predefined areas, specific actions can be triggered. Geofencing is widely used across different industries for a variety of purposes.
We will create 10 million location objects without cloning and with cloning. Then we will do a time and memory usage comparison.
Let's set up a TypeScript example where we generate and clone Location objects representing geographic coordinates (latitude and longitude).
Step 1: Define the Location Interface
We'll start by defining a simple GeoLocation (Location) interface to represent a geographic point with latitude and longitude.
interface GeoLocation {
latitude: number;
longitude: number;
clone: () => GeoLocation;
}
Step 2: Create a Prototype Implementation
Let's create a createGeoLocation function that generates a GeoLocation object with a clone method.
const createGeoLocation = (
latitude: number,
longitude: number
): GeoLocation => {
const clone = (): GeoLocation => {
return createGeoLocation(latitude, longitude);
};
return {
latitude,
longitude,
clone,
};
};
Step 3: Generate and Clone Location Objects
We'll write a function that generates 10 million Location objects using both methods (with and without the prototype pattern) and measure the time taken for each.
const OBJECTS_LENGTH = 10000000;
//Random Location from India
const LATITUDE = 20.5937;
const LONGITUDE = 78.9629;
const generateGeoLocationsWithoutPrototype = (): GeoLocation[] => {
const geoLocations: GeoLocation[] = [];
for (let i = 0; i < OBJECTS_LENGTH; i++) {
const geoLocation: GeoLocation = {
latitude: LATITUDE + i * 0.0001,
longitude: LONGITUDE + i * 0.0001,
clone: () => ({
latitude: LATITUDE + i * 0.0001,
longitude: LONGITUDE + i * 0.0001,
clone: () => geoLocation, // This ensures the clone method returns a valid GeoLocation object
}),
};
geoLocations.push(geoLocation);
}
return geoLocations;
};
const generateGeoLocationsWithPrototype = (): GeoLocation[] => {
const geoLocations: GeoLocation[] = [];
const prototype = createGeoLocation(LATITUDE, LONGITUDE);
for (let i = 0; i < OBJECTS_LENGTH; i++) {
const clonedGeoLocation = prototype.clone();
clonedGeoLocation.latitude += i * 0.0001;
clonedGeoLocation.longitude += i * 0.0001;
geoLocations.push(clonedGeoLocation);
}
return geoLocations;
};
//In the Prototype pattern function abovve, we use the clone() method.
//main.ts
const measurePerformance = () => {
console.time("Without Prototype");
const locationsWithoutPrototype = generateGeoLocationsWithoutPrototype();
console.timeEnd("Without Prototype");
console.time("With Prototype");
const locationsWithPrototype = generateGeoLocationsWithPrototype();
console.timeEnd("With Prototype");
return { locationsWithoutPrototype, locationsWithPrototype };
};
measurePerformance();
The function without using clone generates the result in 4.996 seconds (in my setup).
The function with the clone() function is completed within 2.998 seconds - a good 2 seconds earlier.
Without Prototype: The time taken is typically longer since each object is created from scratch.
With Prototype: The time taken is shorter because cloning an object is generally faster than creating a new object from scratch.
This example illustrates the practical benefits of using the Prototype pattern in scenarios where object creation can be a bottleneck.
Key Factors Contributing to Faster Cloning
Memory Allocation Efficiency:
When you clone an object, you're essentially copying the existing object's properties and references. This operation can be faster because the memory for the object's structure is already allocated, and the process doesn't need to initialize each property from scratch.
In contrast, creating a new object from scratch involves allocating memory and initializing each property individually, which can be more time-consuming.
Avoiding Repeated Initialization:
With the Prototype Design Pattern, the base object (prototype) is already set up. Cloning it avoids the overhead of setting up each new object, especially when you have complex initialization logic or multiple properties.
Initializing new objects repeatedly from scratch often involves setting up default values, running constructor code, and performing any side effects, which can add up in terms of time.
Reference Sharing:
In cloning, if your objects contain references to other objects or functions (e.g., methods), those references can be shared between clones. This avoids the need to recreate these references and can save both time and memory.
When you create new objects, every reference needs to be re-established, which adds to the processing time.
Garbage Collection:
- JavaScript engines use garbage collection to manage memory, which can be triggered more frequently when creating many new objects, adding to the overall time. Cloning can be less demanding on the garbage collector since it may produce fewer short-lived objects.
To compare the memory usage of the two approaches—generateGeoLocationsWithoutPrototype and generateGeoLocationsWithPrototype—we can analyze the key differences in how memory is allocated and used in each case.
To measure the actual memory usage in a Node.js environment, you can use the process.memoryUsage() function before and after generating the locations.
const measureMemoryUsage = () => {
console.log(
"Initial Memory Usage:",
process.memoryUsage().heapUsed / 1024 / 1024,
"MB"
);
console.time("Without Prototype");
const locationsWithoutPrototype = generateGeoLocationsWithoutPrototype();
console.timeEnd("Without Prototype");
console.log(
"Memory Usage After Without Prototype:",
process.memoryUsage().heapUsed / 1024 / 1024,
"MB"
);
console.time("With Prototype");
const locationsWithPrototype = generateGeoLocationsWithPrototype();
console.timeEnd("With Prototype");
console.log(
"Memory Usage After With Prototype:",
process.memoryUsage().heapUsed / 1024 / 1024,
"MB"
);
};
measureMemoryUsage();
Observed Results:
After running on my setup, I got these results:
Memory Usage After With Prototype: 2042.2299 MB
Memory Usage After Without Prototype: 2347.4749 MB
Explanation:
- Time Performance:
With Prototype: The time is lower because the clone method is reused from the prototype, reducing the overhead of creating new functions during each iteration. This reuse leads to faster execution.
Without Prototype: Creating a new clone function for each object adds overhead, leading to a higher execution time.
Memory Usage:
With Prototype: Despite sharing the clone method, the memory usage is higher. This could be due to the nature of how JavaScript engines optimize memory.
With Prototype: Despite sharing the clone method, the memory usage is higher. This could be due to the nature of how JavaScript engines optimize memory.
Key Takeaways:
Prototype-based Object Creation: While it can improve performance in terms of speed, it might not always lead to lower memory usage due to the additional complexity involved in managing prototype chains.
Object Creation Without Prototype: Despite being slower, this approach might result in lower memory usage due to its simplicity and the absence of prototype chain management.
Conclusion
The key reason for the speed difference is that cloning is a more efficient operation compared to creating a new object from scratch. Cloning reuses the existing object's structure, reduces the need for repetitive initialization, and minimizes the memory management overhead, all of which contribute to the faster performance observed in the timing tests.
If your priority is performance (speed), the prototype approach is faster.
If your priority is memory efficiency, the non-prototype approach appears to use less memory in this scenario.
The GITHUB LINK for the code sample containing differences in memory usage and time taken. Note that, your setup is bound to produce different times and memory consumption values. Handle with care!
Subscribe to my newsletter
Read articles from Ganesh Rama Hegde directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Ganesh Rama Hegde
Ganesh Rama Hegde
Passionate Developer | Code Whisperer | Innovator Hi there! I'm a senior software developer with a love for all things tech and a knack for turning complex problems into elegant, scalable solutions. Whether I'm diving deep into TypeScript, crafting seamless user experiences in React Native, or exploring the latest in cloud computing, I thrive on the thrill of bringing ideas to life through code. I’m all about creating clean, maintainable, and efficient code, with a strong focus on best practices like the SOLID principles. My work isn’t just about writing code; it’s about crafting digital experiences that resonate with users and drive impact. Beyond the code editor, I’m an advocate for continuous learning, always exploring new tools and technologies to stay ahead in this ever-evolving field. When I'm not coding, you'll find me blogging about my latest discoveries, experimenting with side projects, or contributing to open-source communities. Let's connect, share knowledge, and build something amazing together!