Home > Web Front-end > JS Tutorial > Mastering the Prototype Design Pattern: A Comprehensive Guide

Mastering the Prototype Design Pattern: A Comprehensive Guide

Barbara Streisand
Release: 2024-11-17 21:41:02
Original
1043 people have browsed it

Have you ever imported an object from a library and tried to clone it, only to fail because cloning it requires extensive knowledge of the library’s internals?

Or perhaps, after working on a project for a long time, you took a break to refactor your code and noticed that you’re recloning many complex objects in various parts of your codebase?

Well, the prototype design pattern has got you covered!

In this article, we will explore the prototype design pattern while building a fully functional journaling templates Node.js CLI application.

Without further ado, let’s dive into it!

Overview

Prototype is a creational design pattern , which is a category of design patterns that deals with the different problems that come with the native way of creating objects with the new keyword or operator.

Problem

The factory design pattern solves the following creational problems:

  1. How can you copy an existing object in your application without depending on its concret classes?

  2. Some complex objects are hard to clone, because they either have a lot of fields which need a particular busines logic that is either not know by you or or has a lot of private fields which are not accessible from outside of the objects.

Let's take as example the socket object imported from the socket.io library , imaging having to clone that yourself?

You will have to go through it's code inside the library, understand how sockets work, the objects has even some circular dependencies which you have to deal with your self in order to clone it.

In addition to that, Your code will depend on the socket class or interface and the corresponding business logic to create it, which violates the solid dependency inversion principle and makes your code less robust for changes.

Solution

The prototype design pattern solves these problems, by delegating the responsiblitiy of copying the object into the object itself, by declaring a clone method in every object's class which is meant to be clonable.

class Socket {
  // code........

  clone(): Socket {
    // business logic to instantiate the socket.
    return new Socket(/*...Params*/)
  }
}


const socket1 = new Socket()
const socket2 = socket1.clone()

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

Structure

To implement the prototype design pattern you can either directly include the clone method inside the clonnable object.

Or create a common interface Prototype which can be implemented by all of the clonnable objects. Mastering the Prototype Design Pattern: A Comprehensive Guide

One benifit of having a common interface is the ability to register all the prototypes, in a common registery service class, which will be responsible on cahing the frequently used prototypes and return them to the user. Instead of having to clone the objects everytime the clone method gets called.

That can be really handy especially when cloning complex objects.

Mastering the Prototype Design Pattern: A Comprehensive Guide

Practical Scenario

In this section, we're going to demo this design pattern by building a mini journaling templates Nodejs CLI application.

As we saw earlier the prototype design pattern delegates the responsiblity of cloning the object into the object itself.

But have you wondered why it's even called prototype?I mean what has that to do with cloning?

We will be answering that through this practical example, keep reading and stay tuned.
You can find the final code in this repository. Just clone it and run the following commands.

Creating our Prototype: Journaling Template Class

First let's create a JournalTemplate which has the following attributes:

  1. name : We will need it to identity the template.
  2. sections : A section is a portion of the journaling template which is reserved to a specific theme or topic such as: Gratitude, Challenges, Tomorrow's Goals....

Each section is consist of the following attributes:

  • title The topic or theme of the section: Gratitude, Challenges, Tomorrow's Goals...
  • prompt The message which will be displayed to the user when he is about to write the section journaling text.

JournalTemplate.ts

class Socket {
  // code........

  clone(): Socket {
    // business logic to instantiate the socket.
    return new Socket(/*...Params*/)
  }
}


const socket1 = new Socket()
const socket2 = socket1.clone()

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

The JournalTemplate class has many utility methods for setting its diffirent attributes.

The display method will be used later to display a colored well formated output to the terminal.

the chalk package is used to color some pieced for the outputed terminal text.

Our JournalTemplate objects are meant to be used as the name implies as templates or prototypes for creating other templates or journaling file entries.

That's why we've added the clone method to the JournalTemplate class.

We've added it to give the responsibility of handling the cloning business logic to the JournalTemplate object itself rather than the consuming code.

Declaring Journaling Template Registery

Now let's create our TemplateRegistry class, which will be responsible on storing the JournalTemplate class prototype instances. While providing methods for manipulating those instances.

TemplateRegistry.ts

class Socket {
  // code........

  clone(): Socket {
    // business logic to instantiate the socket.
    return new Socket(/*...Params*/)
  }
}


const socket1 = new Socket()
const socket2 = socket1.clone()

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

The registery stores those classes in a Map object, for fast retreival by name, and exposes many utility methods for adding or retireiving templates instances.

Instantiating The Journaling Template Registery

Now, Let's instantiate the template registery and then seed some initial templates.

registry.ts

import chalk from "chalk"

import { TemplateSection } from "./types"

export interface TemplateSection {
  title: string
  prompt: string
}

