ホームページ >ウェブフロントエンド >jsチュートリアル >JavaScript デザインパターンガイド
フセイン・アリフ著✏️
建築家のグループが超高層ビルを設計したい状況を想像してください。設計段階では、次のような多くの要素を考慮する必要があります。
考慮すべき要素はたくさんありますが、確実に言えることが 1 つあります。この超高層ビルの建設に役立つ青写真がすでに存在する可能性が高いということです。共通の設計や計画がなければ、建築家は車輪の再発明を行う必要があり、混乱や複数の非効率が生じる可能性があります。
プログラミングの世界でも同様に、開発者はクリーンなコードの原則に従いながらソフトウェアを構築するために一連の設計パターンを参照することがよくあります。さらに、これらのパターンは遍在しているため、プログラマーは毎回車輪の再発明を行うのではなく、新しい機能のリリースに集中できます。
この記事では、一般的に使用されるいくつかの JavaScript デザイン パターンについて学び、一緒に小さな Node.js プロジェクトを構築して、各デザイン パターンの使用法を説明します。
設計パターンは、開発者がコーディング中に繰り返される設計上の問題を解決するために調整できる、事前に作成された青写真です。覚えておくべき重要な点の 1 つは、これらのブループリントはコードの断片ではなく、今後の課題に対処するための一般的な概念であるということです。
デザイン パターンには多くの利点があります:
この記事では、デザイン パターンの 3 つのカテゴリについて説明します。
これらのデザイン パターンを実際に見てみましょう!
名前が示すように、作成パターンは、開発者がオブジェクトを作成するのに役立つさまざまなメソッドで構成されます。
ファクトリ メソッドは、オブジェクトの作成をより詳細に制御できるようにするオブジェクトを作成するためのパターンです。この方法は、オブジェクト作成のロジックを 1 か所に集中させたい場合に適しています。
このパターンの動作を示すサンプル コードを次に示します。
//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
試してみましょう!プログラムが新しく作成した Car インスタンスの詳細をログアウトすることを期待する必要があります:
ビルダー メソッドを使用すると、段階的なオブジェクト構築を使用してオブジェクトを構築できます。そのため、このデザイン パターンは、オブジェクトを作成し、必要な機能のみを適用したい場合に最適です。その結果、柔軟性が向上します。
これは、ビルダー パターンを使用して 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();
上記のコード ブロックで行っていることは次のとおりです:
The expected output should be the properties of the challenger object before and after we reduced its size by four units: This confirms that our builder pattern implementation in JavaScript was successful!
Structural design patterns focus on how different components of our program work together.
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:
When this code is run, we expect our illit object to be part of the groupsWithSoldAlbums list:
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:
The output of the code should confirm that we successfully added new capabilities to our music band via the PerformingArtist class:
This category focuses on how different components in a program communicate with each other.
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: 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:
Additionally, each Leader will have two functions:
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:
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:
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:
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.
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.
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 中国語 Web サイトの他の関連記事を参照してください。