”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > JavaScript 设计模式指南

JavaScript 设计模式指南

发布于2024-08-24
浏览:503

Written by Hussain Arif✏️

Imagine a situation where a group of architects wants to design a skyscraper. During the design stage, they would have to consider a plethora of factors, for example:

  • The architectural style — should the building be brutalist, minimalist, or something else?
  • The width of the base — what sizing is needed to prevent collapse during windy days?
  • Protection against natural disasters — what preventative structural measures need to be in place based on the location of this building to prevent damage from earthquakes, flooding, etc.?

There can be many factors to consider, but one thing can be known for certain: there’s most likely a blueprint already available to help construct this skyscraper. Without a common design or plan, these architects would have to reinvent the wheel, which can lead to confusion and multiple inefficiencies.

Similarly in the programming world, developers often refer to a set of design patterns to help them build software while following clean code principles. Moreover, these patterns are ubiquitous, thus letting programmers focus on shipping new features instead of reinventing the wheel every time.

In this article, you will learn about a few commonly used JavaScript design patterns, and together we’ll build small Node.js projects to illustrate the usage of each design pattern.

What are design patterns in software engineering?

Design patterns are pre-made blueprints that developers can tailor to solve repetitive design problems during coding. One crucial thing to remember is that these blueprints are not code snippets but rather general concepts to approach incoming challenges.

Design patterns have many benefits:

  • Tried and tested — they solve countless problems in software design. Knowing and applying patterns in code is useful because doing so can help you solve all sorts of problems using principles of object-oriented design
  • Define a common language — design patterns help teams communicate in an efficient manner. For example, a teammate can say, “We should just use the factory method to solve this issue,” and everyone will understand what they mean and the motive behind their suggestion

In this article, we will cover three categories of design patterns:

  • Creational — Used for creating objects
  • Structural — Assembling these objects to form a working structure
  • Behavioral — Assigning responsibilities between those objects

Let’s see these design patterns in action!

Creational design patterns

As the name suggests, creational patterns comprise various methods to help developers create objects.

Factory

The factory method is a pattern for creating objects that allow more control over object creation. This method is suitable for cases where we want to keep the logic for object creation centralized in one place.

Here is some sample code that showcases this pattern in action:

//file name: factory-pattern.js
//use the factory JavaScript design pattern:
//Step 1: Create an interface for our object. In this case, we want to create a car
const createCar = ({ company, model, size }) => ({
//the properties of the car:
  company,
  model,
  size,
  //a function that prints out the car's properties:
  showDescription() {
    console.log(
      "The all new ",
      model,
      " is built by ",
      company,
      " and has an engine capacity of ",
      size,
      " CC "
    );
  },
});
//Use the 'createCar' interface to create a car
const challenger = createCar({
  company: "Dodge",
  model: "Challenger",
  size: 6162,
});
//print out this object's traits:
challenger.showDescription();

Let’s break down this code piece by piece:createCarCar

  • Each Car has three properties: company , model and size. Additionally, we have also defined a showDescription function, which will log out the properties of the object. Furthermore, notice that the createCar method demonstrates how we can have granular control when it comes to instantiating objects in memory
  • Later on, we used our createCar instance to initialize an object called challenger
  • Finally, in the last line, we invoked the showDescription on our challenger instance

Let’s test it out! We should expect the program to log out the details of our newly-created Car instance: JavaScript design patterns guide

Builder

The builder method lets us build objects using step-by-step object construction. As a result, this design pattern is great for situations where we want to create an object and only apply necessary functions. As a result, this allows for greater flexibility.

Here is a block of code that uses the builder pattern to create a Car object:

//builder-pattern.js
//Step 1: Create a class reperesentation for our toy car:
class Car {
  constructor({ model, company, size }) {
    this.model = model;
    this.company = company;
    this.size = size;
  }
}
//Use the 'builder' pattern to extend this class and add functions
//note that we have seperated these functions in their entities.
//this means that we have not defined these functions in the 'Car' definition.
Car.prototype.showDescription = function () {
  console.log(
    this.model  
      " is made by "  
      this.company  
      " and has an engine capacity of "  
      this.size  
      " CC "
  );
};
Car.prototype.reduceSize = function () {
  const size = this.size - 2; //function to reduce the engine size of the car.
  this.size = size;
};
const challenger = new Car({
  company: "Dodge",
  model: "Challenger",
  size: 6162,
});
//finally, print out the properties of the car before and after reducing the size:
challenger.showDescription();
console.log('reducing size...');
//reduce size of car twice:
challenger.reduceSize();
challenger.reduceSize();
challenger.showDescription();