export class JournalTemplate {
  constructor(
    public name: string,
    public sections: TemplateSection[]
  ) {}
  clone(): JournalTemplate {
    return new JournalTemplate(
      this.name,
      this.sections.map((s) => ({ ...s }))
    )
  }

  display(): void {
    console.log(chalk.cyan(`\nTemplate: ${this.name}`))
    this.sections.forEach((section, index) => {
      console.log(chalk.yellow(`${index + 1}. ${section.title}`))
      console.log(chalk.gray(` Prompt: ${section.prompt}`))
    })
  }

  addSection(section: TemplateSection): void {
    this.sections.push(section)
  }

  removeSection(index: number): void {
    if (index >= 0 && index < this.sections.length) {
      this.sections.splice(index, 1)
    } else {
      throw new Error("Invalid section index")
    }
  }

  editSection(index: number, newSection: TemplateSection): void {
    if (index >= 0 && index < this.sections.length) {
      this.sections[index] = newSection
    } else {
      throw new Error("Invalid section index")
    }
  }

  getSectionCount(): number {
    return this.sections.length
  }

  getSection(index: number): TemplateSection | undefined {
    return this.sections[index]
  }

  setName(newName: string): void {
    this.name = newName
  }
}

Copy after login
Copy after login

Defining the templates actions methods

In this section, we will define a bunch of functions which will be used in our application menu, to execute various actions like:

  1. Prompt the user to enter the name of the template, then recurrsively prompt it again to create as many sections as he want.
  2. View All the existing or created templates.
  3. Use a template to create a journaling file entry.
  4. Create a new template, from an existing template: The user will be asked to select an existing template then he will have the ability to either use it directly or override its name and sections.

The newly created templates can be used to create new journaling entries (1).

Create a Template :

TemplateActions.ts > createTemplate

import { JournalTemplate } from "./JournalTemplate"

export class TemplateRegistry {
  private templates: Map<string, JournalTemplate> = new Map()

  addTemplate(name: string, template: JournalTemplate): void {
    this.templates.set(name, template)
  }

  getTemplate(name: string): JournalTemplate | undefined {
    const template = this.templates.get(name)
    return template ? template.clone() : undefined
  }

  getTemplateNames(): string[] {
    return Array.from(this.templates.keys())
  }
}

Copy after login
Copy after login
  • To create a template we first prompt the user to enter a template name.
  • Then we instantiate a new template object, with the name and an empty array for the sections.
  • After that, we prompt the user to enter the details of the sections, after entering every section's informations, the user can choose to either stop or enter more sections.

utils.ts > promptForSectionDetails

import { JournalTemplate } from "./JournalTemplate"
import { TemplateRegistry } from "./TemplateRegistry"

export const registry = new TemplateRegistry()

registry.addTemplate(
  "Daily Reflection",
  new JournalTemplate("Daily Reflection", [
    {
      title: "Gratitude",
      prompt: "List three things you're grateful for today.",
    },
    { title: "Accomplishments", prompt: "What did you accomplish today?" },
    {
      title: "Challenges",
      prompt: "What challenges did you face and how did you overcome them?",
    },
    {
      title: "Tomorrow's Goals",
      prompt: "What are your top 3 priorities for tomorrow?",
    },
  ])
)

registry.addTemplate(
  "Weekly Review",
  new JournalTemplate("Weekly Review", [
    { title: "Highlights", prompt: "What were the highlights of your week?" },
    {
      title: "Lessons Learned",
      prompt: "What important lessons did you learn this week?",
    },
    {
      title: "Progress on Goals",
      prompt: "How did you progress towards your goals this week?",
    },
    {
      title: "Next Week's Focus",
      prompt: "What's your main focus for next week?",
    },
  ])
)

Copy after login

The promptForSectionDetails function use the inquirer package to ask for the title, then prompt sequentially from the user.

View The templates :

TemplateActions.ts > viewTemplates

import chalk from "chalk"
import inquirer from "inquirer"

import { JournalTemplate } from "./JournalTemplate"
import { registry } from "./registry"
import { editTemplateSections } from "./templateSectionsActions"
import { promptForSectionDetails } from "./utils"

export async function createTemplate(): Promise<void> {
  const { name } = await inquirer.prompt<{ name: string }>([
    {
      type: "input",
      name: "name",
      message: "Enter a name for the new template:",
    },
  ])

  const newTemplate = new JournalTemplate(name, [])
  let addMore = true
  while (addMore) {
    const newSection = await promptForSectionDetails()
    newTemplate.addSection(newSection)
    const { more } = await inquirer.prompt<{ more: boolean }>([
      {
        type: "confirm",
        name: "more",
        message: "Add another section?",
        default: false,
      },
    ])
    addMore = more
  }

  registry.addTemplate(name, newTemplate)
  console.log(chalk.green(`Template "${name}" created successfully!`))
}

Copy after login

