首页 >web前端 >js教程 >掌握原型设计模式:综合指南

掌握原型设计模式:综合指南

Barbara Streisand
Barbara Streisand原创
2024-11-17 21:41:021053浏览

您是否曾经从库中导入一个对象并尝试克隆它,但却失败了,因为克隆它需要对库的内部结构有广泛的了解?

或者,在一个项目工作了很长时间之后,您休息一下重构代码,并注意到您正在代码库的各个部分重新克隆许多复杂的对象?

嗯,原型设计模式已经满足你了!

在本文中,我们将探索原型设计模式,同时构建功能齐全的日记模板 Node.js CLI 应用程序。

话不多说,让我们开始吧!

概述

原型是一种创意设计模式,它是一类设计模式,用于处理使用new 创建对象的本机方式所带来的不同问题。 关键字或运算符。

问题

工厂设计模式解决了以下创建问题:

  1. 如何复制应用程序中的现有对象而不依赖于其具体类?

  2. 一些复杂的对象很难克隆,因为它们要么有很多需要您不知道的特定业务逻辑的字段,要么有很多无法从外部访问的私有字段物体。

让我们以从 socket.io 库 导入的 socket 对象为例,想象一下必须自己克隆它吗?

您将必须浏览库内的代码,了解套接字如何工作,这些对象甚至有一些循环依赖关系,您必须处理自己才能克隆它。

除此之外,您的代码将依赖于套接字类或接口以及相应的业务逻辑来创建它,这违反了可靠的依赖倒置原则,并使您的代码对于更改的鲁棒性较差。

解决方案

原型设计模式解决了这些问题,通过委托将对象复制到对象本身的责任,通过在每个对象的类中声明一个clone方法,该方法应该是可克隆。

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

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


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

结构

要实现原型设计模式,您可以直接在可克隆对象中包含

clone 方法。

或者创建一个通用接口

原型,它可以由所有可克隆对象实现。 Mastering the Prototype Design Pattern: A Comprehensive Guide

拥有公共接口的一个好处是能够在公共注册服务类中注册所有原型,该服务类将负责缓存常用的原型并将其返回给用户。不必每次调用 clone 方法时都克隆对象。

这非常方便,尤其是在克隆复杂对象时。

Mastering the Prototype Design Pattern: A Comprehensive Guide

实际场景

在本节中,我们将通过构建一个迷你日记模板 Nodejs CLI 应用程序来演示此设计模式。

正如我们之前看到的,原型设计模式将把对象克隆到对象本身的责任委托给对象。

但是你有没有想过为什么它被称为原型?我的意思是这与克隆有什么关系?

我们将通过这个实际例子来回答这个问题,请继续阅读并关注。
您可以在此存储库中找到最终代码。只需克隆它并运行以下命令即可。

创建我们的原型:日记模板类

首先让我们创建一个JournalTemplate,它具有以下属性:

  1. 名称:我们需要它来识别模板。
  2. 部分:部分是日记模板的一部分,保留用于特定主题或主题,例如:感恩、挑战、明天的目标......

每个部分都包含以下属性:

  • 标题该部分的主题或主题:感恩、挑战、明天的目标...
  • 提示 当用户要编写部分日志文本时将向用户显示的消息。

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()

JournalTemplate 类有许多实用方法来设置其不同的属性。

display 方法稍后将用于向终端显示彩色格式良好的输出。

chalk 包用于为输出的终端文本的一些片段着色。

我们的 JournalTemplate 对象顾名思义,用作创建其他模板或日记文件条目的模板或原型。

这就是为什么我们将 clone 方法添加到 JournalTemplate 类中。

我们添加它是为了将处理克隆业务逻辑的责任交给 JournalTemplate 对象本身,而不是消费代码。

声明日志模板注册

现在让我们创建 TemplateRegistry 类,它将负责存储 JournalTemplate 类原型实例。同时提供操作这些实例的方法。

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()

注册表将这些类存储在 Map 对象中,以便按名称快速检索,并公开许多用于添加或停用模板实例的实用方法。

