คู่มือฉบับสมบูรณ์เกี่ยวกับรูปแบบการออกแบบ JavaScript
เผยแพร่แล้ว: 2022-03-11ในฐานะนักพัฒนา JavaScript ที่ดี คุณต้องพยายามเขียนโค้ดที่สะอาด สมบูรณ์ และสามารถบำรุงรักษาได้ คุณแก้ปัญหาที่น่าสนใจซึ่งถึงแม้จะไม่เหมือนใคร แต่ก็ไม่ได้ต้องการโซลูชันที่ไม่เหมือนใคร คุณอาจพบว่าตัวเองกำลังเขียนโค้ดที่คล้ายกับวิธีแก้ปัญหาที่คุณเคยจัดการมาก่อน คุณอาจไม่ทราบ แต่คุณเคยใช้ รูปแบบการออกแบบ JavaScript รูปแบบการออกแบบเป็นวิธีแก้ปัญหาแบบใช้ซ้ำได้สำหรับปัญหาที่เกิดขึ้นโดยทั่วไปในการออกแบบซอฟต์แวร์
ในช่วงอายุการใช้งานของภาษาใดๆ ก็ตาม โซลูชันที่นำกลับมาใช้ใหม่ได้จำนวนมากนั้นถูกสร้างขึ้นและทดสอบโดยนักพัฒนาจำนวนมากจากชุมชนของภาษานั้น เป็นเพราะประสบการณ์ที่ผสมผสานกันของนักพัฒนาหลายคนที่โซลูชันดังกล่าวมีประโยชน์มากเพราะช่วยให้เราเขียนโค้ดด้วยวิธีที่เหมาะสมที่สุดในขณะเดียวกันก็แก้ปัญหาในมือได้
ประโยชน์หลักที่เราได้รับจากรูปแบบการออกแบบมีดังนี้:
- สิ่งเหล่านี้เป็นวิธีแก้ปัญหาที่ได้รับการพิสูจน์แล้ว เนื่องจากรูปแบบการออกแบบมักถูกใช้โดยนักพัฒนาหลายๆ คน คุณจึงมั่นใจได้ว่ารูปแบบเหล่านี้ใช้ได้ และไม่เพียงเท่านั้น คุณยังสามารถมั่นใจได้ว่ามีการแก้ไขหลายครั้งและอาจมีการปรับให้เหมาะสม
- ใช้ซ้ำได้ง่าย: รูปแบบการออกแบบบันทึกวิธีแก้ปัญหาที่นำกลับมาใช้ใหม่ได้ ซึ่งสามารถแก้ไขได้เพื่อแก้ปัญหาเฉพาะหลายอย่าง เนื่องจากไม่ได้เชื่อมโยงกับปัญหาเฉพาะ
- สื่อความหมายได้ชัดเจน: รูปแบบการออกแบบสามารถอธิบายโซลูชันขนาดใหญ่ได้ค่อนข้างหรูหรา
- พวกเขาทำให้การสื่อสารง่ายขึ้น: เมื่อนักพัฒนาคุ้นเคยกับรูปแบบการออกแบบ พวกเขาสามารถสื่อสารกันได้ง่ายขึ้นเกี่ยวกับวิธีแก้ปัญหาที่เป็นไปได้สำหรับปัญหาที่กำหนด
- พวกเขาป้องกันความจำเป็นในการจัดองค์ประกอบโค้ดใหม่: หากแอปพลิเคชันเขียนขึ้นโดยคำนึงถึงรูปแบบการออกแบบ มักจะเป็นกรณีที่คุณไม่จำเป็นต้องจัดองค์ประกอบโค้ดใหม่ในภายหลัง เนื่องจากการใช้รูปแบบการออกแบบที่ถูกต้องกับปัญหาที่กำหนดนั้นเหมาะสมที่สุดแล้ว สารละลาย.
- พวกเขาลดขนาดของ codebase: เนื่องจากรูปแบบการออกแบบมักจะเป็นโซลูชันที่หรูหราและเหมาะสมที่สุด พวกเขาจึงมักต้องการโค้ดน้อยกว่าโซลูชันอื่นๆ
ฉันรู้ว่าคุณพร้อมที่จะเข้าสู่จุดนี้แล้ว แต่ก่อนที่คุณจะเรียนรู้เกี่ยวกับรูปแบบการออกแบบทั้งหมด เรามาทบทวนพื้นฐาน JavaScript กันก่อนดีกว่า
ประวัติโดยย่อของ JavaScript
JavaScript เป็นหนึ่งในภาษาโปรแกรมที่ได้รับความนิยมมากที่สุดสำหรับการพัฒนาเว็บในปัจจุบัน ตอนแรกมันถูกสร้างเป็น "กาว" สำหรับองค์ประกอบ HTML ที่แสดงต่างๆ ซึ่งรู้จักกันในชื่อภาษาสคริปต์ฝั่งไคลเอ็นต์สำหรับหนึ่งในเว็บเบราว์เซอร์เริ่มต้น เรียกว่า Netscape Navigator ซึ่งแสดงได้เฉพาะ HTML แบบคงที่ในขณะนั้น อย่างที่คุณอาจเดาได้ ความคิดเกี่ยวกับภาษาสคริปต์ดังกล่าวทำให้เกิดสงครามเบราว์เซอร์ระหว่างผู้เล่นรายใหญ่ในอุตสาหกรรมการพัฒนาเบราว์เซอร์ในสมัยนั้น เช่น Netscape Communications (ปัจจุบันคือ Mozilla) Microsoft และอื่นๆ
ผู้เล่นรายใหญ่แต่ละคนต้องการที่จะผลักดันการใช้งานภาษาสคริปต์นี้ของตนเอง ดังนั้น Netscape จึงสร้าง JavaScript (ที่จริงแล้ว Brendan Eich ทำ) Microsoft จึงสร้าง JScript และอื่นๆ อย่างที่คุณนึกภาพได้ ความแตกต่างระหว่างการใช้งานเหล่านี้ก็เยี่ยมมาก ดังนั้นการพัฒนาเว็บเบราว์เซอร์จึงถูกสร้างขึ้นทีละเบราว์เซอร์ ด้วยสติกเกอร์ที่ดูดีที่สุดที่มาพร้อมกับหน้าเว็บ ในไม่ช้ามันก็ชัดเจนว่าเราต้องการมาตรฐาน ซึ่งเป็นโซลูชันข้ามเบราว์เซอร์ที่จะรวมกระบวนการพัฒนาให้เป็นหนึ่งเดียว และทำให้การสร้างหน้าเว็บง่ายขึ้น สิ่งที่พวกเขาคิดขึ้นมานั้นเรียกว่า ECMAScript
ECMAScript เป็นข้อกำหนดภาษาสคริปต์มาตรฐานซึ่งเบราว์เซอร์สมัยใหม่ทั้งหมดพยายามสนับสนุน และมีการใช้งานหลายอย่าง (คุณสามารถพูดภาษาถิ่น) ECMAScript หัวข้อที่ได้รับความนิยมมากที่สุดคือหัวข้อของบทความนี้ JavaScript นับตั้งแต่เปิดตัวครั้งแรก ECMAScript ได้กำหนดสิ่งสำคัญมากมายให้เป็นมาตรฐาน และสำหรับผู้ที่สนใจในรายละเอียดเฉพาะมากขึ้น มีรายการรายละเอียดของรายการที่เป็นมาตรฐานสำหรับ ECMAScript แต่ละเวอร์ชันที่มีอยู่ในวิกิพีเดีย การสนับสนุนเบราว์เซอร์สำหรับ ECMAScript เวอร์ชัน 6 (ES6) และสูงกว่านั้นยังไม่สมบูรณ์และต้องแปลงเป็น ES5 เพื่อให้ได้รับการสนับสนุนอย่างเต็มที่
JavaScript คืออะไร?
เพื่อให้เข้าใจเนื้อหาของบทความนี้อย่างถ่องแท้ เรามาทำความรู้จักกับคุณลักษณะทางภาษาที่สำคัญบางอย่างที่เราจำเป็นต้องทราบก่อนลงลึกถึงรูปแบบการออกแบบ JavaScript หากมีคนถามคุณว่า “จาวาสคริปต์คืออะไร” คุณอาจตอบที่ไหนสักแห่งในบรรทัดของ:
JavaScript เป็นภาษาการเขียนโปรแกรมเชิงวัตถุน้ำหนักเบา ตีความ แปลด้วยฟังก์ชันระดับเฟิร์สคลาสที่รู้จักกันทั่วไปว่าเป็นภาษาสคริปต์สำหรับหน้าเว็บ
คำจำกัดความดังกล่าวหมายถึงการกล่าวว่าโค้ด JavaScript มีหน่วยความจำเหลือน้อย ใช้งานง่าย และเรียนรู้ได้ง่าย โดยมีรูปแบบคล้ายกับภาษายอดนิยม เช่น C++ และ Java เป็นภาษาสคริปต์ ซึ่งหมายความว่าโค้ดของมันถูกตีความแทนการคอมไพล์ มีการรองรับรูปแบบการเขียนโปรแกรมเชิงวัตถุและเชิงฟังก์ชัน ซึ่งทำให้นักพัฒนามีความยืดหยุ่นมาก
จนถึงตอนนี้ เราได้พิจารณาคุณลักษณะทั้งหมดที่ฟังดูเหมือนภาษาอื่น ๆ ที่มีอยู่มากมาย ดังนั้น เรามาดูว่าจาวาสคริปต์เฉพาะเกี่ยวกับภาษาอื่น ๆ มีอะไรบ้าง ฉันจะเขียนคุณลักษณะบางประการและพยายามอธิบายให้ดีที่สุดว่าทำไมพวกเขาจึงสมควรได้รับความสนใจเป็นพิเศษ
JavaScript รองรับฟังก์ชั่นชั้นหนึ่ง
คุณลักษณะนี้เคยเป็นเรื่องยากสำหรับฉันที่จะเข้าใจเมื่อฉันเพิ่งเริ่มต้นด้วย JavaScript เนื่องจากฉันมาจากพื้นหลัง C/C++ JavaScript ถือว่าฟังก์ชันต่างๆ เป็นพลเมืองชั้นหนึ่ง ซึ่งหมายความว่าคุณสามารถส่งฟังก์ชันต่างๆ เป็นพารามิเตอร์ไปยังฟังก์ชันอื่นๆ ได้ เช่นเดียวกับที่คุณส่งผ่านฟังก์ชันต่างๆ
// we send in the function as an argument to be // executed from inside the calling function function performOperation(a, b, cb) { var c = a + b; cb(c); } performOperation(2, 3, function(result) { // prints out 5 console.log("The result of the operation is " + result); })
JavaScript เป็นแบบพื้นฐาน
เช่นเดียวกับภาษาเชิงวัตถุอื่นๆ JavaScript รองรับอ็อบเจ็กต์ และหนึ่งในคำศัพท์แรกที่นึกถึงเมื่อคิดถึงอ็อบเจ็กต์คือคลาสและการสืบทอด นี่เป็นจุดที่ยุ่งยากเล็กน้อย เนื่องจากภาษาไม่รองรับคลาสในรูปแบบภาษาธรรมดา แต่ใช้สิ่งที่เรียกว่าการสืบทอดแบบอิงต้นแบบหรือแบบอิงอินสแตนซ์
ตอนนี้ใน ES6 ได้มีการแนะนำ คลาส คำศัพท์ที่เป็นทางการ ซึ่งหมายความว่าเบราว์เซอร์ยังคงไม่รองรับสิ่งนี้ (หากคุณจำได้ว่าในขณะที่เขียน ECMAScript เวอร์ชันล่าสุดที่รองรับอย่างสมบูรณ์คือ 5.1) อย่างไรก็ตาม สิ่งสำคัญที่ควรทราบคือ แม้ว่าคำว่า "คลาส" จะถูกนำไปใช้ใน JavaScript แต่ก็ยังคงใช้การสืบทอดตามต้นแบบภายใต้ประทุน
การเขียนโปรแกรมแบบอิงต้นแบบเป็นรูปแบบหนึ่งของการเขียนโปรแกรมเชิงวัตถุซึ่งพฤติกรรมการนำกลับมาใช้ใหม่ (เรียกว่าการสืบทอด) จะดำเนินการผ่านกระบวนการนำวัตถุที่มีอยู่กลับมาใช้ใหม่ผ่านการมอบหมายที่ทำหน้าที่เป็นต้นแบบ เราจะเจาะลึกลงไปในรายละเอียดเพิ่มเติมเมื่อเราไปที่หัวข้อรูปแบบการออกแบบของบทความ เนื่องจากคุณลักษณะนี้ใช้ในรูปแบบการออกแบบ JavaScript จำนวนมาก
JavaScript Event Loops
หากคุณมีประสบการณ์ในการทำงานกับ JavaScript คุณจะคุ้นเคยกับคำว่า callback function อย่างแน่นอน สำหรับผู้ที่ไม่คุ้นเคยกับคำศัพท์ ฟังก์ชันเรียกกลับคือฟังก์ชันที่ส่งเป็นพารามิเตอร์ (โปรดจำไว้ว่า JavaScript ถือว่าฟังก์ชันเป็นพลเมืองชั้นหนึ่ง) ไปยังฟังก์ชันอื่นและจะดำเนินการหลังจากเกิดเหตุการณ์ไฟไหม้ โดยปกติจะใช้สำหรับสมัครรับกิจกรรมต่างๆ เช่น การคลิกเมาส์หรือการกดปุ่มบนแป้นพิมพ์
ทุกครั้งที่เหตุการณ์ซึ่งมีผู้ฟังติดอยู่ ไฟไหม้ (มิฉะนั้นเหตุการณ์จะสูญหาย) ข้อความจะถูกส่งไปยังคิวของข้อความที่กำลังประมวลผลแบบซิงโครนัสในลักษณะ FIFO (เข้าก่อน-ออกก่อน) ). สิ่งนี้เรียกว่าการ วนรอบเหตุการณ์
แต่ละข้อความในคิวมีฟังก์ชันที่เกี่ยวข้อง เมื่อข้อความถูกถอดออกจากคิว รันไทม์จะดำเนินการฟังก์ชันอย่างสมบูรณ์ก่อนที่จะประมวลผลข้อความอื่น กล่าวคือ ถ้าฟังก์ชันมีการเรียกฟังก์ชันอื่น ๆ ฟังก์ชันทั้งหมดจะถูกดำเนินการก่อนที่จะประมวลผลข้อความใหม่จากคิว สิ่งนี้เรียกว่าการดำเนินการจนเสร็จสิ้น
while (queue.waitForMessage()) { queue.processNextMessage(); }
queue.waitForMessage()
จะรอข้อความใหม่พร้อมกัน ข้อความแต่ละข้อความที่กำลังประมวลผลมีสแต็กของตัวเองและจะถูกประมวลผลจนกว่าสแต็กจะว่างเปล่า เมื่อเสร็จสิ้น ข้อความใหม่จะถูกประมวลผลจากคิว หากมี
คุณอาจเคยได้ยินว่า JavaScript ไม่มีการบล็อก ซึ่งหมายความว่าเมื่อมีการดำเนินการแบบอะซิงโครนัส โปรแกรมสามารถประมวลผลสิ่งอื่นได้ เช่น รับอินพุตของผู้ใช้ ขณะรอให้การดำเนินการแบบอะซิงโครนัสเสร็จสิ้น ไม่ได้บล็อกหลัก เธรดการดำเนินการ นี่เป็นคุณสมบัติที่มีประโยชน์มากของ JavaScript และบทความทั้งหมดสามารถเขียนได้ในหัวข้อนี้ อย่างไรก็ตาม มันอยู่นอกขอบเขตของบทความนี้
รูปแบบการออกแบบคืออะไร?
อย่างที่ฉันพูดไปก่อนหน้านี้ รูปแบบการออกแบบเป็นวิธีแก้ปัญหาแบบใช้ซ้ำได้สำหรับปัญหาที่เกิดขึ้นโดยทั่วไปในการออกแบบซอฟต์แวร์ มาดูหมวดหมู่ของรูปแบบการออกแบบกันบ้าง
รูปแบบโปรโต
หนึ่งจะสร้างรูปแบบได้อย่างไร? สมมติว่าคุณรู้จักปัญหาที่เกิดขึ้นทั่วไป และคุณมีวิธีแก้ปัญหาเฉพาะตัวสำหรับปัญหานี้ ซึ่งไม่เป็นที่รู้จักและจัดทำเป็นเอกสารทั่วโลก คุณใช้โซลูชันนี้ทุกครั้งที่พบปัญหานี้ และคุณคิดว่าใช้ซ้ำได้และชุมชนนักพัฒนาจะได้รับประโยชน์จากปัญหานี้
มันจะกลายเป็นรูปแบบทันทีหรือไม่? โชคดีที่ไม่มี บ่อยครั้ง คนๆ หนึ่งอาจมีแนวทางปฏิบัติในการเขียนโค้ดที่ดีและเข้าใจผิดบางอย่างที่ดูเหมือนรูปแบบสำหรับสิ่งหนึ่ง โดยที่จริงแล้วมันไม่ใช่รูปแบบ
คุณจะรู้ได้อย่างไรว่าสิ่งที่คุณคิดว่าคุณจำได้จริงๆ แล้วคือรูปแบบการออกแบบ?
โดยการรับฟังความคิดเห็นของนักพัฒนาคนอื่นๆ เกี่ยวกับมัน โดยรู้เกี่ยวกับกระบวนการสร้างรูปแบบนั้นเอง และทำความคุ้นเคยกับรูปแบบที่มีอยู่แล้ว มีระยะที่รูปแบบต้องผ่านก่อนที่จะกลายเป็นรูปแบบที่สมบูรณ์ และสิ่งนี้เรียกว่ารูปแบบโปรโต
รูปแบบโปรโตคือรูปแบบที่จะเป็น หาก ผ่านช่วงระยะเวลาหนึ่งของการทดสอบโดยนักพัฒนาและสถานการณ์ต่างๆ ที่รูปแบบพิสูจน์ได้ว่ามีประโยชน์และให้ผลลัพธ์ที่ถูกต้อง มีงานและเอกสารค่อนข้างมาก ซึ่งส่วนใหญ่อยู่นอกขอบเขตของบทความนี้—ที่ต้องทำเพื่อสร้างรูปแบบที่เป็นที่ยอมรับของชุมชนอย่างเต็มที่
ต่อต้านรูปแบบ
เนื่องจากรูปแบบการออกแบบแสดงถึงแนวปฏิบัติที่ดี รูปแบบการต่อต้านหมายถึงแนวปฏิบัติที่ไม่ดี
ตัวอย่างของ anti-pattern คือการปรับเปลี่ยนต้นแบบคลาส Object
ออบเจ็กต์เกือบทั้งหมดใน JavaScript สืบทอดมาจาก Object
(โปรดจำไว้ว่า JavaScript ใช้การสืบทอดตามต้นแบบ) ลองนึกภาพสถานการณ์ที่คุณเปลี่ยนต้นแบบนี้ การเปลี่ยนแปลงของต้นแบบ Object
จะเห็นได้ในออบเจ็กต์ทั้งหมดที่สืบทอดมาจากต้นแบบนี้ ซึ่งจะเป็นออบเจ็กต์ JavaScript ส่วนใหญ่ นี่คือหายนะที่รออยู่
อีกตัวอย่างหนึ่งที่คล้ายกับที่กล่าวไว้ข้างต้นคือการแก้ไขวัตถุที่คุณไม่ได้เป็นเจ้าของ ตัวอย่างนี้จะเป็นการแทนที่ฟังก์ชันจากอ็อบเจ็กต์ที่ใช้ในหลายๆ สถานการณ์ทั่วทั้งแอปพลิเคชัน หากคุณกำลังทำงานกับทีมขนาดใหญ่ ลองจินตนาการถึงความสับสนที่อาจเกิดขึ้น คุณจะพบกับการตั้งชื่อที่ขัดแย้งกัน การใช้งานที่เข้ากันไม่ได้ และฝันร้ายในการบำรุงรักษา
คล้ายกับการรู้วิธีปฏิบัติและแนวทางแก้ไขที่ดีทั้งหมดมีประโยชน์อย่างไร สิ่งสำคัญคือต้องรู้เกี่ยวกับสิ่งที่ไม่ดีด้วย ด้วยวิธีนี้ คุณจะจำพวกมันได้และหลีกเลี่ยงการทำผิดพลาดล่วงหน้า
การจัดหมวดหมู่รูปแบบการออกแบบ
รูปแบบการออกแบบสามารถจำแนกได้หลายวิธี แต่รูปแบบที่นิยมมากที่สุดมีดังต่อไปนี้:
- รูปแบบการออกแบบ ที่สร้างสรรค์
- รูปแบบการออกแบบ โครงสร้าง
- รูปแบบการออกแบบ พฤติกรรม
- รูปแบบการออกแบบ พร้อมกัน
- รูปแบบการออกแบบ สถาปัตยกรรม
รูปแบบการออกแบบที่สร้างสรรค์
รูปแบบเหล่านี้จัดการกับกลไกการสร้างวัตถุที่ปรับการสร้างวัตถุให้เหมาะสมที่สุดเมื่อเทียบกับวิธีการพื้นฐาน รูปแบบพื้นฐานของการสร้างวัตถุอาจส่งผลให้เกิดปัญหาในการออกแบบหรือเพิ่มความซับซ้อนให้กับการออกแบบ รูปแบบการออกแบบเชิงสร้างสรรค์ช่วยแก้ปัญหานี้ด้วยการควบคุมการสร้างวัตถุ รูปแบบการออกแบบที่ได้รับความนิยมบางส่วนในหมวดหมู่นี้คือ:
- วิธีโรงงาน
- โรงงานนามธรรม
- ช่างก่อสร้าง
- ต้นแบบ
- ซิงเกิลตัน
รูปแบบการออกแบบโครงสร้าง
รูปแบบเหล่านี้จัดการกับความสัมพันธ์ของวัตถุ พวกเขาทำให้แน่ใจว่าหากส่วนหนึ่งของระบบเปลี่ยนแปลงไป ทั้งระบบก็ไม่จำเป็นต้องเปลี่ยนแปลงไปพร้อมกับส่วนนั้น รูปแบบที่นิยมมากที่สุดในหมวดนี้คือ:
- อะแดปเตอร์
- สะพาน
- คอมโพสิต
- มัณฑนากร
- ซุ้ม
- ฟลายเวท
- พร็อกซี่
รูปแบบการออกแบบพฤติกรรม
รูปแบบประเภทนี้จะจดจำ นำไปใช้ และปรับปรุงการสื่อสารระหว่างอ็อบเจ็กต์ที่แตกต่างกันในระบบ ช่วยให้แน่ใจว่าส่วนต่าง ๆ ของระบบมีการซิงโครไนซ์ข้อมูล ตัวอย่างยอดนิยมของรูปแบบเหล่านี้คือ:
- สายใยความรับผิดชอบ
- สั่งการ
- ตัววนซ้ำ
- คนกลาง
- ความทรงจำ
- ผู้สังเกตการณ์
- สถานะ
- กลยุทธ์
- ผู้มาเยือน
รูปแบบการออกแบบพร้อมกัน
รูปแบบการออกแบบประเภทนี้จัดการกับกระบวนทัศน์การเขียนโปรแกรมแบบมัลติเธรด บางส่วนที่เป็นที่นิยม ได้แก่ :
- วัตถุที่ใช้งานอยู่
- ปฏิกิริยานิวเคลียร์
- กำหนดการ
รูปแบบการออกแบบสถาปัตยกรรม
ออกแบบลวดลายที่ใช้สำหรับงานสถาปัตยกรรม ที่มีชื่อเสียงที่สุดคือ:
- MVC (รุ่น-มุมมอง-คอนโทรลเลอร์)
- MVP (นางแบบ-ดู-พรีเซนเตอร์)
- MVVM (รุ่น-ดู-ดูรุ่น)
ในส่วนต่อไปนี้ เราจะพิจารณารูปแบบการออกแบบดังกล่าวอย่างละเอียดยิ่งขึ้นพร้อมตัวอย่างที่ให้ไว้เพื่อความเข้าใจที่ดีขึ้น
ตัวอย่างรูปแบบการออกแบบ
รูปแบบการออกแบบแต่ละแบบแสดงถึงประเภทของการแก้ปัญหาเฉพาะประเภท ไม่มีชุดรูปแบบสากลที่เหมาะสมที่สุดเสมอ เราจำเป็นต้องเรียนรู้ว่ารูปแบบใดจะเป็นประโยชน์และจะให้ค่าจริงหรือไม่ เมื่อเราคุ้นเคยกับรูปแบบและสถานการณ์ที่เหมาะสมที่สุดแล้ว เราสามารถระบุได้อย่างง่ายดายว่ารูปแบบเฉพาะนั้นเหมาะสมสำหรับปัญหาหนึ่งๆ หรือไม่
โปรดจำไว้ว่า การใช้รูปแบบที่ไม่ถูกต้องกับปัญหาที่กำหนดอาจนำไปสู่ผลกระทบที่ไม่พึงประสงค์ เช่น ความซับซ้อนของโค้ดที่ไม่จำเป็น ค่าใช้จ่ายที่ไม่จำเป็นต่อประสิทธิภาพ หรือแม้แต่การเกิดขึ้นของรูปแบบการต่อต้านรูปแบบใหม่
สิ่งเหล่านี้ล้วนเป็นสิ่งสำคัญที่ควรพิจารณาเมื่อคิดเกี่ยวกับการนำรูปแบบการออกแบบไปใช้กับโค้ดของเรา เราจะมาดูรูปแบบการออกแบบบางส่วนที่ฉันพบว่ามีประโยชน์เป็นการส่วนตัว และเชื่อว่านักพัฒนา JavaScript อาวุโสทุกคนควรคุ้นเคย
รูปแบบตัวสร้าง
เมื่อคิดถึงภาษาเชิงวัตถุแบบคลาสสิก คอนสตรัคเตอร์คือฟังก์ชันพิเศษในคลาสที่เริ่มต้นวัตถุด้วยชุดค่าดีฟอลต์และ/หรือค่าที่ส่งเข้ามา
วิธีทั่วไปในการสร้างวัตถุใน JavaScript มีสามวิธีดังต่อไปนี้:
// either of the following ways can be used to create a new object var instance = {}; // or var instance = Object.create(Object.prototype); // or var instance = new Object();
หลังจากสร้างวัตถุแล้ว มีสี่วิธี (ตั้งแต่ ES3) ในการเพิ่มคุณสมบัติให้กับวัตถุเหล่านี้ มีดังต่อไปนี้:
// supported since ES3 // the dot notation instance.key = "A key's value"; // the square brackets notation instance["key"] = "A key's value"; // supported since ES5 // setting a single property using Object.defineProperty Object.defineProperty(instance, "key", { value: "A key's value", writable: true, enumerable: true, configurable: true }); // setting multiple properties using Object.defineProperties Object.defineProperties(instance, { "firstKey": { value: "First key's value", writable: true }, "secondKey": { value: "Second key's value", writable: false } });
วิธีที่นิยมมากที่สุดในการสร้างวัตถุคือวงเล็บปีกกา และสำหรับการเพิ่มคุณสมบัติ ให้ใช้เครื่องหมายจุดหรือวงเล็บเหลี่ยม ใครก็ตามที่มีประสบการณ์กับ JavaScript ได้ใช้มัน
เราได้กล่าวไว้ก่อนหน้านี้ว่า JavaScript ไม่สนับสนุนคลาสดั้งเดิม แต่สนับสนุนตัวสร้างผ่านการใช้คำหลัก "ใหม่" ที่นำหน้าการเรียกใช้ฟังก์ชัน ด้วยวิธีนี้ เราสามารถใช้ฟังก์ชันเป็นคอนสตรัคเตอร์และเริ่มต้นคุณสมบัติของมันได้เช่นเดียวกับที่เราใช้กับคอนสตรัคเตอร์ภาษาแบบคลาสสิก
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; this.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();
อย่างไรก็ตาม ยังมีช่องว่างสำหรับการปรับปรุงที่นี่ ถ้าคุณจะจำได้ ฉันได้กล่าวไว้ก่อนหน้านี้ว่า JavaScript ใช้การสืบทอดตามต้นแบบ ปัญหาของแนวทางก่อนหน้านี้คือเมธอด writesCode
ถูกกำหนดใหม่สำหรับแต่ละอินสแตนซ์ของตัวสร้าง Person
เราสามารถหลีกเลี่ยงสิ่งนี้ได้โดยการตั้งค่าเมธอดลงในต้นแบบฟังก์ชัน:
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; } // we extend the function's prototype Person.prototype.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();
ตอนนี้ ทั้งสองอินสแตนซ์ของตัวสร้าง Person
สามารถเข้าถึงอินสแตนซ์ที่ใช้ร่วมกันของ writesCode()
ได้
รูปแบบโมดูล
ตราบใดที่มีลักษณะเฉพาะ JavaScript ไม่เคยหยุดนิ่ง สิ่งที่แปลกประหลาดอีกอย่างสำหรับ JavaScript (อย่างน้อยก็เท่าภาษาเชิงวัตถุ) คือ JavaScript ไม่รองรับตัวแก้ไขการเข้าถึง ในภาษา OOP แบบคลาสสิก ผู้ใช้กำหนดคลาสและกำหนดสิทธิ์การเข้าถึงสำหรับสมาชิก เนื่องจาก JavaScript ในรูปแบบธรรมดาไม่รองรับทั้งคลาสและตัวแก้ไขการเข้าถึง นักพัฒนา JavaScript จึงคิดหาวิธีเลียนแบบพฤติกรรมนี้เมื่อจำเป็น