The viewTemplates function works as follow:

  1. We first get all the templates from the registry , then we loop through the returned templates array and use the display method which we've defined earlier in the JournalTemplate class.

Use a template to create a Journaling Entry : The reason for creating journaling templates, is to make our lives easier when writing our different types of journals, instead of facing an empty page, it's better easier to fill the journal when faced with a bunch of sequential sections titles and prompts.

Let's dive into the useTemplate function:

  1. First we select one template among the existing templates, after getting the template names from the registery.
  2. For every section in the template, the user will asked to open his prefered editor to fill the journal section text.

TemplateActions.ts > useTemplate

class Socket {
  // code........

  clone(): Socket {
    // business logic to instantiate the socket.
    return new Socket(/*...Params*/)
  }
}


const socket1 = new Socket()
const socket2 = socket1.clone()

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

Create a Template From an Existing Template :

Finally, We are going to see the prototype design pattern in action.

Let's explore how can we create new types of templates dynamicly by overriding the existing templates.

  1. First we prompt the user to select the template that he wants to override from the existing ones.
  2. Then we prompt it again to type the name of the newly created template.
  3. We use the registry to get the template given the template name which is selected by the user.
  4. We use the clone method to get a clone object that matches the selected template.

As you can see from the code bellow, we don't even need to know about the details of the JournalTemplate class or to polute our code by importing it.

TemplateActions.ts > createFromExistingTemplate

  1. Finally, we set the template name given by the user to the newly created object, and then prompt the user to perform any crud operations on the existing template sections using the editTemplateSections method, which we will be explaining bellow just after the code block.
import chalk from "chalk"

import { TemplateSection } from "./types"

export interface TemplateSection {
  title: string
  prompt: string
}

export class JournalTemplate {
  constructor(
    public name: string,
    public sections: TemplateSection[]
  ) {}
  clone(): JournalTemplate {
    return new JournalTemplate(
      this.name,
      this.sections.map((s) => ({ ...s }))
    )
  }

  display(): void {
    console.log(chalk.cyan(`\nTemplate: ${this.name}`))
    this.sections.forEach((section, index) => {
      console.log(chalk.yellow(`${index + 1}. ${section.title}`))
      console.log(chalk.gray(` Prompt: ${section.prompt}`))
    })
  }

  addSection(section: TemplateSection): void {
    this.sections.push(section)
  }

  removeSection(index: number): void {
    if (index >= 0 && index < this.sections.length) {
      this.sections.splice(index, 1)
    } else {
      throw new Error("Invalid section index")
    }
  }

  editSection(index: number, newSection: TemplateSection): void {
    if (index >= 0 && index < this.sections.length) {
      this.sections[index] = newSection
    } else {
      throw new Error("Invalid section index")
    }
  }

  getSectionCount(): number {
    return this.sections.length
  }

  getSection(index: number): TemplateSection | undefined {
    return this.sections[index]
  }

  setName(newName: string): void {
    this.name = newName
  }
}

Copy after login
Copy after login

templateSectionsAction > editTemplateSections

import { JournalTemplate } from "./JournalTemplate"

export class TemplateRegistry {
  private templates: Map<string, JournalTemplate> = new Map()

  addTemplate(name: string, template: JournalTemplate): void {
    this.templates.set(name, template)
  }

  getTemplate(name: string): JournalTemplate | undefined {
    const template = this.templates.get(name)
    return template ? template.clone() : undefined
  }

  getTemplateNames(): string[] {
    return Array.from(this.templates.keys())
  }
}

Copy after login
Copy after login

The editTemplateSections defined bellow basically prompts displays a menu, asking the user to override the existing sections as needed by offering different operations like:

  • Add Section
  • Remove Section
  • Edit Section

Application menu

Finally, We make use of all of the previous functions in our index.ts file, which bootsraps the cli app, and displays a menu with the different template manipulation options:

  • Create a Template.
  • Create a Template from an Existing Template.
  • View Templates.
  • Use a Template to create a journaling entry.
  • Exit the program.

index.ts

class Socket {
  // code........

  clone(): Socket {
    // business logic to instantiate the socket.
    return new Socket(/*...Params*/)
  }
}


const socket1 = new Socket()
const socket2 = socket1.clone()

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

Conclusion

The Prototype design pattern provides a powerful way to create new objects by cloning existing ones. In our journaling template application, we've seen how this pattern allows us to create new templates based on existing ones, demonstrating the flexibility and efficiency of the Prototype pattern.

By using this pattern, we've created a system that's easy to extend and modify, showcasing the true power of object-oriented design patterns in real-world applications.

Contact

If you have any questions or want to discuss something further feel free to Contact me here.

Happy coding!

The above is the detailed content of Mastering the Prototype Design Pattern: A Comprehensive Guide. For more information, please follow other related articles on the PHP Chinese website!

source:dev.to
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Articles by Author
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template