Here’s what we’re doing in the code block above:

  • As a first step, we created a Car class which will help us instantiate objects. Notice that earlier in the factory pattern, we used a createCar function, but here we are using classes. This is because classes in JavaScript let developers construct objects in pieces. Or, in simpler words, to implement the JavaScript builder design pattern, we have to opt for the object-oriented paradigm
  • Afterwards, we used the prototype object to extend the Car class. Here, we created two functions — showDescription and reduceSize
  • Later on, we then created our Car instance, named it challenger, and then logged out its information
  • Finally, we invoked the reduceSize method on this object to decrement its size, and then we printed its properties once more

The expected output should be the properties of the challenger object before and after we reduced its size by four units: JavaScript design patterns guide   This confirms that our builder pattern implementation in JavaScript was successful!

Structural design patterns

Structural design patterns focus on how different components of our program work together.

Adapter

The adapter method allows objects with conflicting interfaces to work together. A great use case for this pattern is when we want to adapt old code to a new codebase without introducing breaking changes:

//adapter-pattern.js
//create an array with two fields: 
//'name' of a band and the number of 'sold' albums
const groupsWithSoldAlbums = [
  {
    name: "Twice",
    sold: 23,
  },
  { name: "Blackpink", sold: 23 },
  { name: "Aespa", sold: 40 },
  { name: "NewJeans", sold: 45 },
];
console.log("Before:");
console.log(groupsWithSoldAlbums);
//now we want to add this object to the 'groupsWithSoldAlbums' 
//problem: Our array can't accept the 'revenue' field
// we want to change this field to 'sold'
var illit = { name: "Illit", revenue: 300 };
//Solution: Create an 'adapter' to make both of these interfaces..
//..work with each other
const COST_PER_ALBUM = 30;
const convertToAlbumsSold = (group) => {
  //make a copy of the object and change its properties
  const tempGroup = { name: group.name, sold: 0 };
  tempGroup.sold = parseInt(group.revenue / COST_PER_ALBUM);
  //return this copy:
  return tempGroup;
};
//use our adapter to make a compatible copy of the 'illit' object:
illit = convertToAlbumsSold(illit);
//now that our interfaces are compatible, we can add this object to the array
groupsWithSoldAlbums.push(illit);
console.log("After:");
console.log(groupsWithSoldAlbums);

Here’s what’s happening in this snippet:

  • First, we created an array of objects called groupsWithSoldAlbums. Each object will have a name and sold property
  • We then made an illit object which had two properties — name and revenue. Here, we want to append this to the groupsWithSoldAlbums array. This might be an issue, since the array doesn’t accept a revenue property
  • To mitigate this problem, use the adapter method. The convertToAlbumsSold function will adjust the illit object so that it can be added to our array

When this code is run, we expect our illit object to be part of the groupsWithSoldAlbums list: JavaScript design patterns guide

Decorator

This design pattern lets you add new methods and properties to objects after creation. This is useful when we want to extend the capabilities of a component during runtime.

If you come from a React background, this is similar to using Higher Order Components. Here is a block of code that demonstrates the use of the JavaScript decorator design pattern:

//file name: decorator-pattern.js
//Step 1: Create an interface
class MusicArtist {
  constructor({ name, members }) {
    this.name = name;
    this.members = members;
  }
  displayMembers() {
    console.log(
      "Group name",
      this.name,
      " has",
      this.members.length,
      " members:"
    );
    this.members.map((item) => console.log(item));
  }
}
//Step 2: Create another interface that extends the functionality of MusicArtist
class PerformingArtist extends MusicArtist {
  constructor({ name, members, eventName, songName }) {
    super({ name, members });
    this.eventName = eventName;
    this.songName = songName;
  }
  perform() {
    console.log(
      this.name  
        " is now performing at "  
        this.eventName  
        " They will play their hit song "  
        this.songName
    );
  }
}
//create an instance of PerformingArtist and print out its properties:
const akmu = new PerformingArtist({
  name: "Akmu",
  members: ["Suhyun", "Chanhyuk"],
  eventName: "MNET",
  songName: "Hero",
});
akmu.displayMembers();
akmu.perform();

Let's explain what's happening here:

  • In the first step, we created a MusicArtist class which has two properties: name and members. It also has a displayMembers method, which will print out the name and the members of the current music band
  • Later on, we extended MusicArtist and created a child class called PerformingArtist. In addition to the properties of MusicArtist, the new class will have two more properties: eventName and songName. Furthermore, PerformingArtist also has a perform function, which will print out the name and the songName properties to the console
  • Afterwards, we created a PerformingArtist instance and named it akmu
  • Finally, we logged out the details of akmu and invoked the perform function

