ฉันควรใช้การเขียนโปรแกรมตามเหตุการณ์เมื่อใด


65

ฉันได้ผ่านการเรียกกลับหรือเพียงแค่เรียกใช้ฟังก์ชันจากฟังก์ชันอื่นในโปรแกรมของฉันเพื่อทำให้สิ่งต่าง ๆ เกิดขึ้นเมื่องานเสร็จสมบูรณ์ เมื่อบางสิ่งบางอย่างเสร็จสิ้นฉันจะเรียกใช้ฟังก์ชันโดยตรง:

var ground = 'clean';

function shovelSnow(){
    console.log("Cleaning Snow");
    ground = 'clean';
}

function makeItSnow(){
    console.log("It's snowing");
    ground = 'snowy';
    shovelSnow();
}

แต่ฉันได้อ่านเกี่ยวกับกลยุทธ์ต่าง ๆ ในการเขียนโปรแกรมและกลยุทธ์ที่ฉันเข้าใจว่ามีประสิทธิภาพ แต่ยังไม่ได้ฝึกฝนนั้นเป็นเหตุการณ์ที่ใช้ (ฉันคิดว่าวิธีการที่ฉันอ่านเกี่ยวกับเรียกว่า"pub-sub" ):

var ground = 'clean';

function shovelSnow(){
    console.log("Cleaning Snow");
    ground = 'clean';
}

function makeItSnow(){
    console.log("It's snowing");
    ground = 'snowy';
    $(document).trigger('snow');
}

$(document).bind('snow', shovelSnow);

ฉันต้องการที่จะเข้าใจจุดแข็งและจุดอ่อนของการเขียนโปรแกรมเชิงเหตุการณ์เทียบกับการเรียกฟังก์ชั่นทั้งหมดของคุณจากภายในฟังก์ชั่นอื่น ๆ การโปรแกรมมิงตามเหตุการณ์ใช้สถานการณ์ใดในการโปรแกรม


2
$(document).bind('snow', shovelShow)เช่นกันคุณก็สามารถใช้ ไม่จำเป็นต้องห่อในฟังก์ชันที่ไม่ระบุชื่อ
Karl Bielefeldt

4
คุณอาจสนใจที่จะเรียนรู้เกี่ยวกับ "การเขียนโปรแกรมเชิงโต้ตอบ" ซึ่งมีหลายอย่างที่เหมือนกันกับการเขียนโปรแกรมเชิงเหตุการณ์
Eric Lippert

คำตอบ:


75

เหตุการณ์คือการแจ้งเตือนการอธิบายปรากฏการณ์ที่เกิดขึ้นจากอดีตที่ผ่านมา