ก่อนที่เราจะพูดถึงรูปแบบโมดูลเฉพาะ เรามาพูดถึงแนวคิดของการปิดกันก่อน การ ปิด เป็นฟังก์ชันที่สามารถเข้าถึงขอบเขตหลักได้ แม้ว่าฟังก์ชันหลักจะปิดไปแล้วก็ตาม สิ่งเหล่านี้ช่วยเราเลียนแบบพฤติกรรมของตัวแก้ไขการเข้าถึงผ่านการกำหนดขอบเขต ลองแสดงสิ่งนี้ผ่านตัวอย่าง:
// we used an immediately invoked function expression // to create a private variable, counter var counterIncrementer = (function() { var counter = 0; return function() { return ++counter; }; })(); // prints out 1 console.log(counterIncrementer()); // prints out 2 console.log(counterIncrementer()); // prints out 3 console.log(counterIncrementer());
อย่างที่คุณเห็น โดยใช้ IIFE เราได้เชื่อมโยงตัวแปรตัวนับกับฟังก์ชันที่ถูกเรียกใช้และปิด แต่ยังคงสามารถเข้าถึงได้โดยฟังก์ชันย่อยที่เพิ่มค่าดังกล่าว เนื่องจากเราไม่สามารถเข้าถึงตัวแปรตัวนับจากภายนอกนิพจน์ฟังก์ชัน เราจึงทำให้เป็นส่วนตัวผ่านการปรับขอบเขต
เมื่อใช้การปิด เราสามารถสร้างวัตถุที่มีส่วนส่วนตัวและส่วนสาธารณะได้ สิ่งเหล่านี้เรียกว่า โมดูล และมีประโยชน์มากเมื่อใดก็ตามที่เราต้องการซ่อนบางส่วนของวัตถุและเปิดเผยเฉพาะส่วนต่อประสานกับผู้ใช้โมดูลเท่านั้น ขอแสดงสิ่งนี้ในตัวอย่าง:
// through the use of a closure we expose an object // as a public API which manages the private objects array var collection = (function() { // private members var objects = []; // public members return { addObject: function(object) { objects.push(object); }, removeObject: function(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } }, getObjects: function() { return JSON.parse(JSON.stringify(objects)); } }; })(); collection.addObject("Bob"); collection.addObject("Alice"); collection.addObject("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(collection.getObjects()); collection.removeObject("Alice"); // prints ["Bob", "Franck"] console.log(collection.getObjects());
สิ่งที่มีประโยชน์มากที่สุดที่รูปแบบนี้แนะนำคือการแยกส่วนส่วนตัวและส่วนสาธารณะของวัตถุออกอย่างชัดเจน ซึ่งเป็นแนวคิดที่คล้ายกับนักพัฒนาที่มาจากพื้นหลังแบบคลาสสิกเชิงวัตถุ
อย่างไรก็ตาม ไม่ใช่ทุกอย่างจะสมบูรณ์แบบนัก เมื่อคุณต้องการเปลี่ยนการเปิดเผยของสมาชิก คุณต้องแก้ไขรหัสทุกที่ที่คุณใช้สมาชิกนี้ เนื่องจากลักษณะการเข้าถึงส่วนสาธารณะและส่วนส่วนตัวต่างกัน นอกจากนี้ วิธีการที่เพิ่มไปยังวัตถุหลังจากการสร้างไม่สามารถเข้าถึงสมาชิกส่วนตัวของวัตถุได้
เปิดเผยรูปแบบโมดูล
รูปแบบนี้เป็นการปรับปรุงรูปแบบโมดูลดังที่แสดงไว้ด้านบน ความแตกต่างหลัก ๆ คือ เราเขียนลอจิกของอ็อบเจ็กต์ทั้งหมดในขอบเขตส่วนตัวของโมดูล จากนั้นจึงเปิดเผยส่วนที่เราต้องการเปิดเผยต่อสาธารณะโดยส่งคืนอ็อบเจ็กต์ที่ไม่ระบุชื่อ นอกจากนี้เรายังสามารถเปลี่ยนชื่อของสมาชิกส่วนตัวเมื่อทำการแมปสมาชิกส่วนตัวกับสมาชิกสาธารณะที่เกี่ยวข้อง
// we write the entire object logic as private members and // expose an anonymous object which maps members we wish to reveal // to their corresponding public members var namesCollection = (function() { // private members var objects = []; function addObject(object) { objects.push(object); } function removeObject(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } } function getObjects() { return JSON.parse(JSON.stringify(objects)); } // public members return { addName: addObject, removeName: removeObject, getNames: getObjects }; })(); namesCollection.addName("Bob"); namesCollection.addName("Alice"); namesCollection.addName("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(namesCollection.getNames()); namesCollection.removeName("Alice"); // prints ["Bob", "Franck"] console.log(namesCollection.getNames());
รูปแบบโมดูลที่เปิดเผยเป็นหนึ่งในสามวิธีที่เราใช้รูปแบบโมดูลเป็นอย่างน้อย ความแตกต่างระหว่างรูปแบบโมดูลที่เปิดเผยและรูปแบบอื่น ๆ ของรูปแบบโมดูลนั้นส่วนใหญ่อยู่ที่วิธีการอ้างอิงสมาชิกสาธารณะ เป็นผลให้รูปแบบโมดูลที่เปิดเผยนั้นง่ายต่อการใช้และปรับเปลี่ยน อย่างไรก็ตาม มันอาจพิสูจน์ได้ว่าเปราะบางในบางสถานการณ์ เช่น การใช้วัตถุ RMP เป็นต้นแบบในห่วงโซ่การสืบทอด สถานการณ์ที่เป็นปัญหามีดังนี้:
- หากเรามีฟังก์ชั่นส่วนตัวที่อ้างถึงฟังก์ชั่นสาธารณะ เราไม่สามารถแทนที่ฟังก์ชั่นสาธารณะได้ เนื่องจากฟังก์ชั่นส่วนตัวจะยังคงอ้างถึงการใช้งานส่วนตัวของฟังก์ชั่น ดังนั้นจึงเป็นการแนะนำจุดบกพร่องในระบบของเรา
- หากเรามีสมาชิกสาธารณะที่ชี้ไปที่ตัวแปรส่วนตัว และพยายามแทนที่สมาชิกสาธารณะจากภายนอกโมดูล ฟังก์ชันอื่นๆ จะยังคงอ้างถึงค่าส่วนตัวของตัวแปร ทำให้เกิดจุดบกพร่องในระบบของเรา
รูปแบบซิงเกิล
รูปแบบซิงเกิลตันใช้ในสถานการณ์เมื่อเราต้องการอินสแตนซ์ของคลาสเพียงตัวเดียว ตัวอย่างเช่น เราจำเป็นต้องมีวัตถุที่มีการกำหนดค่าบางอย่างสำหรับบางสิ่งบางอย่าง ในกรณีเหล่านี้ ไม่จำเป็นต้องสร้างอ็อบเจ็กต์ใหม่เมื่อใดก็ตามที่ต้องการอ็อบเจ็กต์คอนฟิกูเรชันที่ใดที่หนึ่งในระบบ
var singleton = (function() { // private singleton value which gets initialized only once var config; function initializeConfiguration(values){ this.randomNumber = Math.random(); values = values || {}; this.number = values.number || 5; this.size = values.size || 10; } // we export the centralized method for retrieving the singleton value return { getConfig: function(values) { // we initialize the singleton value only once if (config === undefined) { config = new initializeConfiguration(values); } // and return the same config value wherever it is asked for return config; } }; })(); var configObject = singleton.getConfig({ "size": 8 }); // prints number: 5, size: 8, randomNumber: someRandomDecimalValue console.log(configObject); var configObject1 = singleton.getConfig({ "number": 8 }); // prints number: 5, size: 8, randomNumber: same randomDecimalValue as in first config console.log(configObject1);
ดังที่คุณเห็นในตัวอย่าง หมายเลขสุ่มที่สร้างขึ้นจะเหมือนกันเสมอ เช่นเดียวกับค่าปรับแต่งที่ส่งเข้ามา
สิ่งสำคัญคือต้องสังเกตว่าจุดเชื่อมต่อสำหรับการดึงค่าซิงเกิลตันต้องเป็นค่าเดียวเท่านั้นและเป็นที่รู้จักกันดี ข้อเสียของการใช้รูปแบบนี้คือการทดสอบค่อนข้างยาก
รูปแบบผู้สังเกตการณ์
รูปแบบผู้สังเกตเป็นเครื่องมือที่มีประโยชน์มากเมื่อเรามีสถานการณ์ที่เราจำเป็นต้องปรับปรุงการสื่อสารระหว่างส่วนต่างๆ ที่แตกต่างกันของระบบของเราในลักษณะที่เหมาะสมที่สุด มันส่งเสริมการมีเพศสัมพันธ์แบบหลวม ๆ ระหว่างวัตถุ
รูปแบบนี้มีหลายรูปแบบ แต่ในรูปแบบพื้นฐานที่สุด เรามีสองส่วนหลักของรูปแบบ อันแรกเป็นหัวข้อ อันที่สองเป็นผู้สังเกตการณ์
หัวเรื่องจัดการการดำเนินการทั้งหมดเกี่ยวกับหัวข้อเฉพาะที่ผู้สังเกตการณ์สมัครรับข้อมูล การดำเนินการเหล่านี้สมัครรับข้อมูลจากผู้สังเกตการณ์ในหัวข้อใดหัวข้อหนึ่ง ยกเลิกการสมัครรับข้อมูลจากผู้สังเกตการณ์จากหัวข้อใดหัวข้อหนึ่ง และแจ้งให้ผู้สังเกตการณ์ทราบเกี่ยวกับหัวข้อใดหัวข้อหนึ่งเมื่อมีการเผยแพร่กิจกรรม
อย่างไรก็ตาม มีรูปแบบอื่นที่เรียกว่ารูปแบบผู้เผยแพร่/สมาชิก ซึ่งฉันจะใช้เป็นตัวอย่างในส่วนนี้ ความแตกต่างที่สำคัญระหว่างรูปแบบผู้สังเกตแบบคลาสสิกกับรูปแบบผู้เผยแพร่/สมาชิกคือผู้เผยแพร่/ผู้ติดตามส่งเสริมการมีเพศสัมพันธ์ที่หลวมกว่ารูปแบบผู้สังเกตการณ์
ในรูปแบบผู้สังเกตการณ์ หัวข้อมีการอ้างอิงถึงผู้สังเกตการณ์ที่สมัครรับข้อมูลและเรียกใช้เมธอดโดยตรงจากออบเจกต์เอง ในขณะที่ในรูปแบบผู้เผยแพร่/สมาชิก เรามีช่องทางซึ่งทำหน้าที่เป็นสะพานเชื่อมการสื่อสารระหว่างสมาชิกและผู้เผยแพร่ ผู้จัดพิมพ์เริ่มเหตุการณ์และเรียกใช้ฟังก์ชันเรียกกลับที่ส่งสำหรับเหตุการณ์นั้น
ฉันจะแสดงตัวอย่างสั้นๆ ของรูปแบบผู้เผยแพร่/สมาชิก แต่สำหรับผู้ที่สนใจ ตัวอย่างรูปแบบผู้สังเกตการณ์แบบคลาสสิกสามารถหาได้ง่ายทางออนไลน์
var publisherSubscriber = {}; // we send in a container object which will handle the subscriptions and publishings (function(container) { // the id represents a unique subscription id to a topic var id = 0; // we subscribe to a specific topic by sending in // a callback function to be executed on event firing container.subscribe = function(topic, f) { if (!(topic in container)) { container[topic] = []; } container[topic].push({ "id": ++id, "callback": f }); return id; } // each subscription has its own unique ID, which we use // to remove a subscriber from a certain topic container.unsubscribe = function(topic, id) { var subscribers = []; for (var subscriber of container[topic]) { if (subscriber.id !== id) { subscribers.push(subscriber); } } container[topic] = subscribers; } container.publish = function(topic, data) { for (var subscriber of container[topic]) { // when executing a callback, it is usually helpful to read // the documentation to know which arguments will be // passed to our callbacks by the object firing the event subscriber.callback(data); } } })(publisherSubscriber); var subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Bob's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) { console.log("I am Bob's callback function for a hovered mouse event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID3 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Alice's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); // NOTE: after publishing an event with its data, all of the // subscribed callbacks will execute and will receive // a data object from the object firing the event // there are 3 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"}); // we unsubscribe from an event by removing the subscription ID publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3); // there are 2 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"});
รูปแบบการออกแบบนี้มีประโยชน์ในสถานการณ์ที่เราจำเป็นต้องดำเนินการหลายอย่างในเหตุการณ์เดียวที่กำลังถูกไล่ออก ลองนึกภาพว่าคุณมีสถานการณ์ที่เราจำเป็นต้องทำการเรียก AJAX หลายครั้งไปยังบริการแบ็คเอนด์ จากนั้นทำการเรียก AJAX อื่นๆ ขึ้นอยู่กับผลลัพธ์ คุณจะต้องซ้อนการเรียก AJAX ไว้ในที่อื่น อาจเข้าสู่สถานการณ์ที่เรียกว่า callback hell การใช้รูปแบบผู้เผยแพร่/สมาชิกเป็นโซลูชันที่หรูหรากว่ามาก
ข้อเสียของการใช้รูปแบบนี้คือการทดสอบส่วนต่างๆ ของระบบของเราได้ยาก ไม่มีทางที่เราจะรู้ว่าส่วนที่สมัครสมาชิกของระบบทำงานตามที่คาดไว้หรือไม่
รูปแบบคนกลาง
เราจะพูดถึงรูปแบบสั้นๆ ซึ่งมีประโยชน์มากเช่นกันเมื่อพูดถึงระบบแยกส่วน เมื่อเรามีสถานการณ์สมมติที่หลายส่วนของระบบจำเป็นต้องสื่อสารและประสานงานกัน บางทีทางออกที่ดีอาจเป็นการแนะนำตัวกลาง
ผู้ไกล่เกลี่ยคืออ็อบเจ็กต์ที่ใช้เป็นจุดศูนย์กลางสำหรับการสื่อสารระหว่างส่วนต่าง ๆ ของระบบและจัดการเวิร์กโฟลว์ระหว่างกัน ตอนนี้ สิ่งสำคัญคือต้องเน้นว่าการจัดการเวิร์กโฟลว์ ทำไมสิ่งนี้จึงสำคัญ?
เนื่องจากมีความคล้ายคลึงกันมากกับรูปแบบผู้เผยแพร่/สมาชิก คุณอาจถามตัวเองว่า โอเค ดังนั้นรูปแบบทั้งสองนี้จึงช่วยให้การสื่อสารระหว่างวัตถุดีขึ้น... อะไรคือความแตกต่าง?
ความแตกต่างก็คือผู้ไกล่เกลี่ยจะจัดการเวิร์กโฟลว์ ในขณะที่ผู้เผยแพร่/สมาชิกใช้สิ่งที่เรียกว่าการสื่อสารประเภท "ไฟไหม้และลืม" ผู้จัดพิมพ์/สมาชิกเป็นเพียงผู้รวบรวมเหตุการณ์ หมายความว่าเพียงดูแลการไล่เหตุการณ์และให้สมาชิกที่ถูกต้องทราบว่ากิจกรรมใดถูกไล่ออก ผู้รวบรวมเหตุการณ์ไม่สนใจว่าจะเกิดอะไรขึ้นเมื่อเหตุการณ์ถูกไล่ออก ซึ่งไม่ใช่กรณีที่มีผู้ไกล่เกลี่ย
ตัวอย่างที่ดีของผู้ไกล่เกลี่ยคืออินเทอร์เฟซประเภทวิซาร์ด สมมติว่าคุณมีขั้นตอนการลงทะเบียนจำนวนมากสำหรับระบบที่คุณเคยใช้งาน บ่อยครั้ง เมื่อผู้ใช้ต้องการข้อมูลจำนวนมาก แนวทางปฏิบัติที่ดีที่จะแบ่งออกเป็นหลายขั้นตอน
ด้วยวิธีนี้ รหัสจะสะอาดขึ้นมาก (ง่ายต่อการบำรุงรักษา) และผู้ใช้จะไม่ถูกครอบงำด้วยจำนวนข้อมูลที่ขอเพียงเพื่อลงทะเบียนให้เสร็จสิ้น ผู้ไกล่เกลี่ยคือออบเจ็กต์ที่จะจัดการขั้นตอนการลงทะเบียน โดยคำนึงถึงเวิร์กโฟลว์ที่แตกต่างกันที่อาจเกิดขึ้นเนื่องจากผู้ใช้แต่ละคนอาจมีกระบวนการลงทะเบียนที่ไม่ซ้ำกัน
ประโยชน์ที่ชัดเจนจากรูปแบบการออกแบบนี้คือการสื่อสารที่ดีขึ้นระหว่างส่วนต่างๆ ของระบบ ซึ่งขณะนี้ทั้งหมดสื่อสารผ่านตัวกลางและโค้ดเบสที่สะอาดขึ้น
ข้อเสียคือตอนนี้เราได้แนะนำจุดล้มเหลวจุดเดียวในระบบของเราแล้ว ซึ่งหมายความว่าหากตัวกลางของเราล้มเหลว ระบบทั้งหมดก็จะหยุดทำงาน
รูปแบบต้นแบบ
ดังที่เราได้กล่าวไปแล้วในบทความว่า JavaScript ไม่รองรับคลาสในรูปแบบดั้งเดิม การสืบทอดระหว่างอ็อบเจ็กต์ถูกนำไปใช้โดยใช้การเขียนโปรแกรมแบบต้นแบบ
ช่วยให้เราสามารถสร้างวัตถุที่สามารถใช้เป็นต้นแบบสำหรับวัตถุอื่น ๆ ที่ถูกสร้างขึ้น ออบเจ็กต์ต้นแบบถูกใช้เป็นพิมพ์เขียวสำหรับแต่ละอ็อบเจ็กต์ที่คอนสตรัคเตอร์สร้างขึ้น
ดังที่เราได้กล่าวไปแล้วในหัวข้อที่แล้ว เรามาแสดงตัวอย่างง่ายๆ ว่ารูปแบบนี้จะถูกนำมาใช้อย่างไร
var personPrototype = { sayHi: function() { console.log("Hello, my name is " + this.name + ", and I am " + this.age); }, sayBye: function() { console.log("Bye Bye!"); } }; function Person(name, age) { name = name || "John Doe"; age = age || 26; function constructorFunction(name, age) { this.name = name; this.age = age; }; constructorFunction.prototype = personPrototype; var instance = new constructorFunction(name, age); return instance; } var person1 = Person(); var person2 = Person("Bob", 38); // prints out Hello, my name is John Doe, and I am 26 person1.sayHi(); // prints out Hello, my name is Bob, and I am 38 person2.sayHi();
Take notice how prototype inheritance makes a performance boost as well because both objects contain a reference to the functions which are implemented in the prototype itself, instead of in each of the objects.
Command Pattern
The command pattern is useful in cases when we want to decouple objects executing the commands from objects issuing the commands. For example, imagine a scenario where our application is using a large number of API service calls. Then, let's say that the API services change. We would have to modify the code wherever the APIs that changed are called.
This would be a great place to implement an abstraction layer, which would separate the objects calling an API service from the objects which are telling them when to call the API service. This way, we avoid modification in all of the places where we have a need to call the service, but rather have to change only the objects which are making the call itself, which is only one place.
As with any other pattern, we have to know when exactly is there a real need for such a pattern. We need to be aware of the tradeoff we are making, as we are adding an additional abstraction layer over the API calls, which will reduce performance but potentially save a lot of time when we need to modify objects executing the commands.
// the object which knows how to execute the command var invoker = { add: function(x, y) { return x + y; }, subtract: function(x, y) { return x - y; } } // the object which is used as an abstraction layer when // executing commands; it represents an interface // toward the invoker object var manager = { execute: function(name, args) { if (name in invoker) { return invoker[name].apply(invoker, [].slice.call(arguments, 1)); } return false; } } // prints 8 console.log(manager.execute("add", 3, 5)); // prints 2 console.log(manager.execute("subtract", 5, 3));
Facade Pattern
The facade pattern is used when we want to create an abstraction layer between what is shown publicly and what is implemented behind the curtain. It is used when an easier or simpler interface to an underlying object is desired.
A great example of this pattern would be selectors from DOM manipulation libraries such as jQuery, Dojo, or D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:
jQuery(".parent .child div.span")
It simplifies the selection features a lot, and even though it seems simple on the surface, there is an entire complex logic implemented under the hood in order for this to work.
We also need to be aware of the performance-simplicity tradeoff. It is desirable to avoid extra complexity if it isn't beneficial enough. In the case of the aforementioned libraries, the tradeoff was worth it, as they are all very successful libraries.
ขั้นตอนถัดไป
Design patterns are a very useful tool which any senior JavaScript developer should be aware of. Knowing the specifics regarding design patterns could prove incredibly useful and save you a lot of time in any project's lifecycle, especially the maintenance part. Modifying and maintaining systems written with the help of design patterns which are a good fit for the system's needs could prove invaluable.
In order to keep the article relatively brief, we will not be displaying any more examples. For those interested, a great inspiration for this article came from the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Software and Addy Osmani's Learning JavaScript Design Patterns . I highly recommend both books.