The output of the code should confirm that we successfully added new capabilities to our music band via the PerformingArtist class: JavaScript design patterns guide

Behavioral design patterns

This category focuses on how different components in a program communicate with each other.

Chain of Responsibility

The Chain of Responsibility design pattern allows for passing requests through a chain of components. When the program receives a request, components in the chain either handle it or pass it on until the program finds a suitable handler.

Here’s an illustration that explains this design pattern: JavaScript design patterns guide The bucket, or request, is passed down the chain of components until a capable component is found. When a suitable component is found, it will process the request. Source: Refactoring Guru.[/caption] The best use for this pattern is a chain of Express middleware functions, where a function would either process an incoming request or pass it to the next function via the next() method:

//Real-world situation: Event management of a concert
//implement COR JavaScript design pattern:
//Step 1: Create a class that will process a request
class Leader {
  constructor(responsibility, name) {
    this.responsibility = responsibility;
    this.name = name;
  }
  //the 'setNext' function will pass the request to the next component in the chain.
  setNext(handler) {
    this.nextHandler = handler;
    return handler;
  }
  handle(responsibility) {
  //switch to the next handler and throw an error message:
    if (this.nextHandler) {
      console.log(this.name   " cannot handle operation: "   responsibility);
      return this.nextHandler.handle(responsibility);
    }
    return false;
  }
}
//create two components to handle certain requests of a concert
//first component: Handle the lighting of the concert:
class LightsEngineerLead extends Leader {
  constructor(name) {
    super("Light management", name);
  }
  handle(responsibility) {
  //if 'LightsEngineerLead' gets the responsibility(request) to handle lights,
  //then they will handle it
    if (responsibility == "Lights") {
      console.log("The lights are now being handled by ", this.name);
      return;
    }
    //otherwise, pass it to the next component.
    return super.handle(responsibility);
  }
}

//second component: Handle the sound management of the event:
class SoundEngineerLead extends Leader {
  constructor(name) {
    super("Sound management", name);
  }
  handle(responsibility) {
  //if 'SoundEngineerLead' gets the responsibility to handle sounds,
  // they will handle it
    if (responsibility == "Sound") {
      console.log("The sound stage is now being handled by ", this.name);
      return;
    }
    //otherwise, forward this request down the chain:
    return super.handle(responsibility);
  }
}
//create two instances to handle the lighting and sounds of an event:
const minji = new LightsEngineerLead("Minji");
const danielle = new SoundEngineerLead("Danielle");
//set 'danielle' to be the next handler component in the chain.
minji.setNext(danielle);
//ask Minji to handle the Sound and Lights:
//since Minji can't handle Sound Management, 
// we expect this request to be forwarded 
minji.handle("Sound");
//Minji can handle Lights, so we expect it to be processed
minji.handle("Lights");

In the above code, we’ve modeled a situation at a music concert. Here, we want different people to handle different responsibilities. If a person cannot handle a certain task, it’s delegated to the next person in the list.

Initially, we declared a Leader base class with two properties:

  • responsibility — the kind of task the leader can handle
  • name — the name of the handler

Additionally, each Leader will have two functions:

  • setNext: As the name suggests, this function will add a Leader to the responsibility chain
  • handle: The function will check if the current Leader can process a certain responsibility; otherwise, it will forward that responsibility to the next person via the setNext method

Next, we created two child classes called LightsEngineerLead (responsible for lighting), and SoundEngineerLead (handles audio). Later on, we initialized two objects — minji and danielle. We used the setNext function to set danielle as the next handler in the responsibility chain.

Lastly, we asked minji to handle Sound and Lights.

When the code is run, we expect minji to attempt at processing our Sound and Light responsibilities. Since minji is not an audio engineer, it should hand over Sound to a capable handler. In this case, it is danielle: JavaScript design patterns guide

Strategy

The strategy method lets you define a collection of algorithms and swap between them during runtime. This pattern is useful for navigation apps. These apps can leverage this pattern to switch between routes for different user types (cycling, driving, or running):

This code block demonstrates the strategy design pattern in JavaScript code:

//situation: Build a calculator app that executes an operation between 2 numbers.
//depending on the user input, change between division and modulus operations

