เชี่ยวชาญ JavaScript design patterns ด้วยคู่มือฉบับสมบูรณ์ของเรา เรียนรู้รูปแบบ creational, structural, และ behavioral พร้อมตัวอย่างโค้ดที่ใช้งานได้จริง
JavaScript Design Patterns: คู่มือการนำไปใช้งานฉบับสมบูรณ์สำหรับนักพัฒนายุคใหม่
บทนำ: พิมพ์เขียวสำหรับโค้ดที่แข็งแกร่ง
ในโลกของการพัฒนาซอฟต์แวร์ที่มีการเปลี่ยนแปลงอยู่เสมอ การเขียนโค้ดที่แค่ทำงานได้เป็นเพียงก้าวแรกเท่านั้น ความท้าทายที่แท้จริงและเป็นเครื่องหมายของนักพัฒนามืออาชีพ คือการสร้างโค้ดที่สามารถขยายขนาด (scalable), บำรุงรักษาได้ (maintainable) และง่ายต่อการทำความเข้าใจและทำงานร่วมกันของผู้อื่น นี่คือจุดที่ design patterns เข้ามามีบทบาท มันไม่ใช่ thuật toán หรือ library ที่เฉพาะเจาะจง แต่เป็นพิมพ์เขียวระดับสูงที่ไม่ขึ้นอยู่กับภาษาใดภาษาหนึ่ง สำหรับการแก้ปัญหาที่เกิดขึ้นซ้ำๆ ในสถาปัตยกรรมซอฟต์แวร์
สำหรับนักพัฒนา JavaScript การทำความเข้าใจและการประยุกต์ใช้ design patterns มีความสำคัญมากกว่าที่เคยเป็นมา เนื่องจากแอปพลิเคชันมีความซับซ้อนเพิ่มขึ้น ตั้งแต่ front-end framework ที่ซับซ้อนไปจนถึง backend service ที่ทรงพลังบน Node.js รากฐานทางสถาปัตยกรรมที่มั่นคงจึงเป็นสิ่งที่ขาดไม่ได้ Design patterns เป็นผู้มอบรากฐานนี้ โดยนำเสนอโซลูชันที่ผ่านการทดสอบมาแล้ว ซึ่งส่งเสริมการพึ่งพากันน้อย (loose coupling), การแบ่งแยกหน้าที่ (separation of concerns) และการนำโค้ดกลับมาใช้ใหม่ (code reusability)
คู่มือฉบับสมบูรณ์นี้จะพาคุณไปทำความรู้จักกับ design patterns ทั้งสามหมวดหมู่พื้นฐาน พร้อมคำอธิบายที่ชัดเจนและตัวอย่างการนำไปใช้งานด้วย JavaScript สมัยใหม่ (ES6+) ที่ใช้ได้จริง เป้าหมายของเราคือการมอบความรู้ให้คุณสามารถระบุได้ว่าควรใช้ pattern ใดสำหรับปัญหาที่กำหนด และจะนำไปใช้อย่างมีประสิทธิภาพในโปรเจกต์ของคุณได้อย่างไร
สามเสาหลักของ Design Patterns
โดยทั่วไป Design patterns จะถูกจัดแบ่งออกเป็นสามกลุ่มหลัก โดยแต่ละกลุ่มจะจัดการกับความท้าทายทางสถาปัตยกรรมที่แตกต่างกันไป:
- Creational Patterns: รูปแบบเหล่านี้เน้นที่กลไกการสร้างอ็อบเจกต์ โดยพยายามสร้างอ็อบเจกต์ในลักษณะที่เหมาะสมกับสถานการณ์ ช่วยเพิ่มความยืดหยุ่นและการนำโค้ดที่มีอยู่กลับมาใช้ใหม่
- Structural Patterns: รูปแบบเหล่านี้เกี่ยวข้องกับการประกอบอ็อบเจกต์ อธิบายวิธีการรวมอ็อบเจกต์และคลาสเข้าด้วยกันเป็นโครงสร้างที่ใหญ่ขึ้น ในขณะที่ยังคงรักษาความยืดหยุ่นและประสิทธิภาพของโครงสร้างเหล่านั้นไว้
- Behavioral Patterns: รูปแบบเหล่านี้เกี่ยวข้องกับอัลกอริทึมและการมอบหมายความรับผิดชอบระหว่างอ็อบเจกต์ โดยจะอธิบายว่าอ็อบเจกต์มีปฏิสัมพันธ์และกระจายความรับผิดชอบกันอย่างไร
เรามาเจาะลึกแต่ละหมวดหมู่พร้อมตัวอย่างที่ใช้งานได้จริงกัน
Creational Patterns: การสร้างอ็อบเจกต์อย่างเชี่ยวชาญ
Creational patterns นำเสนอกลไกการสร้างอ็อบเจกต์ที่หลากหลาย ซึ่งช่วยเพิ่มความยืดหยุ่นและการนำโค้ดที่มีอยู่กลับมาใช้ใหม่ ช่วยลดการผูกมัดของระบบกับวิธีการสร้าง, ประกอบ และแสดงผลอ็อบเจกต์
The Singleton Pattern
แนวคิด: Singleton pattern ทำให้แน่ใจว่าคลาสมีอินสแตนซ์เพียงหนึ่งเดียวและมีจุดเข้าถึงแบบ global เพียงจุดเดียว ไม่ว่าจะพยายามสร้างอินสแตนซ์ใหม่กี่ครั้ง ก็จะได้รับอินสแตนซ์เดิมกลับไปเสมอ
กรณีการใช้งานทั่วไป: รูปแบบนี้มีประโยชน์สำหรับการจัดการทรัพยากรหรือ state ที่ใช้ร่วมกัน ตัวอย่างเช่น database connection pool, ตัวจัดการการตั้งค่าส่วนกลาง (global configuration manager) หรือ logging service ที่ควรเป็นหนึ่งเดียวกันทั้งแอปพลิเคชัน
การนำไปใช้ใน JavaScript: JavaScript สมัยใหม่ โดยเฉพาะคลาสใน ES6 ทำให้การสร้าง Singleton เป็นเรื่องง่าย เราสามารถใช้ static property บนคลาสเพื่อเก็บอินสแตนซ์เดียวไว้ได้
ตัวอย่าง: Logger Service Singleton
class Logger { constructor() { if (Logger.instance) { return Logger.instance; } this.logs = []; Logger.instance = this; } log(message) { const timestamp = new Date().toISOString(); this.logs.push({ message, timestamp }); console.log(`${timestamp} - ${message}`); } getLogCount() { return this.logs.length; } } // แม้จะเรียกใช้ 'new' แต่ตรรกะใน constructor จะทำให้แน่ใจว่ามีเพียงอินสแตนซ์เดียว const logger1 = new Logger(); const logger2 = new Logger(); console.log("Are loggers the same instance?", logger1 === logger2); // true logger1.log("First message from logger1."); logger2.log("Second message from logger2."); console.log("Total logs:", logger1.getLogCount()); // 2
ข้อดีและข้อเสีย:
- ข้อดี: รับประกันว่ามีอินสแตนซ์เดียว, มีจุดเข้าถึงแบบ global และช่วยประหยัดทรัพยากรโดยหลีกเลี่ยงการสร้างอินสแตนซ์ของอ็อบเจกต์หนักๆ หลายครั้ง
- ข้อเสีย: อาจถูกมองว่าเป็น anti-pattern เนื่องจากสร้าง global state ซึ่งทำให้การทำ unit test เป็นเรื่องยาก และยังทำให้โค้ดผูกมัดกับอินสแตนซ์ของ Singleton อย่างแน่นหนา ซึ่งขัดกับหลักการ dependency injection
The Factory Pattern
แนวคิด: Factory pattern มีอินเทอร์เฟซสำหรับสร้างอ็อบเจกต์ใน superclass แต่ยอมให้ subclass เปลี่ยนแปลงประเภทของอ็อบเจกต์ที่จะถูกสร้างขึ้นได้ มันคือการใช้เมธอดหรือคลาส "factory" ที่ออกแบบมาโดยเฉพาะเพื่อสร้างอ็อบเจกต์โดยไม่ต้องระบุ concrete class ของมัน
กรณีการใช้งานทั่วไป: เมื่อคุณมีคลาสที่ไม่สามารถคาดเดาประเภทของอ็อบเจกต์ที่ต้องสร้างได้ หรือเมื่อคุณต้องการให้ผู้ใช้ library ของคุณสามารถสร้างอ็อบเจกต์ได้โดยไม่จำเป็นต้องรู้รายละเอียดการทำงานภายใน ตัวอย่างทั่วไปคือการสร้างผู้ใช้ประเภทต่างๆ (Admin, Member, Guest) ตามพารามิเตอร์
การนำไปใช้ใน JavaScript:
ตัวอย่าง: A User Factory
class RegularUser { constructor(name) { this.name = name; this.role = 'Regular'; } viewDashboard() { console.log(`${this.name} is viewing the user dashboard.`); } } class AdminUser { constructor(name) { this.name = name; this.role = 'Admin'; } viewDashboard() { console.log(`${this.name} is viewing the admin dashboard with full privileges.`); } } class UserFactory { static createUser(type, name) { switch (type.toLowerCase()) { case 'admin': return new AdminUser(name); case 'regular': return new RegularUser(name); default: throw new Error('Invalid user type specified.'); } } } const admin = UserFactory.createUser('admin', 'Alice'); const regularUser = UserFactory.createUser('regular', 'Bob'); admin.viewDashboard(); // Alice is viewing the admin dashboard... regularUser.viewDashboard(); // Bob is viewing the user dashboard. console.log(admin.role); // Admin console.log(regularUser.role); // Regular
ข้อดีและข้อเสีย:
- ข้อดี: ส่งเสริม loose coupling โดยการแยก client code ออกจาก concrete classes ทำให้โค้ดขยายได้ง่ายขึ้น เนื่องจากการเพิ่มประเภทผลิตภัณฑ์ใหม่ทำได้เพียงแค่สร้างคลาสใหม่และอัปเดต factory
- ข้อเสีย: อาจทำให้เกิดคลาสจำนวนมากหากต้องการผลิตภัณฑ์หลายประเภท ซึ่งทำให้ codebase ซับซ้อนขึ้น
The Prototype Pattern
แนวคิด: Prototype pattern คือการสร้างอ็อบเจกต์ใหม่โดยการคัดลอกอ็อบเจกต์ที่มีอยู่แล้ว ซึ่งเรียกว่า "prototype" แทนที่จะสร้างอ็อบเจกต์ตั้งแต่ต้น คุณจะสร้างโคลนของอ็อบเจกต์ที่กำหนดค่าไว้ล่วงหน้า นี่เป็นพื้นฐานของวิธีการทำงานของ JavaScript ผ่าน prototypal inheritance
กรณีการใช้งานทั่วไป: รูปแบบนี้มีประโยชน์เมื่อต้นทุนในการสร้างอ็อบเจกต์แพงหรือซับซ้อนกว่าการคัดลอกอ็อบเจกต์ที่มีอยู่ นอกจากนี้ยังใช้เพื่อสร้างอ็อบเจกต์ที่ประเภทถูกระบุในขณะ runtime
การนำไปใช้ใน JavaScript: JavaScript มีการสนับสนุนรูปแบบนี้ในตัวผ่าน `Object.create()`
ตัวอย่าง: Clonable Vehicle Prototype
const vehiclePrototype = { init: function(model) { this.model = model; }, getModel: function() { return `The model of this vehicle is ${this.model}`; } }; // สร้างอ็อบเจกต์รถยนต์ใหม่จาก vehicle prototype const car = Object.create(vehiclePrototype); car.init('Ford Mustang'); console.log(car.getModel()); // The model of this vehicle is Ford Mustang // สร้างอ็อบเจกต์อีกอัน เป็นรถบรรทุก const truck = Object.create(vehiclePrototype); truck.init('Tesla Cybertruck'); console.log(truck.getModel()); // The model of this vehicle is Tesla Cybertruck
ข้อดีและข้อเสีย:
- ข้อดี: สามารถเพิ่มประสิทธิภาพได้อย่างมากสำหรับการสร้างอ็อบเจกต์ที่ซับซ้อน ช่วยให้คุณสามารถเพิ่มหรือลบคุณสมบัติจากอ็อบเจกต์ในขณะ runtime ได้
- ข้อเสีย: การสร้างโคลนของอ็อบเจกต์ที่มีการอ้างอิงแบบวงกลม (circular references) อาจเป็นเรื่องยุ่งยาก อาจจำเป็นต้องใช้ deep copy ซึ่งอาจซับซ้อนในการสร้างให้ถูกต้อง
Structural Patterns: การประกอบโค้ดอย่างชาญฉลาด
Structural patterns เกี่ยวข้องกับวิธีการรวมอ็อบเจกต์และคลาสเพื่อสร้างโครงสร้างที่ใหญ่และซับซ้อนขึ้น โดยเน้นการทำให้โครงสร้างง่ายขึ้นและระบุความสัมพันธ์
The Adapter Pattern
แนวคิด: Adapter pattern ทำหน้าที่เป็นสะพานเชื่อมระหว่างสองอินเทอร์เฟซที่เข้ากันไม่ได้ ประกอบด้วยคลาสเดียว (adapter) ที่เชื่อมต่อฟังก์ชันการทำงานของอินเทอร์เฟซที่ไม่ขึ้นต่อกันหรือเข้ากันไม่ได้ ลองนึกถึงอะแดปเตอร์ปลั๊กไฟที่ช่วยให้คุณเสียบอุปกรณ์ของคุณเข้ากับเต้ารับไฟฟ้าในต่างประเทศได้
กรณีการใช้งานทั่วไป: การรวม library ของบุคคลที่สามเข้ากับแอปพลิเคชันที่มีอยู่ที่คาดหวัง API ที่แตกต่างกัน หรือการทำให้ legacy code ทำงานกับระบบที่ทันสมัยโดยไม่ต้องเขียน legacy code ใหม่
การนำไปใช้ใน JavaScript:
ตัวอย่าง: การปรับ API ใหม่ให้เข้ากับอินเทอร์เฟซเก่า
// อินเทอร์เฟซเก่าที่แอปพลิเคชันของเราใช้งานอยู่ class OldCalculator { operation(term1, term2, operation) { switch (operation) { case 'add': return term1 + term2; case 'sub': return term1 - term2; default: return NaN; } } } // library ใหม่ที่มีอินเทอร์เฟซแตกต่างออกไป class NewCalculator { add(term1, term2) { return term1 + term2; } subtract(term1, term2) { return term1 - term2; } } // คลาส Adapter class CalculatorAdapter { constructor() { this.calculator = new NewCalculator(); } operation(term1, term2, operation) { switch (operation) { case 'add': // ปรับการเรียกใช้ให้เข้ากับอินเทอร์เฟซใหม่ return this.calculator.add(term1, term2); case 'sub': return this.calculator.subtract(term1, term2); default: return NaN; } } } // Client code สามารถใช้ adapter ได้ราวกับว่าเป็นเครื่องคิดเลขเก่า const oldCalc = new OldCalculator(); console.log("Old calculator result:", oldCalc.operation(10, 5, 'add')); // 15 const adaptedCalc = new CalculatorAdapter(); console.log("Adapted calculator result:", adaptedCalc.operation(10, 5, 'add')); // 15
ข้อดีและข้อเสีย:
- ข้อดี: แยก client ออกจากการ υλοποίηση ของ target interface ทำให้สามารถใช้การ υλοποίηση ที่แตกต่างกันสลับกันได้ เพิ่มความสามารถในการนำโค้ดกลับมาใช้ใหม่
- ข้อเสีย: อาจเพิ่มความซับซ้อนอีกชั้นหนึ่งให้กับโค้ด
The Decorator Pattern
แนวคิด: Decorator pattern ช่วยให้คุณสามารถเพิ่มพฤติกรรมหรือความรับผิดชอบใหม่ๆ ให้กับอ็อบเจกต์ได้แบบไดนามิกโดยไม่ต้องแก้ไขโค้ดดั้งเดิม ทำได้โดยการห่อหุ้มอ็อบเจกต์ดั้งเดิมด้วยอ็อบเจกต์ "decorator" พิเศษที่มีฟังก์ชันการทำงานใหม่
กรณีการใช้งานทั่วไป: การเพิ่มคุณสมบัติให้กับ UI component, การเพิ่มสิทธิ์ให้กับอ็อบเจกต์ผู้ใช้ หรือการเพิ่มพฤติกรรมการบันทึก/แคชให้กับ service เป็นทางเลือกที่ยืดหยุ่นแทนการทำ subclassing
การนำไปใช้ใน JavaScript: ฟังก์ชันเป็น first-class citizen ใน JavaScript ทำให้การสร้าง decorator เป็นเรื่องง่าย
ตัวอย่าง: การตกแต่งรายการสั่งกาแฟ
// ส่วนประกอบพื้นฐาน class SimpleCoffee { getCost() { return 10; } getDescription() { return 'Simple coffee'; } } // Decorator 1: นม function MilkDecorator(coffee) { const originalCost = coffee.getCost(); const originalDescription = coffee.getDescription(); coffee.getCost = function() { return originalCost + 2; }; coffee.getDescription = function() { return `${originalDescription}, with milk`; }; return coffee; } // Decorator 2: น้ำตาล function SugarDecorator(coffee) { const originalCost = coffee.getCost(); const originalDescription = coffee.getDescription(); coffee.getCost = function() { return originalCost + 1; }; coffee.getDescription = function() { return `${originalDescription}, with sugar`; }; return coffee; } // มาสร้างและตกแต่งกาแฟกัน let myCoffee = new SimpleCoffee(); console.log(myCoffee.getCost(), myCoffee.getDescription()); // 10, Simple coffee myCoffee = MilkDecorator(myCoffee); console.log(myCoffee.getCost(), myCoffee.getDescription()); // 12, Simple coffee, with milk myCoffee = SugarDecorator(myCoffee); console.log(myCoffee.getCost(), myCoffee.getDescription()); // 13, Simple coffee, with milk, with sugar
ข้อดีและข้อเสีย:
- ข้อดี: มีความยืดหยุ่นสูงในการเพิ่มความรับผิดชอบให้กับอ็อบเจกต์ในขณะ runtime หลีกเลี่ยงคลาสที่เต็มไปด้วยฟีเจอร์ที่ไม่จำเป็นในลำดับชั้นบนๆ
- ข้อเสีย: อาจส่งผลให้มีอ็อบเจกต์ขนาดเล็กจำนวนมาก ลำดับของ decorator อาจมีความสำคัญ ซึ่งอาจไม่ชัดเจนสำหรับ client
The Facade Pattern
แนวคิด: Facade pattern จัดเตรียมอินเทอร์เฟซระดับสูงที่เรียบง่ายให้กับระบบย่อยที่ซับซ้อนซึ่งประกอบด้วยคลาส, library หรือ API ต่างๆ ช่วยซ่อนความซับซ้อนที่อยู่เบื้องหลังและทำให้ระบบย่อยใช้งานง่ายขึ้น
กรณีการใช้งานทั่วไป: การสร้าง API ที่เรียบง่ายสำหรับชุดการทำงานที่ซับซ้อน เช่น กระบวนการชำระเงินใน e-commerce ที่เกี่ยวข้องกับระบบย่อยของสต็อกสินค้า, การชำระเงิน และการจัดส่ง อีกตัวอย่างหนึ่งคือเมธอดเดียวสำหรับเริ่มเว็บแอปพลิเคชันซึ่งภายในจะตั้งค่าเซิร์ฟเวอร์, ฐานข้อมูล และ middleware
การนำไปใช้ใน JavaScript:
ตัวอย่าง: A Mortgage Application Facade
// ระบบย่อยที่ซับซ้อน class BankService { verify(name, amount) { console.log(`Verifying sufficient funds for ${name} for amount ${amount}`); return amount < 100000; } } class CreditHistoryService { get(name) { console.log(`Checking credit history for ${name}`); // จำลองว่ามีคะแนนเครดิตดี return true; } } class BackgroundCheckService { run(name) { console.log(`Running background check for ${name}`); return true; } } // The Facade class MortgageFacade { constructor() { this.bank = new BankService(); this.credit = new CreditHistoryService(); this.background = new BackgroundCheckService(); } applyFor(name, amount) { console.log(`--- Applying for mortgage for ${name} ---`); const isEligible = this.bank.verify(name, amount) && this.credit.get(name) && this.background.run(name); const result = isEligible ? 'Approved' : 'Rejected'; console.log(`--- Application result for ${name}: ${result} ---\n`); return result; } } // Client code โต้ตอบกับ Facade ที่เรียบง่าย const mortgage = new MortgageFacade(); mortgage.applyFor('John Smith', 75000); // Approved mortgage.applyFor('Jane Doe', 150000); // Rejected
ข้อดีและข้อเสีย:
- ข้อดี: แยก client ออกจากกลไกการทำงานภายในที่ซับซ้อนของระบบย่อย ทำให้โค้ดอ่านง่ายและบำรุงรักษาได้ดีขึ้น
- ข้อเสีย: facade อาจกลายเป็น "god object" ที่ผูกติดกับทุกคลาสของระบบย่อย และไม่ได้ป้องกันไม่ให้ client เข้าถึงคลาสของระบบย่อยโดยตรงหากต้องการความยืดหยุ่นมากขึ้น
Behavioral Patterns: การควบคุมการสื่อสารของอ็อบเจกต์
Behavioral patterns เกี่ยวข้องกับวิธีที่อ็อบเจกต์สื่อสารกัน โดยเน้นการกำหนดความรับผิดชอบและการจัดการปฏิสัมพันธ์อย่างมีประสิทธิภาพ
The Observer Pattern
แนวคิด: Observer pattern กำหนดความสัมพันธ์แบบ one-to-many ระหว่างอ็อบเจกต์ เมื่ออ็อบเจกต์หนึ่ง (เรียกว่า "subject" หรือ "observable") เปลี่ยนสถานะ อ็อบเจกต์ทั้งหมดที่ขึ้นอยู่กับมัน (เรียกว่า "observers") จะได้รับการแจ้งเตือนและอัปเดตโดยอัตโนมัติ
กรณีการใช้งานทั่วไป: รูปแบบนี้เป็นรากฐานของการเขียนโปรแกรมเชิงเหตุการณ์ (event-driven programming) ใช้กันอย่างแพร่หลายในการพัฒนา UI (DOM event listeners), library การจัดการ state (เช่น Redux หรือ Vuex) และระบบส่งข้อความ
การนำไปใช้ใน JavaScript:
ตัวอย่าง: สำนักข่าวและผู้ติดตาม
// The Subject (Observable) class NewsAgency { constructor() { this.subscribers = []; } subscribe(subscriber) { this.subscribers.push(subscriber); console.log(`${subscriber.name} has subscribed.`); } unsubscribe(subscriber) { this.subscribers = this.subscribers.filter(sub => sub !== subscriber); console.log(`${subscriber.name} has unsubscribed.`); } notify(news) { console.log(`--- NEWS AGENCY: Broadcasting news: "${news}" ---`); this.subscribers.forEach(subscriber => subscriber.update(news)); } } // The Observer class Subscriber { constructor(name) { this.name = name; } update(news) { console.log(`${this.name} received the latest news: "${news}"`); } } const agency = new NewsAgency(); const sub1 = new Subscriber('Reader A'); const sub2 = new Subscriber('Reader B'); const sub3 = new Subscriber('Reader C'); agency.subscribe(sub1); agency.subscribe(sub2); agency.notify('Global markets are up!'); agency.subscribe(sub3); agency.unsubscribe(sub2); agency.notify('New tech breakthrough announced!');
ข้อดีและข้อเสีย:
- ข้อดี: ส่งเสริม loose coupling ระหว่าง subject และ observers ของมัน subject ไม่จำเป็นต้องรู้อะไรเกี่ยวกับ observers นอกจากว่าพวกมัน υλοποίηση observer interface สนับสนุนการสื่อสารแบบ broadcast
- ข้อเสีย: Observers จะได้รับการแจ้งเตือนในลำดับที่คาดเดาไม่ได้ อาจนำไปสู่ปัญหาด้านประสิทธิภาพหากมี observers จำนวนมากหรือหากตรรกะการอัปเดตมีความซับซ้อน
The Strategy Pattern
แนวคิด: Strategy pattern กำหนดตระกูลของอัลกอริทึมที่สามารถสับเปลี่ยนกันได้ และห่อหุ้มแต่ละอัลกอริทึมไว้ในคลาสของตัวเอง ซึ่งช่วยให้อัลกอริทึมสามารถถูกเลือกและสลับได้ในขณะ runtime โดยไม่ขึ้นอยู่กับ client ที่ใช้งาน
กรณีการใช้งานทั่วไป: การ υλοποίηση อัลกอริทึมการเรียงลำดับที่แตกต่างกัน, กฎการตรวจสอบความถูกต้อง หรือวิธีการคำนวณค่าจัดส่งสำหรับเว็บไซต์ e-commerce (เช่น อัตราคงที่, ตามน้ำหนัก, ตามปลายทาง)
การนำไปใช้ใน JavaScript:
ตัวอย่าง: กลยุทธ์การคำนวณค่าจัดส่ง
// The Context class Shipping { constructor() { this.company = null; } setStrategy(company) { this.company = company; console.log(`Shipping strategy set to: ${company.constructor.name}`); } calculate(pkg) { if (!this.company) { throw new Error('Shipping strategy has not been set.'); } return this.company.calculate(pkg); } } // The Strategies class FedExStrategy { calculate(pkg) { // การคำนวณที่ซับซ้อนตามน้ำหนัก ฯลฯ const cost = pkg.weight * 2.5 + 5; console.log(`FedEx cost for package of ${pkg.weight}kg is $${cost}`); return cost; } } class UPSStrategy { calculate(pkg) { const cost = pkg.weight * 2.1 + 4; console.log(`UPS cost for package of ${pkg.weight}kg is $${cost}`); return cost; } } class PostalServiceStrategy { calculate(pkg) { const cost = pkg.weight * 1.8; console.log(`Postal Service cost for package of ${pkg.weight}kg is $${cost}`); return cost; } } const shipping = new Shipping(); const packageA = { from: 'New York', to: 'London', weight: 5 }; shipping.setStrategy(new FedExStrategy()); shipping.calculate(packageA); shipping.setStrategy(new UPSStrategy()); shipping.calculate(packageA); shipping.setStrategy(new PostalServiceStrategy()); shipping.calculate(packageA);
ข้อดีและข้อเสีย:
- ข้อดี: เป็นทางเลือกที่สะอาดแทนคำสั่ง `if/else` หรือ `switch` ที่ซับซ้อน ห่อหุ้มอัลกอริทึม ทำให้ง่ายต่อการทดสอบและบำรุงรักษา
- ข้อเสีย: อาจเพิ่มจำนวนอ็อบเจกต์ในแอปพลิเคชัน Client ต้องทราบถึงกลยุทธ์ต่างๆ เพื่อเลือกกลยุทธ์ที่เหมาะสม
รูปแบบสมัยใหม่และข้อควรพิจารณาทางสถาปัตยกรรม
แม้ว่า design patterns แบบคลาสสิกจะอยู่เหนือกาลเวลา แต่ระบบนิเวศของ JavaScript ได้พัฒนาขึ้น ทำให้เกิดการตีความที่ทันสมัยและรูปแบบสถาปัตยกรรมขนาดใหญ่ซึ่งมีความสำคัญสำหรับนักพัฒนายุคปัจจุบัน
The Module Pattern
Module pattern เป็นหนึ่งในรูปแบบที่แพร่หลายที่สุดใน JavaScript ยุคก่อน ES6 สำหรับการสร้าง scope แบบ private และ public โดยใช้ closures เพื่อห่อหุ้ม state และ behavior ปัจจุบัน รูปแบบนี้ถูกแทนที่โดย ES6 Modules (`import`/`export`) ซึ่งเป็นระบบโมดูลมาตรฐานแบบไฟล์ การทำความเข้าใจ ES6 modules เป็นพื้นฐานสำหรับนักพัฒนา JavaScript สมัยใหม่ทุกคน เนื่องจากเป็นมาตรฐานสำหรับการจัดระเบียบโค้ดทั้งในแอปพลิเคชันฝั่ง front-end และ back-end
Architectural Patterns (MVC, MVVM)
สิ่งสำคัญคือต้องแยกความแตกต่างระหว่าง design patterns และ architectural patterns ในขณะที่ design patterns แก้ปัญหาเฉพาะจุด แต่ architectural patterns จะให้โครงสร้างระดับสูงสำหรับทั้งแอปพลิเคชัน
- MVC (Model-View-Controller): รูปแบบที่แบ่งแอปพลิเคชันออกเป็นสามส่วนประกอบที่เชื่อมต่อกัน: the Model (ข้อมูลและ business logic), the View (the UI), และ the Controller (จัดการ input ของผู้ใช้และอัปเดต Model/View) เฟรมเวิร์กเช่น Ruby on Rails และ Angular เวอร์ชันเก่าทำให้รูปแบบนี้เป็นที่นิยม
- MVVM (Model-View-ViewModel): คล้ายกับ MVC แต่มี ViewModel ที่ทำหน้าที่เป็นตัวผูกระหว่าง Model และ View โดย ViewModel จะเปิดเผยข้อมูลและคำสั่ง และ View จะอัปเดตโดยอัตโนมัติด้วย data-binding รูปแบบนี้เป็นหัวใจสำคัญของเฟรมเวิร์กสมัยใหม่เช่น Vue.js และมีอิทธิพลในสถาปัตยกรรมแบบคอมโพเนนต์ของ React
เมื่อทำงานกับเฟรมเวิร์กเช่น React, Vue หรือ Angular คุณกำลังใช้ architectural patterns เหล่านี้โดยธรรมชาติ ซึ่งมักจะผสมผสานกับ design patterns ขนาดเล็ก (เช่น Observer pattern สำหรับการจัดการ state) เพื่อสร้างแอปพลิเคชันที่แข็งแกร่ง
สรุป: การใช้ Patterns อย่างชาญฉลาด
JavaScript design patterns ไม่ใช่กฎที่ตายตัว แต่เป็นเครื่องมือที่ทรงพลังในคลังอาวุธของนักพัฒนา มันแสดงถึงภูมิปัญญาที่สั่งสมมาของชุมชนวิศวกรรมซอฟต์แวร์ ซึ่งนำเสนอโซลูชันที่สวยงามสำหรับปัญหาทั่วไป
กุญแจสำคัญในการเรียนรู้คือการไม่ท่องจำทุกรูปแบบ แต่ให้ทำความเข้าใจ ปัญหา ที่แต่ละรูปแบบแก้ไข เมื่อคุณเผชิญกับความท้าทายในโค้ดของคุณ ไม่ว่าจะเป็นการผูกมัดที่แน่นหนา, การสร้างอ็อบเจกต์ที่ซับซ้อน หรืออัลกอริทึมที่ไม่ยืดหยุ่น คุณก็จะสามารถเลือกใช้รูปแบบที่เหมาะสมเป็นโซลูชันที่กำหนดไว้อย่างดีได้
คำแนะนำสุดท้ายของเราคือ: เริ่มต้นด้วยการเขียนโค้ดที่ง่ายที่สุดที่ทำงานได้ เมื่อแอปพลิเคชันของคุณพัฒนาขึ้น ให้ refactor โค้ดของคุณไปสู่รูปแบบเหล่านี้ในจุดที่เหมาะสม อย่าบังคับใช้รูปแบบในที่ที่ไม่จำเป็น การนำไปใช้อย่างรอบคอบจะทำให้คุณเขียนโค้ดที่ไม่เพียงแต่ใช้งานได้ แต่ยังสะอาด, ขยายขนาดได้ และง่ายต่อการบำรุงรักษาไปอีกหลายปี