การใช้งานตามปกติของระบบที่ขับเคลื่อนด้วยเหตุการณ์ใช้ฟังก์ชั่นตัวกระจายเหตุการณ์และฟังก์ชันตัวจัดการ (หรือสมาชิก ) โปรแกรมเลือกจ่ายงานจัดหา API ให้กับตัวจัดการสายไฟสูงสุดถึงเหตุการณ์ (jQuery's bind) และวิธีการเผยแพร่กิจกรรมให้กับสมาชิก ( triggerใน jQuery) เมื่อคุณกำลังพูดถึงเหตุการณ์ IO หรือ UI มักจะมีวงวนเหตุการณ์ซึ่งตรวจจับเหตุการณ์ใหม่เช่นการคลิกเมาส์และส่งต่อไปยังโปรแกรมเลือกจ่ายงาน ใน JS-land ตัวกระจายข้อมูลและเหตุการณ์วนรอบจะถูกจัดเตรียมโดยเบราว์เซอร์

สำหรับรหัสที่มีการโต้ตอบกับผู้ใช้โดยตรง - ตอบสนองต่อการกดแป้นและคลิก - การเขียนโปรแกรมเหตุการณ์ที่ขับเคลื่อนด้วย (หรือรูปแบบดังกล่าวเช่นการเขียนโปรแกรมปฏิกิริยาการทำงาน ) หลีกเลี่ยงไม่ได้เกือบ คุณโปรแกรมเมอร์ไม่มีความคิดว่าผู้ใช้จะคลิกเมื่อใดหรือที่ไหนดังนั้นจึงลงไปที่กรอบ GUI หรือเบราว์เซอร์เพื่อตรวจจับการกระทำของผู้ใช้ในลูปเหตุการณ์และแจ้งรหัสของคุณ โครงสร้างพื้นฐานประเภทนี้ยังใช้ในแอปพลิเคชั่นเครือข่าย (cf NodeJS)

ตัวอย่างของคุณในกรณีที่คุณเพิ่มเหตุการณ์ในรหัสของคุณแทนที่จะเรียกฟังก์ชั่นโดยตรงจะมีการแลกเปลี่ยนที่น่าสนใจมากขึ้นซึ่งฉันจะกล่าวถึงด้านล่าง ข้อแตกต่างที่สำคัญคือผู้เผยแพร่กิจกรรม ( makeItSnow) ไม่ได้ระบุผู้รับของการโทร ที่มีสายอื่น ๆ (ในbindตัวอย่างของคุณ) สิ่งนี้เรียกว่าไฟและลืม : makeItSnowประกาศให้โลกรู้ว่าหิมะตก แต่ไม่สนใจว่าใครกำลังฟังเกิดอะไรขึ้นต่อไปหรือเมื่อมันเกิดขึ้น - มันออกอากาศข้อความและฝุ่นออกจากมือ


ดังนั้นวิธีการที่ขับเคลื่อนด้วยเหตุการณ์จะแยกผู้ส่งข้อความออกจากผู้รับ ข้อได้เปรียบอย่างหนึ่งที่ทำให้คุณมั่นใจคือเหตุการณ์ที่กำหนดอาจมีตัวจัดการหลายตัว คุณสามารถผูกgritRoadsฟังก์ชันกับเหตุการณ์หิมะโดยไม่ส่งผลกระทบต่อshovelSnowตัวจัดการที่มีอยู่ คุณมีความยืดหยุ่นในการใช้งานแอปพลิเคชันของคุณ หากต้องการปิดการทำงานคุณเพียงแค่ลบการbindโทรออกแทนที่จะไปหาโค้ดเพื่อค้นหาอินสแตนซ์ทั้งหมดของการทำงาน

ข้อดีอีกอย่างของการเขียนโปรแกรมที่ขับเคลื่อนด้วยเหตุการณ์ก็คือมันทำให้คุณมีข้อกังวลข้าม ตัวแจกจ่ายเหตุการณ์มีบทบาทเป็นผู้ไกล่เกลี่ยและบางไลบรารี (เช่นBrighter ) ใช้ไพพ์ไลน์เพื่อให้คุณสามารถปลั๊กอินข้อกำหนดทั่วไปเช่นการบันทึกหรือคุณภาพของบริการได้อย่างง่ายดาย

การเปิดเผยอย่างเต็มรูปแบบ: Brighter พัฒนาขึ้นที่ Huddle ที่ซึ่งฉันทำงานอยู่

ข้อได้เปรียบประการที่สามของการแยกตัวส่งสัญญาณเหตุการณ์จากผู้รับคือมันให้ความยืดหยุ่นเมื่อคุณจัดการกับเหตุการณ์ คุณสามารถประมวลผลเหตุการณ์แต่ละประเภทด้วยเธรดของตัวเอง (หากผู้แจกจ่ายเหตุการณ์ของคุณสนับสนุน) หรือคุณสามารถใส่เหตุการณ์ที่เพิ่มเข้ามาในนายหน้าข้อความเช่นRabbitMQและจัดการกับกระบวนการแบบอะซิงโครนัสหรือดำเนินการแบบข้ามคืนจำนวนมาก ผู้รับของเหตุการณ์อาจอยู่ในกระบวนการแยกต่างหากหรือบนเครื่องแยกต่างหาก คุณไม่จำเป็นต้องเปลี่ยนรหัสเพื่อให้เหตุการณ์เกิดขึ้น! นี่คือความคิดที่ยิ่งใหญ่ที่อยู่เบื้องหลังสถาปัตยกรรม "บริการไมโครสโคป": บริการอิสระสื่อสารโดยใช้กิจกรรมด้วยการส่งข้อความมิดเดิลแวร์เป็นแกนหลักของแอปพลิเคชัน

สำหรับตัวอย่างที่แตกต่างกันของสไตล์ที่ขับเคลื่อนด้วยเหตุการณ์ให้ดูที่การออกแบบที่ขับเคลื่อนด้วยโดเมนโดยที่กิจกรรมโดเมนจะถูกใช้เพื่อช่วยแยกมวลรวมออกจากกัน ตัวอย่างเช่นพิจารณาร้านค้าออนไลน์ที่แนะนำผลิตภัณฑ์โดยอ้างอิงจากประวัติการซื้อของคุณ Customerความต้องการที่จะมีประวัติของการซื้อของการอัปเดตเมื่อShoppingCartมีการจ่ายเงินสำหรับ ผลShoppingCartรวมอาจแจ้งCustomerโดยการเพิ่มCheckoutCompletedเหตุการณ์ Customerจะได้รับการปรับปรุงในการทำธุรกรรมที่แยกต่างหากในการตอบสนองต่อเหตุการณ์ที่เกิดขึ้น


ข้อเสียหลักของโมเดลที่ขับเคลื่อนด้วยเหตุการณ์นี้คือการอ้อม ตอนนี้หารหัสที่จัดการเหตุการณ์ได้ยากขึ้นเพราะคุณไม่สามารถนำทางได้โดยใช้ IDE ของคุณ คุณต้องคิดออกว่าเหตุการณ์ถูกผูกไว้ในการกำหนดค่าและหวังว่าคุณจะได้พบกับตัวจัดการทั้งหมด มีหลายสิ่งที่คุณจะต้องเก็บไว้ในใจตลอดเวลา การประชุมลักษณะรหัสสามารถช่วยได้ที่นี่ (ตัวอย่างเช่นการโทรทั้งหมดไปยังbindไฟล์เดียว) เพื่อประโยชน์ด้านสุขภาพจิตของคุณเป็นเรื่องสำคัญที่จะต้องใช้ผู้ส่งเหตุการณ์หนึ่งรายเท่านั้นและใช้อย่างสม่ำเสมอ

ข้อเสียอีกอย่างหนึ่งก็คือมันยากที่จะปรับโครงสร้างเหตุการณ์ หากคุณต้องการเปลี่ยนรูปแบบของกิจกรรมคุณต้องเปลี่ยนเครื่องรับทั้งหมด สิ่งนี้จะทวีความรุนแรงมากขึ้นเมื่อสมาชิกของกิจกรรมอยู่บนเครื่องที่แตกต่างกันเพราะตอนนี้คุณต้องซิงโครไนซ์การเปิดตัวซอฟต์แวร์!

ในบางสถานการณ์ประสิทธิภาพอาจมีความกังวล เมื่อประมวลผลข้อความโปรแกรมเลือกจ่ายงานจะต้อง:

  1. ค้นหาตัวจัดการที่ถูกต้องในโครงสร้างข้อมูลบางอย่าง
  2. สร้างไปป์ไลน์การประมวลผลข้อความสำหรับแต่ละตัวจัดการ นี่อาจเกี่ยวข้องกับการจัดสรรหน่วยความจำมากมาย
  3. เรียกตัวจัดการแบบไดนามิก (อาจใช้การสะท้อนหากภาษาต้องการ)

นี่จะช้ากว่าการเรียกใช้ฟังก์ชันปกติซึ่งเกี่ยวข้องกับการกดเฟรมใหม่บนสแต็กเท่านั้น อย่างไรก็ตามความยืดหยุ่นที่สถาปัตยกรรมที่ขับเคลื่อนด้วยเหตุการณ์ทำให้คุณสามารถแยกและทำให้โค้ดช้าได้ง่ายขึ้น การมีความสามารถในการส่งงานไปยังตัวประมวลผลแบบอะซิงโครนัสเป็นชัยชนะที่ยิ่งใหญ่ที่นี่เนื่องจากช่วยให้คุณสามารถให้บริการตามคำขอได้ทันทีในขณะที่การทำงานอย่างหนักจะได้รับการจัดการในพื้นหลัง ไม่ว่าในกรณีใดถ้าคุณกำลังโต้ตอบกับฐานข้อมูลหรือการวาดรูปบนหน้าจอค่าใช้จ่ายของ IO จะเต็มไปด้วยค่าใช้จ่ายในการประมวลผลข้อความ เป็นกรณีของการหลีกเลี่ยงการเพิ่มประสิทธิภาพก่อนวัยอันควร


โดยสรุปเหตุการณ์เป็นวิธีที่ดีในการสร้างซอฟต์แวร์แบบคู่อย่างอิสระ แต่ไม่มีค่าใช้จ่าย มันจะเป็นความผิดพลาดเช่นการเปลี่ยนทุกฟังก์ชั่นในการประยุกต์ใช้ของคุณกับเหตุการณ์ ใช้เหตุการณ์เพื่อทำให้แผนกสถาปัตยกรรมที่มีความหมาย


2
คำตอบนี้บอกเหมือนกับคำตอบของ 5377 ที่ฉันเลือกให้ถูกต้อง ฉันกำลังเปลี่ยนการเลือกของฉันเพื่อทำเครื่องหมายสิ่งนี้เพราะมันจะอธิบายเพิ่มเติม
Viziionary

1
ความเร็วเป็นข้อเสียที่สำคัญของรหัสที่ขับเคลื่อนด้วยเหตุการณ์หรือไม่ ดูเหมือนว่าจะเป็นไปได้ แต่ฉันก็ไม่รู้
raptortech97

1
@ raptortech97 มันสามารถ สำหรับรหัสที่จำเป็นต้องรวดเร็วเป็นพิเศษคุณอาจต้องการหลีกเลี่ยงการส่งกิจกรรมเป็นวงภายใน โชคดีที่ในสถานการณ์เช่นนี้มันมักจะกำหนดสิ่งที่คุณต้องทำอย่างดีดังนั้นคุณจึงไม่ต้องการความยืดหยุ่นในการจัดกิจกรรมเพิ่มเติม (หรือเผยแพร่ / สมัครสมาชิกหรือผู้สังเกตการณ์ซึ่งเป็นกลไกที่เทียบเท่ากับคำศัพท์ที่แตกต่างกัน)
จูลส์

1
โปรดทราบว่ามีบางภาษา (เช่น Erlang) ที่สร้างขึ้นรอบ ๆ โมเดลนักแสดงซึ่งทุกอย่างเป็นข้อความ (กิจกรรม) ในกรณีนี้คอมไพเลอร์สามารถตัดสินใจว่าจะใช้ข้อความ / เหตุการณ์เป็นการเรียกใช้ฟังก์ชันโดยตรงหรือเป็นการสื่อสาร
เบรนแดน

1
สำหรับ "ประสิทธิภาพ" ฉันคิดว่าเราต้องแยกความแตกต่างระหว่างประสิทธิภาพแบบเธรดเดียวและความสามารถในการปรับขนาดได้ ข้อความ / เหตุการณ์อาจแย่ลงสำหรับการทำงานแบบเธรดเดียว (แต่สามารถเปลี่ยนเป็นการเรียกใช้ฟังก์ชันได้โดยไม่เสียค่าใช้จ่ายเพิ่มเติมและไม่แย่ไปกว่านี้) และสำหรับความสามารถในการปรับขยายมันเหนือกว่าในทุก ๆ ด้าน (เช่น -CPU และระบบอนาคต "CPU หลายตัว")
เบรนแดน

25

การเขียนโปรแกรมตามเหตุการณ์ใช้เมื่อโปรแกรมไม่ได้ควบคุมลำดับของเหตุการณ์ที่เกิดขึ้น แต่โฟลว์ของโปรแกรมจะถูกควบคุมโดยกระบวนการภายนอกเช่นผู้ใช้ (เช่น GUI), ระบบอื่น (เช่นไคลเอนต์ / เซิร์ฟเวอร์) หรือกระบวนการอื่น (เช่น RPC)

ตัวอย่างเช่นสคริปต์การประมวลผลแบบแบตช์รู้ว่าต้องทำอะไร มันไม่ได้เป็นไปตามเหตุการณ์

โปรแกรมประมวลผลคำอยู่ที่นั่นและรอให้ผู้ใช้เริ่มพิมพ์ Keypresses เป็นเหตุการณ์ที่ก่อให้เกิดการทำงานเพื่ออัพเดทบัฟเฟอร์เอกสารภายใน โปรแกรมไม่สามารถรู้ได้ว่าคุณต้องการพิมพ์อะไรจึงต้องมีการขับเคลื่อนเหตุการณ์

โปรแกรม GUI ส่วนใหญ่เป็นไปตามเหตุการณ์เนื่องจากสร้างขึ้นจากการโต้ตอบกับผู้ใช้ อย่างไรก็ตามโปรแกรมตามเหตุการณ์ไม่ได้ จำกัด อยู่ที่ GUI ซึ่งเป็นเพียงตัวอย่างที่คุ้นเคยที่สุดสำหรับคนส่วนใหญ่ เว็บเซิร์ฟเวอร์รอให้ลูกค้าเชื่อมต่อและทำตามสำนวนที่คล้ายกัน กระบวนการเบื้องหลังในคอมพิวเตอร์ของคุณอาจตอบสนองต่อเหตุการณ์ได้เช่นกัน ตัวอย่างเช่นเครื่องสแกนไวรัสตามความต้องการอาจได้รับเหตุการณ์จากระบบปฏิบัติการเกี่ยวกับไฟล์ที่สร้างขึ้นใหม่หรืออัพเดทจากนั้นสแกนไฟล์นั้นเพื่อหาไวรัส


18

ในแอปพลิเคชันที่ยึดตามเหตุการณ์แนวคิดของEvent Listenersจะให้ความสามารถในการเขียนแอปพลิเคชันแบบคู่ที่หลวมมากขึ้น

ตัวอย่างเช่นโมดูลหรือปลั๊กอินของบุคคลที่สามสามารถลบบันทึกออกจากฐานข้อมูลจากนั้นทริกเกอร์receordDeletedเหตุการณ์และปล่อยให้ส่วนที่เหลือไปยังผู้ฟังเหตุการณ์เพื่อทำงานของพวกเขา ทุกอย่างจะทำงานได้ดีแม้ว่าโมดูลที่กระตุ้นการทำงานจะไม่รู้ว่าใครกำลังฟังเหตุการณ์นี้อยู่หรือว่าจะเกิดอะไรขึ้นต่อไป


6

การเปรียบเทียบง่าย ๆ ที่ฉันต้องการเพิ่มที่ช่วยฉัน:

คิดว่าองค์ประกอบ (หรือวัตถุ) ของแอปพลิเคชันของคุณเป็นกลุ่มใหญ่ของเพื่อน Facebook

เมื่อเพื่อนของคุณคนใดคนหนึ่งต้องการบอกคุณบางอย่างพวกเขาสามารถโทรหาคุณโดยตรงหรือโพสต์ลงบนวอลล์ Facebook ของพวกเขา เมื่อพวกเขาโพสต์ลงใน Facebook จากนั้นทุกคน สามารถเห็นมันและโต้ตอบกับมัน แต่คนจำนวนมากไม่ได้ บางครั้งมันเป็นสิ่งสำคัญที่ผู้คนจะต้องตอบสนองต่อมันเช่น "เรากำลังมีลูก!" หรือ "วง So-so-so กำลังทำเซอร์ไพรส์คอนเสิร์ตที่บาร์ Drunkin 'Clam!" ในกรณีสุดท้ายนั้นเพื่อนที่เหลืออาจมีปฏิกิริยาต่อมันโดยเฉพาะอย่างยิ่งหากพวกเขาสนใจวงนั้น

หากเพื่อนของคุณต้องการเก็บความลับระหว่างคุณกับพวกเขาพวกเขาอาจจะไม่โพสต์ไว้ที่ผนัง Facebook ของพวกเขาพวกเขาจะโทรหาคุณโดยตรงและบอกคุณ นึกภาพสถานการณ์ที่คุณบอกผู้หญิงคนหนึ่งว่าคุณต้องการพบเธอที่ร้านอาหารเพื่อออกเดท แทนที่จะโทรหาเธอโดยตรงและถามเธอคุณโพสต์ไว้บนกระดาน Facebook เพื่อให้เพื่อน ๆ ทุกคนเห็น วิธีนี้ใช้ได้ผล แต่ถ้าคุณมีคนที่ขี้หึงคุณก็จะเห็นและปรากฏตัวที่ร้านอาหารเพื่อทำลายวันของคุณ

เมื่อตัดสินใจว่าจะสร้างในตัวรับฟังเหตุการณ์เพื่อนำสิ่งใดไปใช้หรือไม่ให้นึกถึงการเปรียบเทียบนี้ องค์ประกอบนี้จำเป็นต้องนำธุรกิจของพวกเขาออกไปให้ทุกคนได้เห็นหรือไม่? หรือพวกเขาจำเป็นต้องโทรหาใครบางคนโดยตรง? สิ่งต่าง ๆ อาจดูยุ่งเหยิงได้ง่ายดังนั้นควรระวัง


0

การเปรียบเทียบต่อไปนี้อาจช่วยให้คุณเข้าใจการเขียนโปรแกรม I / O ที่ขับเคลื่อนด้วยเหตุการณ์โดยวาดเส้นคู่ขนานกับเส้นรอที่แผนกต้อนรับของหมอ

การบล็อก I / O เหมือนกับว่าถ้าคุณยืนอยู่ในคิวพนักงานต้อนรับจะถามคนที่อยู่ข้างหน้าคุณให้กรอกแบบฟอร์มและเธอจะรอจนกว่าเขาจะเสร็จ คุณต้องรอตาคุณจนกว่าผู้ชายจะเสร็จสิ้นแบบฟอร์มของเขานี่คือการปิดกั้น

ถ้าผู้ชายคนเดียวใช้เวลา 3 นาทีในการกรอกคนที่ 10 ต้องรอจนถึง 30 นาที ตอนนี้เพื่อลดเวลารอคนที่ 10 นี้ทางออกจะเพิ่มจำนวนพนักงานต้อนรับซึ่งมีค่าใช้จ่ายเพิ่มขึ้น นี่คือสิ่งที่เกิดขึ้นในเว็บเซิร์ฟเวอร์แบบดั้งเดิม หากคุณร้องขอข้อมูลผู้ใช้ผู้ใช้รายอื่นจะต้องรอจนกว่าการดำเนินการปัจจุบันการดึงข้อมูลจากฐานข้อมูลจะเสร็จสมบูรณ์ สิ่งนี้จะเพิ่ม "เวลาในการตอบสนอง" ของคำร้องขอที่ 10 และจะเพิ่มขึ้นอย่างทวีคูณสำหรับผู้ใช้ที่ n เพื่อหลีกเลี่ยงเว็บเซิร์ฟเวอร์แบบดั้งเดิมนี้จะสร้างเธรด (เทียบเท่ากับจำนวนพนักงานต้อนรับที่เพิ่มขึ้น) สำหรับทุกคำร้องขอเดียวโดยทั่วไปจะสร้างสำเนาของเซิร์ฟเวอร์สำหรับแต่ละคำขอซึ่งเป็นปริมาณการใช้ CPU ที่มีค่าใช้จ่ายสูงเนื่องจากคำขอทุกครั้งจะต้องใช้ระบบปฏิบัติการ ด้าย. ในการขยายขนาดแอป

Event Driven : อีกวิธีหนึ่งในการไต่ระดับ "เวลาตอบสนอง" ของคิวคือการไปหาวิธีที่ขับเคลื่อนด้วยเหตุการณ์โดยที่ผู้ชายในคิวจะถูกส่งมอบแบบฟอร์มขอให้กรอกข้อมูลและกลับมาเสร็จสมบูรณ์ ดังนั้นพนักงานต้อนรับสามารถขอได้เสมอ นี่คือสิ่งที่ javascript ใช้งานนับตั้งแต่ก่อตั้งขึ้น ในเบราว์เซอร์จาวาสคริปต์จะตอบสนองต่อเหตุการณ์การคลิกของผู้ใช้เลื่อนเลื่อนหรือดึงฐานข้อมูลและอื่น ๆ สิ่งนี้มีความเป็นไปได้ในจาวาสคริปต์โดยเนื้อแท้เพราะจาวาสคริปต์ใช้งานฟังก์ชั่นเป็นวัตถุชั้นหนึ่งและพวกเขาสามารถส่งผ่านเป็นพารามิเตอร์ไปยังฟังก์ชั่นอื่น ๆ (เรียกว่าโทรกลับ) และสามารถเรียกเมื่อเสร็จสิ้นภารกิจเฉพาะ นี่คือสิ่งที่ node.js ทำบนเซิร์ฟเวอร์ คุณสามารถค้นหาข้อมูลเพิ่มเติมเกี่ยวกับการเขียนโปรแกรมที่ขับเคลื่อนด้วยเหตุการณ์และการบล็อก i / o ในบริบทของโหนดที่นี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.