class CalculationStrategy {
  performExecution(a, b) {}
}
//create an algorithm for division
class DivisionStrategy extends CalculationStrategy {
  performExecution(a, b) {
    return a / b;
  }
}
//create another algorithm for performing modulus
class ModuloStrategy extends CalculationStrategy {
  performExecution(a, b) {
    return a % b;
  }
}
//this class will help the program switch between our algorithms:
class StrategyManager {
  setStrategy(strategy) {
    this.strategy = strategy;
  }
  executeStrategy(a, b) {
    return this.strategy.performExecution(a, b);
  }
}

const moduloOperation = new ModuloStrategy();
const divisionOp = new DivisionStrategy();
const strategyManager = new StrategyManager();
//use the division algorithm to divide two numbers:
strategyManager.setStrategy(divisionOp);
var result = strategyManager.executeStrategy(20, 4);
console.log("Result is: ", result);
//switch to the modulus strategy to perform modulus:
strategyManager.setStrategy(moduloOperation);
result = strategyManager.executeStrategy(20, 4);
console.log("Result of modulo is ", result);

Here’s what we did in the above block:

  • First we created a base CalculationStrategy abstract class which will process two numbers — a and b
  • We then defined two child classes — DivisionStrategy and ModuloStrategy. These two classes consist of division and modulo algorithms and return the output
  • Next, we declared a StrategyManager class which will let the program alternate between different algorithms
  • In the end, we used our DivisionStrategy and ModuloStrategy algorithms to process two numbers and return its output. To switch between these strategies, the strategyManager instance was used

When we execute this program, the expected output is strategyManager first using DivisionStrategy to divide two numbers and then switching to ModuloStrategy to return the modulo of those inputs: JavaScript design patterns guide

Conclusion

In this article, we learned about what design patterns are, and why they are useful in the software development industry. Furthermore, we also learned about different categories of JavaScript design patterns and implemented them in code.


LogRocket: Debug JavaScript errors more easily by understanding the context

Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.

JavaScript design patterns guide

LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

Try it for free.