实例化日志模板注册表

现在,让我们实例化模板寄存器,然后播种一些初始模板。

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
  }
}

定义模板操作方法

在本节中,我们将定义一系列将在我们的应用程序菜单中使用的函数,以执行各种操作,例如:

  1. 提示用户输入模板的名称,然后再次递归提示以创建任意数量的部分。
  2. 查看所有现有或已创建的模板。
  3. 使用模板创建日记文件条目。
  4. 从现有模板创建新模板:用户将被要求选择一个现有模板,然后他将能够直接使用它或覆盖其名称和部分。

新创建的模板可用于创建新的日记条目 (1)。

创建模板 :

TemplateActions.ts >创建模板

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())
  }
}

  • 要创建模板,我们首先提示用户输入模板名称。
  • 然后我们实例化一个新的模板对象,其中包含名称和各个部分的空数组。
  • 之后,我们会提示用户输入各个部分的详细信息,输入每个部分的信息后,用户可以选择停止或进入更多部分。

utils.ts>提示部分详细信息

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?",
    },
  ])
)

promptForSectionDetails 函数使用 inquirer 包询问标题,然后按顺序向用户提示。

查看模板 :

TemplateActions.ts >视图模板

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!`))
}

viewTemplates 函数的工作原理如下:

  1. 我们首先从 registry 获取所有模板,然后循环返回的模板数组并使用我们之前在 JournalTemplate 中定义的 display 方法 上课。

使用模板创建日记条目:创建日记模板的原因,是为了让我们在撰写不同类型的日记时变得更轻松,而不是面对空白页,更好的是更容易当面对一堆连续的章节标题和提示时,填写日记。

让我们深入了解 useTemplate 函数:

  1. 首先,我们从注册表获取模板名称后,在现有模板中选择一个模板。
  2. 对于模板中的每个部分,用户都会要求打开他喜欢的编辑器来填写日记部分文本。

TemplateActions.ts >使用模板

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

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


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

从现有模板创建模板 :

最后,我们将看到原型设计模式的实际应用。

让我们探索如何通过覆盖现有模板来动态创建新类型的模板

  1. 首先,我们提示用户从现有模板中选择他想要覆盖的模板。
  2. 然后我们再次提示它输入新创建的模板的名称。
  3. 我们使用注册表来获取给定用户选择的模板名称的模板。
  4. 我们使用clone方法来获取与所选模板匹配的克隆对象。

从下面的代码中可以看到,我们甚至不需要了解 JournalTemplate 类的详细信息,也不需要通过导入它来污染我们的代码。

TemplateActions.ts >从现有模板创建

  1. 最后,我们将用户指定的模板名称设置为新创建的对象,然后提示用户使用 editTemplateSections 方法对现有模板部分执行任何增删改查操作,我们将使用该方法在代码块之后解释如下。
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
  }
}

templateSectionsAction >编辑模板部分

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())
  }
}

下面定义的 editTemplateSections 基本上会提示显示一个菜单,要求用户通过提供不同的操作来覆盖现有部分,例如:

  • 添加部分
  • 删除部分
  • 编辑部分

应用菜单

最后,我们利用 index.ts 文件中的所有先前函数,该文件引导 cli 应用程序,并显示带有不同模板操作选项的菜单:

  • 创建模板。
  • 从现有模板创建模板。
  • 查看模板。
  • 使用模板创建日记条目。
  • 退出程序。

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()

结论

原型设计模式提供了一种通过克隆现有对象来创建新对象的强大方法。在我们的日记模板应用程序中,我们已经看到该模式如何允许我们基于现有模板创建新模板,展示了原型模式的灵活性和效率。

通过使用此模式,我们创建了一个易于扩展和修改的系统,展示了面向对象设计模式在实际应用程序中的真正威力。

接触

如果您有任何疑问或想进一步讨论,请随时与我联系。

编码愉快!

以上是掌握原型设计模式:综合指南的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn