>웹 프론트엔드 >JS 튜토리얼 >JavaScript 디자인 패턴 가이드

JavaScript 디자인 패턴 가이드

WBOY
WBOY원래의
2024-08-07 00:08:23558검색

후세인 아리프 작사✏️

건축가 그룹이 초고층 빌딩을 설계하려는 상황을 상상해 보세요. 디자인 단계에서는 다음과 같은 다양한 요소를 고려해야 합니다.

  • 건축 스타일 - 건물이 브루탈리즘, 미니멀리스트, 아니면 다른 것이어야 합니까?
  • 베이스의 너비 - 바람이 부는 날 무너지는 것을 방지하려면 어떤 크기가 필요합니까?
  • 자연재해 방지 — 지진, 홍수 등으로 인한 피해를 방지하려면 이 건물의 위치에 따라 어떤 예방적 구조 조치를 취해야 합니까?

고려해야 할 요소가 많을 수 있지만 한 가지는 확실하게 알 수 있습니다. 이 초고층 빌딩을 건설하는 데 도움이 되는 청사진이 이미 있을 가능성이 높다는 것입니다. 공통된 디자인이나 계획이 없으면 이러한 건축가는 바퀴를 다시 만들어야 하며 이로 인해 혼란과 여러 가지 비효율성이 발생할 수 있습니다.

마찬가지로 프로그래밍 세계에서도 개발자는 깔끔한 코드 원칙을 따르면서 소프트웨어를 구축하는 데 도움이 되도록 디자인 패턴 세트를 참조하는 경우가 많습니다. 더욱이 이러한 패턴은 어디에나 존재하므로 프로그래머는 매번 바퀴를 재발명하는 대신 새로운 기능을 제공하는 데 집중할 수 있습니다.

이 기사에서는 일반적으로 사용되는 몇 가지 JavaScript 디자인 패턴에 대해 알아보고 함께 작은 Node.js 프로젝트를 구축하여 각 디자인 패턴의 사용법을 설명하겠습니다.

소프트웨어 엔지니어링의 디자인 패턴은 무엇입니까?

디자인 패턴은 개발자가 코딩 중에 반복적인 디자인 문제를 해결하기 위해 맞춤 설정할 수 있는 미리 만들어진 청사진입니다. 기억해야 할 중요한 점 중 하나는 이러한 청사진이 코드 조각이 아니라 다가오는 과제에 접근하기 위한 일반적인 개념이라는 것입니다.

디자인 패턴에는 많은 이점이 있습니다.

  • 시험과 테스트를 거쳤습니다 — 소프트웨어 설계의 수많은 문제를 해결합니다. 코드의 패턴을 알고 적용하는 것은 객체 지향 설계의 원리를 사용하여 모든 종류의 문제를 해결하는 데 도움이 될 수 있으므로 유용합니다
  • 공통 언어 정의 — 디자인 패턴은 팀이 효율적인 방식으로 의사소통하는 데 도움이 됩니다. 예를 들어, 팀원이 "이 문제를 해결하려면 팩토리 메소드를 사용해야 합니다"라고 말하면 모두가 그 의미와 제안의 동기를 이해할 것입니다.

이 기사에서는 디자인 패턴의 세 가지 범주를 다룹니다.

  • 생성 — 객체 생성에 사용됩니다
  • 구조적 — 작업 구조를 형성하기 위해 이러한 개체를 조립
  • 행동 — 해당 객체 간에 책임 할당

이러한 디자인 패턴이 실제로 어떻게 작동하는지 살펴보겠습니다!

창조적인 디자인 패턴

이름에서 알 수 있듯이 생성 패턴은 개발자가 객체를 생성하는 데 도움이 되는 다양한 방법으로 구성됩니다.

공장

팩토리 메소드는 객체 생성을 보다 효과적으로 제어할 수 있는 객체 생성 패턴입니다. 이 방법은 객체 생성 논리를 한 곳에 집중시키려는 경우에 적합합니다.

다음은 이 패턴이 실제로 작동하는 모습을 보여주는 몇 가지 샘플 코드입니다.

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

이 코드를 하나씩 분석해 보겠습니다:createCarCar

  • 각 자동차에는 회사, 모델, 크기라는 세 가지 속성이 있습니다. 또한 개체의 속성을 로그아웃하는 showDescription 함수도 정의했습니다. 또한, createCar 메소드는 메모리에서 객체를 인스턴스화할 때 어떻게 세부적으로 제어할 수 있는지 보여줍니다
  • 나중에 createCar 인스턴스를 사용하여 Challenger라는 객체를 초기화했습니다.
  • 마지막 줄에서 도전자 인스턴스에 대한 showDescription을 호출했습니다

시험해 보자! 프로그램이 새로 생성된 Car 인스턴스의 세부 정보를 로그아웃할 것으로 예상됩니다. JavaScript design patterns guide

빌더

빌더 방법을 사용하면 단계별 개체 구성을 사용하여 개체를 만들 수 있습니다. 결과적으로 이 디자인 패턴은 객체를 생성하고 필요한 기능만 적용하려는 상황에 적합합니다. 결과적으로 이는 더 큰 유연성을 허용합니다.

다음은 빌더 패턴을 사용하여 Car 객체를 생성하는 코드 블록입니다.

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

위의 코드 블록에서 수행하는 작업은 다음과 같습니다.

  • 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.

위 내용은 JavaScript 디자인 패턴 가이드의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.