版本声明 本文转载于:https://dev.to/logrocket/javascript-design-patterns-guide-3p8k?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • 增强您的 Web 动画:像专业人士一样优化 requestAnimationFrame
    增强您的 Web 动画:像专业人士一样优化 requestAnimationFrame
    流畅且高性能的动画在现代 Web 应用程序中至关重要。然而,管理不当可能会使浏览器的主线程过载,导致性能不佳和动画卡顿。 requestAnimationFrame (rAF) 是一种浏览器 API,旨在将动画与显示器的刷新率同步,从而确保与 setTimeout 等替代方案相比更流畅的运动。但有效...
    编程 发布于2024-11-06
  • 为什么MySQL服务器在60秒内就消失了?
    为什么MySQL服务器在60秒内就消失了?
    MySQL 服务器已消失 - 恰好在 60 秒内在此场景中,之前成功运行的 MySQL 查询现在遇到了60 秒后超时,显示错误“MySQL 服务器已消失”。即使调整了 wait_timeout 变量,问题仍然存在。分析:超时正好发生在 60 秒,这表明是设置而不是资源限制是原因。直接从 MySQL ...
    编程 发布于2024-11-06
  • 为什么带有“display: block”和“width: auto”的按钮无法拉伸以填充其容器?
    为什么带有“display: block”和“width: auto”的按钮无法拉伸以填充其容器?
    了解具有“display: block”和“width: auto”的按钮的行为当您设置“display: block”时一个按钮,它会调整其布局以占据可用的整个宽度。但是,如果将其与“width: auto”结合使用,则按钮会出现意外行为,并且无法拉伸以填充其容器。此行为源于按钮作为替换元素的基本...
    编程 发布于2024-11-06
  • 为 Bluesky Social 创建机器人
    为 Bluesky Social 创建机器人
    How the bot will work We will develop a bot for the social network Bluesky, we will use Golang for this, this bot will monitor some hashtags ...
    编程 发布于2024-11-06
  • 为什么 PHP 的浮点运算会产生意外的结果?
    为什么 PHP 的浮点运算会产生意外的结果?
    PHP 中的浮点数计算精度:为什么它很棘手以及如何克服它在 PHP 中处理浮点数时,这一点至关重要了解其固有的准确性限制。如代码片段所示:echo("success");} else {echo("error");} 您可能会惊讶地发现,尽管值之间的差异小于 ...
    编程 发布于2024-11-06
  • Python中可以通过变量ID逆向获取对象吗?
    Python中可以通过变量ID逆向获取对象吗?
    从 Python 中的变量 ID 检索对象引用Python 中的 id() 函数返回对象的唯一标识。人们很容易想知道是否可以反转此过程并从其 ID 获取对象。具体来说,我们想要检查取消引用变量的 ID 是否会检索原始对象:dereference(id(a)) == a理解解引用的概念及其在 Pyth...
    编程 发布于2024-11-06
  • Go 的 Defer 关键字如何在函数执行顺序中发挥作用?
    Go 的 Defer 关键字如何在函数执行顺序中发挥作用?
    了解 Go 的 Defer 关键字的功能使用 Go 时,了解 defer 关键字的行为至关重要。该关键字允许开发人员推迟函数的执行,直到周围的函数返回。但是,需要注意的是,函数的值和参数在执行 defer 语句时进行评估。示例:评估 Defer Order为了说明这一点,请考虑以下内容代码:pack...
    编程 发布于2024-11-06
  • WordPress Gutenberg 全局状态管理初学者指南
    WordPress Gutenberg 全局状态管理初学者指南
    构建复杂的 WordPress 块编辑器 (Gutenberg) 应用程序时,有效管理状态变得至关重要。这就是 @wordpress/data 发挥作用的地方。它允许您跨 WordPress 应用程序中的不同块和组件管理和共享全局状态。 如果您不熟悉管理全局状态或使用@wordpress/data,...
    编程 发布于2024-11-06
  • 亚马逊解析简单且完全由您自己完成
    亚马逊解析简单且完全由您自己完成
    I came across a script on the Internet that allows you to parse product cards from Amazon. And I just needed a solution to a problem like that. I wrac...
    编程 发布于2024-11-06
  • React JSX 如何在幕后转换为 JavaScript
    React JSX 如何在幕后转换为 JavaScript
    当您编写 React 时,您会经常看到 JSX – 一种在 JavaScript 代码中看起来像 HTML 的语法。但你有没有想过这段代码在浏览器中是如何运行的? 神奇之处在于:JSX 不是有效的 JavaScript!浏览器无法直接理解它。在幕后,像 Babel 这样的工具介入将 JSX 转换(或...
    编程 发布于2024-11-06
  • 如何通过 CSS 变换实现倾斜:两侧倾斜
    如何通过 CSS 变换实现倾斜:两侧倾斜
    使用 CSS 变换实现倾斜:倾斜两侧提供的图像展示了一种有趣的倾斜效果,该效果使元素的两个角都形成角度。要使用 CSS 转换重新创建此效果,请按照下列步骤操作:应用透视倾斜:要添加透视,请使用以下 CSS 属性:transform: perspective(distance) rotateY(ang...
    编程 发布于2024-11-06
  • Express.js 基础知识:初学者指南 - Node.js 教程系列 - 第 10 部分
    Express.js 基础知识:初学者指南 - Node.js 教程系列 - 第 10 部分
    介绍: 嘿!如果您是 Node.js 新手,您可能听说过 Express.js——一个用于构建 Web 服务器和 API 的轻量级、快速且灵活的框架。在本指南中,我将引导您了解 Express 的基础知识,并向您展示入门是多么容易。 准备好?让我们开始吧! 1.安装...
    编程 发布于2024-11-06
  • Python:未来的语言
    Python:未来的语言
    在不断发展的技术领域,某些编程语言已经占据主导地位,并塑造了我们构建软件和与软件交互的方式。其中,Python 脱颖而出,它不仅获得了巨大的普及,而且还将自己定位为未来技术的关键工具。其简单性、多功能性和强大的库使 Python 成为从 Web 开发到数据科学、人工智能、自动化等各种应用程序的首选语...
    编程 发布于2024-11-06
  • 如何在 PHP 中将 PDF 文件存储为 MySQL BLOB(带有代码示例)?
    如何在 PHP 中将 PDF 文件存储为 MySQL BLOB(带有代码示例)?
    使用 PHP 将 PDF 文件存储为 MySQL BLOB使用 PHP 在 MySQL 中将 PDF 文件存储为 BLOB(二进制大对象)时,建议考虑在数据库中存储二进制数据的潜在缺点。但是,如果您选择这样做,可以采用以下方法:首先,定义一个包含整数 ID 字段和名为 DATA 的 BLOB 列的表...
    编程 发布于2024-11-06
  • 使用 React Router v6 在 React 中实现面包屑
    使用 React Router v6 在 React 中实现面包屑
    面包屑在网页开发中非常重要,因为它们为用户提供了一种方法来跟踪他们在我们网页中的当前位置,并帮助我们的网页导航。 在本指南中,我们将使用 React-router v6 和 Bootstrap 在 React 中实现面包屑。 React-router v6 是 React 和 React Nati...
    编程 发布于2024-11-06

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3