โหมดผู้สังเกตการณ์ หรือที่รู้จักกันในชื่อโหมดเผยแพร่/สมัครสมาชิกถูกเสนอโดยกลุ่มสี่คน (GOF คือ Erich Gamma, Richard Helm, Ralph Johnson และ John Vlissides) ในปี 1994 "รูปแบบการออกแบบ: พื้นฐานของซอฟต์แวร์เชิงวัตถุที่นำกลับมาใช้ใหม่ได้" (ดูหน้า 293-313 แม้ว่ารูปแบบนี้จะมีประวัติจำนวนมาก แต่ก็ยังคงมีผลบังคับใช้อย่างกว้างขวางกับสถานการณ์ที่หลากหลายและได้กลายเป็นส่วนสำคัญของห้องสมุด Java มาตรฐาน แม้ว่าจะมีบทความมากมายเกี่ยวกับรูปแบบผู้สังเกตการณ์ แต่พวกเขาทั้งหมดมุ่งเน้นไปที่การใช้งานใน Java แต่ไม่สนใจปัญหาต่าง ๆ ที่นักพัฒนาพบเมื่อใช้รูปแบบผู้สังเกตการณ์ใน Java
ความตั้งใจดั้งเดิมของการเขียนบทความนี้คือการเติมช่องว่างนี้: บทความนี้ส่วนใหญ่แนะนำการใช้รูปแบบผู้สังเกตการณ์โดยใช้สถาปัตยกรรม Java8 และสำรวจปัญหาที่ซับซ้อนเกี่ยวกับรูปแบบคลาสสิกบนพื้นฐานนี้รวมถึงชั้นเรียนภายในที่ไม่ระบุชื่อ แม้ว่าเนื้อหาของบทความนี้จะไม่ครอบคลุม แต่ปัญหาที่ซับซ้อนจำนวนมากที่เกี่ยวข้องในโมเดลนี้ไม่สามารถอธิบายได้ในบทความเดียว แต่หลังจากอ่านบทความนี้ผู้อ่านสามารถเข้าใจว่ารูปแบบผู้สังเกตการณ์คืออะไรความเป็นสากลใน Java และวิธีจัดการกับปัญหาที่พบบ่อยเมื่อใช้รูปแบบผู้สังเกตการณ์ใน Java
โหมดผู้สังเกตการณ์
ตามคำจำกัดความคลาสสิกที่เสนอโดย GOF ธีมของรูปแบบผู้สังเกตการณ์คือ:
กำหนดการพึ่งพาแบบหนึ่งถึงหลายคนระหว่างวัตถุ เมื่อสถานะของวัตถุเปลี่ยนแปลงวัตถุทั้งหมดที่ขึ้นอยู่กับมันจะได้รับแจ้งและอัปเดตโดยอัตโนมัติ
หมายความว่าอย่างไร? ในแอพพลิเคชั่นซอฟต์แวร์จำนวนมากรัฐระหว่างวัตถุจะพึ่งพาซึ่งกันและกัน ตัวอย่างเช่นหากแอปพลิเคชันมุ่งเน้นไปที่การประมวลผลข้อมูลเชิงตัวเลขข้อมูลนี้อาจแสดงผ่านตารางหรือแผนภูมิของส่วนต่อประสานกราฟิกผู้ใช้ (GUI) หรือใช้ในเวลาเดียวกันนั่นคือเมื่อข้อมูลพื้นฐานได้รับการปรับปรุงส่วนประกอบ GUI ที่สอดคล้องกันจะต้องได้รับการปรับปรุง กุญแจสำคัญในการแก้ไขปัญหาคือวิธีการอัปเดตข้อมูลพื้นฐานเมื่อส่วนประกอบ GUI ได้รับการอัปเดตและในขณะเดียวกันก็ลดการมีเพศสัมพันธ์ระหว่างส่วนประกอบ GUI และข้อมูลพื้นฐาน
วิธีแก้ปัญหาที่ง่ายและไม่สามารถปรับขนาดได้คือการอ้างถึงส่วนประกอบของตารางและภาพ GUI ของวัตถุที่จัดการข้อมูลพื้นฐานเหล่านี้เพื่อให้วัตถุสามารถแจ้งส่วนประกอบ GUI เมื่อข้อมูลพื้นฐานเปลี่ยนแปลง เห็นได้ชัดว่าโซลูชันที่เรียบง่ายนี้แสดงให้เห็นถึงข้อบกพร่องของแอปพลิเคชันที่ซับซ้อนซึ่งจัดการส่วนประกอบ GUI ได้มากขึ้น ตัวอย่างเช่นมี 20 องค์ประกอบ GUI ที่ทั้งหมดพึ่งพาข้อมูลพื้นฐานดังนั้นวัตถุที่จัดการข้อมูลพื้นฐานจำเป็นต้องรักษาข้อมูลอ้างอิงถึงองค์ประกอบ 20 รายการเหล่านี้ เมื่อจำนวนวัตถุขึ้นอยู่กับข้อมูลที่เกี่ยวข้องเพิ่มขึ้นระดับของการมีเพศสัมพันธ์ระหว่างการจัดการข้อมูลและวัตถุกลายเป็นเรื่องยากที่จะควบคุม
ทางออกที่ดีกว่าอีกทางหนึ่งคือการอนุญาตให้วัตถุลงทะเบียนเพื่อรับสิทธิ์ในการอัปเดตข้อมูลที่น่าสนใจซึ่งตัวจัดการข้อมูลจะแจ้งวัตถุเหล่านั้นเมื่อข้อมูลเปลี่ยนแปลง ในแง่ของ Layman ให้วัตถุข้อมูลที่น่าสนใจบอกผู้จัดการ: "โปรดแจ้งให้ฉันทราบเมื่อข้อมูลเปลี่ยนแปลง" นอกจากนี้วัตถุเหล่านี้ไม่เพียงสามารถลงทะเบียนเพื่อรับการแจ้งเตือนการอัปเดตเท่านั้น แต่ยังยกเลิกการลงทะเบียนเพื่อให้แน่ใจว่าตัวจัดการข้อมูลจะไม่แจ้งวัตถุอีกต่อไปเมื่อข้อมูลเปลี่ยนแปลง ในคำจำกัดความดั้งเดิมของ GOF วัตถุที่ลงทะเบียนเพื่อรับการอัปเดตเรียกว่า "ผู้สังเกตการณ์" ตัวจัดการข้อมูลที่เกี่ยวข้องเรียกว่า "หัวเรื่อง" ข้อมูลที่ผู้สังเกตการณ์สนใจเรียกว่า "สถานะเป้าหมาย" กระบวนการลงทะเบียนเรียกว่า "เพิ่ม" ดังที่ได้กล่าวไว้ข้างต้นโหมดผู้สังเกตการณ์เรียกอีกอย่างว่าโหมด Publish-Subscribe สามารถเข้าใจได้ว่าลูกค้าสมัครสมาชิกกับผู้สังเกตการณ์เกี่ยวกับเป้าหมาย เมื่อสถานะเป้าหมายได้รับการอัปเดตเป้าหมายจะเผยแพร่การอัปเดตเหล่านี้ไปยังสมาชิก (รูปแบบการออกแบบนี้จะขยายไปยังสถาปัตยกรรมทั่วไปที่เรียกว่าสถาปัตยกรรม Publish-Subscribe) แนวคิดเหล่านี้สามารถแสดงได้ด้วยแผนภาพคลาสต่อไปนี้:
concereteobserver ใช้เพื่อรับการเปลี่ยนแปลงสถานะการอัปเดตและส่งผ่านการอ้างอิงไปยัง conceretesubject ไปยังตัวสร้าง สิ่งนี้ให้การอ้างอิงถึงเรื่องที่เฉพาะเจาะจงสำหรับผู้สังเกตการณ์เฉพาะซึ่งสามารถรับการอัปเดตได้เมื่อรัฐเปลี่ยนแปลง พูดง่ายๆคือผู้สังเกตการณ์เฉพาะจะได้รับการบอกกล่าวให้อัปเดตหัวข้อและในขณะเดียวกันก็ใช้การอ้างอิงในคอนสตรัคเตอร์เพื่อให้ได้สถานะของหัวข้อเฉพาะและในที่สุดก็จัดเก็บวัตถุสถานะการค้นหาเหล่านี้ภายใต้คุณสมบัติการสังเกตการณ์ของผู้สังเกตการณ์เฉพาะ กระบวนการนี้แสดงในแผนภาพลำดับต่อไปนี้:
ความเชี่ยวชาญของโมเดลคลาสสิก <br /> แม้ว่าโมเดลผู้สังเกต
1. ระบุพารามิเตอร์ไปยังวัตถุสถานะและส่งผ่านไปยังวิธีการอัปเดตที่เรียกโดยผู้สังเกตการณ์ ในโหมดคลาสสิกเมื่อผู้สังเกตการณ์ได้รับแจ้งว่าสถานะของเรื่องมีการเปลี่ยนแปลงสถานะที่อัปเดตจะได้รับโดยตรงจากเรื่อง สิ่งนี้ต้องการให้ผู้สังเกตการณ์บันทึกการอ้างอิงวัตถุไปยังสถานะที่ดึงมา สิ่งนี้เกิดจากการอ้างอิงแบบวงกลมการอ้างอิงของคอนกรีตพ. จุดไปยังรายการผู้สังเกตการณ์และการอ้างอิงของคอนกรีตบอสเซิร์ฟเวอร์ชี้ไปที่คอนกรีตที่สามารถรับสถานะเรื่องได้ นอกเหนือจากการได้รับสถานะที่อัปเดตแล้วไม่มีการเชื่อมต่อระหว่างผู้สังเกตการณ์และเรื่องที่ลงทะเบียนเพื่อฟัง ผู้สังเกตการณ์ใส่ใจเกี่ยวกับวัตถุของรัฐไม่ใช่เรื่องของตัวเอง กล่าวคือในหลาย ๆ กรณีคอนกรีตเซิร์ฟเวอร์และคอนกรีตมีการเชื่อมโยงเข้าด้วยกัน ในทางตรงกันข้ามเมื่อ concretesubject เรียกฟังก์ชั่นการอัปเดตวัตถุสถานะจะถูกส่งไปยังคอนกรีตบอร์เวอร์และทั้งสองไม่จำเป็นต้องเชื่อมโยง ความสัมพันธ์ระหว่างคอนกรีตเซิร์ฟเวอร์และวัตถุของรัฐลดระดับการพึ่งพาระหว่างผู้สังเกตการณ์และรัฐ (ดูบทความของมาร์ตินฟาวเลอร์สำหรับความแตกต่างเพิ่มเติมในการเชื่อมโยงและการพึ่งพา)
2. รวมคลาสบทคัดย่อเรื่องและคอนกรีตในคลาส Singlesubject ในกรณีส่วนใหญ่การใช้คลาสนามธรรมในหัวเรื่องไม่ได้ปรับปรุงความยืดหยุ่นของโปรแกรมและความสามารถในการปรับขนาดดังนั้นการรวมคลาสนามธรรมและคลาสคอนกรีตนี้ทำให้การออกแบบง่ายขึ้น
หลังจากรวมทั้งสองรุ่นพิเศษนี้แผนภาพคลาสที่เรียบง่ายมีดังนี้:
ในโมเดลพิเศษเหล่านี้โครงสร้างคลาสคงที่นั้นง่ายขึ้นอย่างมากและการโต้ตอบระหว่างคลาสก็ง่ายขึ้นเช่นกัน แผนภาพลำดับในเวลานี้มีดังนี้:
คุณสมบัติอีกประการหนึ่งของโหมดความเชี่ยวชาญคือการลบตัวแปรสมาชิกสังเกตการณ์ของคอนกรีต บางครั้งผู้สังเกตการณ์เฉพาะไม่จำเป็นต้องบันทึกสถานะล่าสุดของเรื่อง แต่จำเป็นต้องตรวจสอบสถานะของเรื่องเมื่อมีการอัปเดตสถานะ ตัวอย่างเช่นหากผู้สังเกตการณ์อัปเดตค่าของตัวแปรสมาชิกไปยังเอาต์พุตมาตรฐานเขาสามารถลบการสังเกตการณ์ซึ่งจะช่วยขจัดความสัมพันธ์ระหว่างคอนกรีตบอสเซิร์ฟเวอร์และระดับรัฐ
กฎการตั้งชื่อทั่วไป <br /> โหมดคลาสสิกและแม้แต่โหมดมืออาชีพที่กล่าวถึงคำพูดข้างต้นใช้คำเช่นแนบ, detach และ observer ในขณะที่การใช้งาน Java จำนวนมากใช้พจนานุกรมที่แตกต่างกันรวมถึงการลงทะเบียน, unregister, ผู้ฟังและอื่น ๆ ชื่อเฉพาะของวัตถุสถานะขึ้นอยู่กับสถานการณ์ที่ใช้ในโหมดผู้สังเกตการณ์ ตัวอย่างเช่นในโหมดผู้สังเกตการณ์ในฉากที่ผู้ฟังฟังเหตุการณ์ที่เกิดขึ้นผู้ฟังที่ลงทะเบียนจะได้รับการแจ้งเตือนเมื่อเหตุการณ์เกิดขึ้น วัตถุสถานะในเวลานี้คือเหตุการณ์นั่นคือไม่ว่าเหตุการณ์จะเกิดขึ้นหรือไม่
ในแอปพลิเคชันจริงการตั้งชื่อของเป้าหมายไม่ค่อยมีหัวเรื่อง ตัวอย่างเช่นสร้างแอพเกี่ยวกับสวนสัตว์ลงทะเบียนผู้ฟังหลายคนเพื่อสังเกตชั้นเรียนสวนสัตว์และรับการแจ้งเตือนเมื่อสัตว์ใหม่เข้าสวนสัตว์ เป้าหมายในกรณีนี้คือคลาสสวนสัตว์ เพื่อให้คำศัพท์สอดคล้องกับโดเมนปัญหาที่กำหนดคำว่า "หัวเรื่อง" จะไม่ถูกนำมาใช้ซึ่งหมายความว่าคลาสสวนสัตว์จะไม่ได้รับการตั้งชื่อ zoosubject
การตั้งชื่อของผู้ฟังมักจะตามมาด้วยคำต่อท้ายของผู้ฟัง ตัวอย่างเช่นผู้ฟังที่กล่าวถึงข้างต้นเพื่อตรวจสอบสัตว์ใหม่จะได้รับการตั้งชื่อว่า AnimaladdedListener ในทำนองเดียวกันการตั้งชื่อของฟังก์ชั่นเช่นการลงทะเบียนการไม่ลงทะเบียนและการแจ้งเตือนมักจะถูกต่อท้ายโดยชื่อผู้ฟังที่เกี่ยวข้อง ตัวอย่างเช่นการลงทะเบียนการไม่ลงทะเบียนและการแจ้งการทำงานของ AnimaladdedListener จะได้รับการตั้งชื่อว่า RegisteranimaladdedListener, UnregisteranimaladdedListener และ NotifyanimaladdedDlisteners ควรสังเกตว่าชื่อฟังก์ชั่นการแจ้งเตือนถูกใช้เพราะฟังก์ชั่นการแจ้งเตือนนั้นจัดการกับผู้ฟังหลายคนแทนที่จะเป็นผู้ฟังคนเดียว
วิธีการตั้งชื่อนี้จะดูยาวนานและโดยปกติแล้ววิชาจะลงทะเบียนผู้ฟังหลายประเภท ตัวอย่างเช่นในตัวอย่างสวนสัตว์ที่กล่าวถึงข้างต้นในสวนสัตว์นอกเหนือจากการลงทะเบียนผู้ฟังใหม่สำหรับการตรวจสอบสัตว์มันยังต้องลงทะเบียนผู้ฟังสัตว์เพื่อลดผู้ฟัง ในเวลานี้จะมีฟังก์ชั่นการลงทะเบียนสองฟังก์ชั่น: (registeranimaladdedListener และ registeranimalremovedListener ด้วยวิธีนี้ประเภทของผู้ฟังจะใช้เป็นตัวคัดเลือกเพื่อระบุประเภทของผู้สังเกตการณ์อีกวิธีหนึ่งคือการสร้างฟังก์ชั่นการลงทะเบียน
ไวยากรณ์สำนวนอื่นคือการใช้กับคำนำหน้าแทนการอัปเดตตัวอย่างเช่นฟังก์ชั่นการอัปเดตมีชื่อ onanimaladded แทน updateanimaladded สถานการณ์นี้เป็นเรื่องธรรมดามากขึ้นเมื่อผู้ฟังได้รับการแจ้งเตือนสำหรับลำดับเช่นการเพิ่มสัตว์ลงในรายการ แต่ไม่ค่อยใช้เพื่ออัปเดตข้อมูลแยกต่างหากเช่นชื่อของสัตว์
ถัดไปบทความนี้จะใช้กฎสัญลักษณ์ของ Java แม้ว่ากฎเชิงสัญลักษณ์จะไม่เปลี่ยนการออกแบบที่แท้จริงและการใช้งานระบบ แต่เป็นหลักการพัฒนาที่สำคัญในการใช้คำศัพท์ที่นักพัฒนาคนอื่นคุ้นเคยดังนั้นคุณต้องคุ้นเคยกับกฎสัญลักษณ์สัญลักษณ์ผู้สังเกตการณ์ใน Java ที่อธิบายไว้ข้างต้น แนวคิดข้างต้นจะอธิบายไว้ด้านล่างโดยใช้ตัวอย่างง่ายๆในสภาพแวดล้อม Java 8
ตัวอย่างง่ายๆ
นอกจากนี้ยังเป็นตัวอย่างของสวนสัตว์ที่กล่าวถึงข้างต้น การใช้อินเทอร์เฟซ API ของ Java8 เพื่อใช้ระบบง่าย ๆ อธิบายหลักการพื้นฐานของรูปแบบผู้สังเกตการณ์ ปัญหาอธิบายเป็น:
สร้างสวนสัตว์ระบบช่วยให้ผู้ใช้ฟังและยกเลิกสถานะของการเพิ่มสัตว์วัตถุใหม่และสร้างผู้ฟังเฉพาะซึ่งรับผิดชอบในการส่งสัญญาณของสัตว์ใหม่
จากการเรียนรู้ก่อนหน้าของรูปแบบผู้สังเกตการณ์เรารู้ว่าการใช้แอปพลิเคชันดังกล่าวเราจำเป็นต้องสร้างคลาส 4 คลาสโดยเฉพาะ:
ก่อนอื่นเราสร้างคลาสสัตว์ซึ่งเป็นวัตถุ Java อย่างง่ายที่มีตัวแปรสมาชิกชื่อตัวสร้าง, getters และวิธีการตั้งค่า รหัสมีดังนี้:
สัตว์ชั้นสาธารณะ {ชื่อสตริงส่วนตัว; สัตว์สาธารณะ (ชื่อสตริง) {this.name = name; } สตริงสาธารณะ getName () {return this.name; } โมฆะสาธารณะ setName (ชื่อสตริง) {this.name = name; -ใช้คลาสนี้เพื่อเป็นตัวแทนของวัตถุสัตว์จากนั้นคุณสามารถสร้างอินเทอร์เฟซ AnimalAddedListener:
Public Interface AnimaladdedListener {โมฆะสาธารณะ onanimaladded (สัตว์สัตว์);}สองชั้นแรกนั้นง่ายมากดังนั้นฉันจะไม่แนะนำพวกเขาอย่างละเอียด ถัดไปสร้างคลาส Zoo:
สวนสาธารณะระดับสาธารณะ {รายการส่วนตัว <Anity> สัตว์ = New ArrayList <> (); รายการส่วนตัว <AnverAddedListener> Listeners = new ArrayList <> (); โมฆะสาธารณะ addanimal (สัตว์สัตว์) {// เพิ่มสัตว์ลงในรายการสัตว์ this.animals.add (สัตว์); // แจ้งรายชื่อผู้ฟังที่ลงทะเบียนนี้ NotifyanimaladdedListeners (สัตว์); } โมฆะสาธารณะ registeranimaladdedListener (AnimalAddedListener Listener) {// เพิ่มผู้ฟังลงในรายชื่อผู้ฟังที่ลงทะเบียน this.listeners.add (ผู้ฟัง); } โมฆะสาธารณะ UnregisteranimaladdedListener (AnimalAddedListener Listener) {// ลบผู้ฟังออกจากรายชื่อผู้ฟังที่ลงทะเบียน this.listeners.remove (ผู้ฟัง); } โมฆะได้รับการป้องกัน NotifyanimaladdedListeners (สัตว์สัตว์) {// แจ้งผู้ฟังแต่ละคนในรายชื่อผู้ฟังที่ลงทะเบียนผู้ฟัง this.listeners.foreach (ผู้ฟัง -> listener.updateanimaladded (สัตว์)); -การเปรียบเทียบนี้ซับซ้อนกว่าสองก่อนหน้า มันมีสองรายการหนึ่งรายการถูกใช้เพื่อเก็บสัตว์ทั้งหมดในสวนสัตว์และอีกรายการหนึ่งใช้เพื่อจัดเก็บผู้ฟังทั้งหมด เนื่องจากวัตถุที่เก็บไว้ในสัตว์และคอลเลกชันผู้ฟังนั้นง่ายบทความนี้เลือก ArrayList สำหรับการจัดเก็บ โครงสร้างข้อมูลเฉพาะของผู้ฟังที่เก็บไว้นั้นขึ้นอยู่กับปัญหา ตัวอย่างเช่นสำหรับปัญหาสวนสัตว์ที่นี่หากผู้ฟังมีความสำคัญคุณควรเลือกโครงสร้างข้อมูลอื่นหรือเขียนอัลกอริทึมการลงทะเบียนของผู้ฟัง
การใช้งานการลงทะเบียนและการลบเป็นทั้งวิธีการแทนอย่างง่าย: ผู้ฟังแต่ละคนจะถูกเพิ่มหรือลบออกจากรายการการฟังของผู้ฟังเป็นพารามิเตอร์ การใช้งานฟังก์ชั่นการแจ้งเตือนนั้นปิดเล็กน้อยจากรูปแบบมาตรฐานของรูปแบบผู้สังเกตการณ์ มันรวมถึงพารามิเตอร์อินพุต: สัตว์ที่เพิ่มขึ้นใหม่เพื่อให้ฟังก์ชั่นการแจ้งเตือนสามารถผ่านการอ้างอิงสัตว์ที่เพิ่มขึ้นใหม่ไปยังผู้ฟัง ใช้ฟังก์ชั่น foreach ของสตรีม API เพื่อสำรวจผู้ฟังและดำเนินการฟังก์ชั่น TheonAnimaladded ในผู้ฟังแต่ละคน
ในฟังก์ชั่น addanimal วัตถุและผู้ฟังที่เพิ่มเข้ามาใหม่จะถูกเพิ่มเข้าไปในรายการที่เกี่ยวข้อง หากความซับซ้อนของกระบวนการแจ้งเตือนไม่ได้นำมาพิจารณาตรรกะนี้ควรรวมอยู่ในวิธีการโทรที่สะดวก คุณจะต้องผ่านการอ้างอิงถึงวัตถุสัตว์ที่เพิ่มขึ้นใหม่ นี่คือเหตุผลที่การใช้งานเชิงตรรกะของผู้ฟังการแจ้งเตือนถูกห่อหุ้มในฟังก์ชั่น NotifyanimaladdedListeners ซึ่งถูกกล่าวถึงในการดำเนินการของ Addanimal
นอกเหนือจากปัญหาเชิงตรรกะของฟังก์ชั่นการแจ้งเตือนแล้วยังมีความจำเป็นที่จะต้องเน้นประเด็นการโต้เถียงเกี่ยวกับการมองเห็นฟังก์ชั่นการแจ้งเตือน ในโมเดลผู้สังเกตการณ์แบบคลาสสิกดังที่ GOF กล่าวไว้ในหน้า 301 ของรูปแบบการออกแบบหนังสือฟังก์ชั่นการแจ้งเตือนเป็นแบบสาธารณะ แต่ถึงแม้ว่าจะใช้ในรูปแบบคลาสสิก แต่นี่ไม่ได้หมายความว่ามันจะต้องเปิดเผยต่อสาธารณะ การเลือกการมองเห็นควรขึ้นอยู่กับแอปพลิเคชัน ตัวอย่างเช่นในตัวอย่างสวนสัตว์ของบทความนี้ฟังก์ชั่นการแจ้งเตือนเป็นประเภทที่ได้รับการปกป้องและไม่ต้องการให้แต่ละวัตถุเริ่มการแจ้งเตือนผู้สังเกตการณ์ที่ลงทะเบียน จำเป็นต้องตรวจสอบให้แน่ใจว่าวัตถุสามารถสืบทอดฟังก์ชั่นจากคลาสแม่ แน่นอนว่านี่ไม่ใช่กรณีที่แน่นอน มีความจำเป็นที่จะต้องพิจารณาว่าคลาสใดที่สามารถเปิดใช้งานฟังก์ชั่นการแจ้งเตือนจากนั้นพิจารณาการมองเห็นของฟังก์ชั่น
ถัดไปคุณจะต้องใช้คลาส PrintnameanimaladdedListener คลาสนี้ใช้วิธี System.out.println เพื่อส่งออกชื่อของสัตว์ใหม่ รหัสเฉพาะมีดังนี้:
Public Class PrintnameanimaladdedListener ใช้ AnimaladdedListener {@Override โมฆะสาธารณะ updateanimaladded (สัตว์สัตว์) {// พิมพ์ชื่อของระบบสัตว์ที่เพิ่มเข้ามาใหม่ -ในที่สุดเราจำเป็นต้องใช้ฟังก์ชั่นหลักที่ขับเคลื่อนแอปพลิเคชัน:
คลาสสาธารณะหลัก {โมฆะคงที่สาธารณะหลัก (สตริง [] args) {// สร้างสวนสัตว์เพื่อจัดเก็บสัตว์ Zoo Zoo = สวนสัตว์ใหม่ (); // ลงทะเบียนผู้ฟังที่จะได้รับการแจ้งเตือนเมื่อสัตว์ถูกเพิ่มสวนสัตว์ registeranimaladdedListener (ใหม่ printnameanimaladdedlistener ()); // เพิ่มสัตว์แจ้งให้ทราบถึง Zoo.Addanimal (สัตว์ใหม่ ("Tiger")); -ฟังก์ชั่นหลักเพียงแค่สร้างวัตถุสวนสัตว์ลงทะเบียนผู้ฟังที่ส่งออกชื่อสัตว์และสร้างวัตถุสัตว์ใหม่เพื่อกระตุ้นผู้ฟังที่ลงทะเบียน ผลลัพธ์สุดท้ายคือ:
เพิ่มสัตว์ใหม่ด้วยชื่อ 'Tiger'
เพิ่มผู้ฟัง
ข้อดีของโหมดผู้สังเกตการณ์จะปรากฏขึ้นอย่างสมบูรณ์เมื่อผู้ฟังได้รับการสร้างใหม่และเพิ่มเข้าไปในวัตถุ ตัวอย่างเช่นหากคุณต้องการเพิ่มผู้ฟังที่คำนวณจำนวนสัตว์ทั้งหมดในสวนสัตว์คุณเพียงแค่ต้องสร้างคลาสผู้ฟังที่เฉพาะเจาะจงและลงทะเบียนกับคลาสสวนสัตว์โดยไม่ต้องปรับเปลี่ยนคลาส Zoo การเพิ่มรหัสการนับจำนวนมาก
การนับชั้นเรียนสาธารณะการนับแอนนิเมalddedListenerใช้ AnimaladdedListener {สัตว์ inttatic ส่วนตัวแบบคงที่ส่วนตัว = 0; @Override โมฆะสาธารณะ updateanimaladded (สัตว์สัตว์) {// เพิ่มจำนวนสัตว์ที่สัตว์ได้รับการดูแล ++; // พิมพ์จำนวนสัตว์ System.out.println ("เพิ่มสัตว์ทั้งหมด:" + สัตว์ที่ได้รับการดูแล); -ฟังก์ชั่นหลักที่ได้รับการแก้ไขมีดังนี้:
คลาสสาธารณะหลัก {โมฆะคงที่สาธารณะหลัก (สตริง [] args) {// สร้างสวนสัตว์เพื่อจัดเก็บสัตว์ Zoo Zoo = สวนสัตว์ใหม่ (); // ลงทะเบียนผู้ฟังที่จะได้รับแจ้งเมื่อสัตว์ถูกเพิ่มสวนสัตว์ registeranimaladdedListener (printnameanimaladdedlistener ()); Zoo.registeranimaladdedListener (ใหม่ CountinganimaladdedListener ()); // เพิ่มสัตว์แจ้งให้ทราบถึง Zoo.Addanimal (สัตว์ใหม่ ("Tiger")); Zoo.Addanimal (สัตว์ใหม่ ("Lion")); Zoo.Addanimal (สัตว์ใหม่ ("Bear")); -ผลลัพธ์ผลลัพธ์คือ:
เพิ่มสัตว์ใหม่ที่มีชื่อ 'สัตว์ Tiger'total เพิ่ม: 1 เพิ่มสัตว์ใหม่ที่มีชื่อ' สัตว์สิงโต 'เพิ่ม: 2 เพิ่มสัตว์ใหม่ที่มีชื่อ' สัตว์หมีเพิ่ม: 3: 3
ผู้ใช้สามารถสร้างผู้ฟังได้หากแก้ไขรหัสการลงทะเบียนผู้ฟังเท่านั้น ความสามารถในการปรับขนาดนี้ส่วนใหญ่เป็นเพราะตัวแบบเกี่ยวข้องกับอินเทอร์เฟซ Observer แทนที่จะเชื่อมโยงโดยตรงกับคอนกรีต ตราบใดที่อินเทอร์เฟซยังไม่ได้รับการแก้ไขไม่จำเป็นต้องแก้ไขหัวเรื่องของอินเทอร์เฟซ
คลาสภายในที่ไม่ระบุชื่อฟังก์ชั่นแลมบ์ดาและการลงทะเบียนผู้ฟัง
การปรับปรุงที่สำคัญใน Java 8 คือการเพิ่มคุณสมบัติการทำงานเช่นการเพิ่มฟังก์ชั่นแลมบ์ดา ก่อนที่จะแนะนำฟังก์ชั่นแลมบ์ดา Java ให้ฟังก์ชั่นที่คล้ายกันผ่านคลาสภายในที่ไม่ระบุชื่อซึ่งยังคงใช้ในแอปพลิเคชันที่มีอยู่จำนวนมาก ในโหมดผู้สังเกตการณ์ผู้ฟังใหม่สามารถสร้างได้ตลอดเวลาโดยไม่ต้องสร้างคลาส Observer เฉพาะ ตัวอย่างเช่นคลาส printnameanimaladdedlistener สามารถนำไปใช้ในฟังก์ชั่นหลักที่มีคลาสภายในที่ไม่ระบุชื่อ รหัสการใช้งานเฉพาะมีดังนี้:
คลาสสาธารณะหลัก {โมฆะคงที่สาธารณะหลัก (สตริง [] args) {// สร้างสวนสัตว์เพื่อจัดเก็บสัตว์ Zoo Zoo = สวนสัตว์ใหม่ (); // ลงทะเบียนผู้ฟังที่จะได้รับแจ้งเมื่อมีการเพิ่มสัตว์ zoo.registeranimaladdedListener (ใหม่ imentaddedListener () {@Override โมฆะสาธารณะ updateanimaladded (สัตว์สัตว์) {// พิมพ์ชื่อของสัตว์ที่เพิ่มขึ้นใหม่) // เพิ่มสัตว์แจ้งให้ทราบถึง Zoo.Addanimal (สัตว์ใหม่ ("Tiger")); -ในทำนองเดียวกันฟังก์ชั่นแลมบ์ดาสามารถใช้เพื่อทำงานให้เสร็จสมบูรณ์:
คลาสสาธารณะหลัก {โมฆะคงที่สาธารณะหลัก (สตริง [] args) {// สร้างสวนสัตว์เพื่อจัดเก็บสัตว์ Zoo Zoo = สวนสัตว์ใหม่ (); // ลงทะเบียนผู้ฟังที่จะได้รับแจ้งเมื่อสัตว์ถูกเพิ่ม Zoo.registeranimaladdedListener ((สัตว์) -> system.out.println ("เพิ่มสัตว์ใหม่ที่มีชื่อ '" iment.getName () + "'")); // เพิ่มสัตว์แจ้งให้ทราบถึง Zoo.Addanimal (สัตว์ใหม่ ("Tiger")); -ควรสังเกตว่าฟังก์ชั่นแลมบ์ดาเหมาะสำหรับสถานการณ์ที่มีเพียงฟังก์ชั่นเดียวในอินเทอร์เฟซผู้ฟัง แม้ว่าข้อกำหนดนี้ดูเหมือนจะเข้มงวด แต่ผู้ฟังหลายคนเป็นฟังก์ชั่นเดียวเช่น AnimaladdedListener ในตัวอย่าง หากอินเทอร์เฟซมีหลายฟังก์ชั่นคุณสามารถเลือกใช้คลาสภายในที่ไม่ระบุชื่อ
มีปัญหาดังกล่าวเกี่ยวกับการลงทะเบียนโดยนัยของผู้ฟังที่สร้างขึ้น: เนื่องจากวัตถุถูกสร้างขึ้นภายในขอบเขตของการโทรลงทะเบียนจึงเป็นไปไม่ได้ที่จะเก็บข้อมูลอ้างอิงไปยังผู้ฟังเฉพาะ ซึ่งหมายความว่าผู้ฟังที่ลงทะเบียนผ่านฟังก์ชั่นแลมบ์ดาหรือคลาสภายในที่ไม่ระบุชื่อไม่สามารถเพิกถอนได้เนื่องจากฟังก์ชั่นการเพิกถอนต้องมีการอ้างอิงถึงผู้ฟังที่ลงทะเบียน วิธีง่ายๆในการแก้ปัญหานี้คือการส่งคืนการอ้างอิงไปยังผู้ฟังที่ลงทะเบียนในฟังก์ชัน registeranimaladdedListener ด้วยวิธีนี้คุณสามารถยกเลิกการลงทะเบียนผู้ฟังที่สร้างด้วยฟังก์ชั่นแลมบ์ดาหรือคลาสภายในที่ไม่ระบุชื่อ รหัสวิธีที่ได้รับการปรับปรุงมีดังนี้:
Public AnimaladdedListener RegisteranimaladdedListener (AnimaladdedListener Listener) {// เพิ่มผู้ฟังลงในรายชื่อผู้ฟังที่ลงทะเบียน this.listeners.add (ผู้ฟัง); ส่งคืนผู้ฟัง;}รหัสไคลเอนต์สำหรับการโต้ตอบฟังก์ชั่นที่ออกแบบใหม่มีดังนี้:
คลาสสาธารณะหลัก {โมฆะคงที่สาธารณะหลัก (สตริง [] args) {// สร้างสวนสัตว์เพื่อจัดเก็บสัตว์ Zoo Zoo = สวนสัตว์ใหม่ (); // ลงทะเบียนผู้ฟังที่จะได้รับแจ้งเมื่อสัตว์ถูกเพิ่ม imentaddedListener Listener = zoo.registeranimaladdedListener ((สัตว์) -> system.out.println ("เพิ่มสัตว์ใหม่ที่มีชื่อ" + iment.getName () + "")); // เพิ่มสัตว์แจ้งให้ทราบถึง Zoo.Addanimal (สัตว์ใหม่ ("Tiger")); // ยกเลิกการลงทะเบียนสวนสัตว์ผู้ฟัง UNREGISTERANINAMALADDEDLISTENER (ผู้ฟัง); // เพิ่มสัตว์อื่นซึ่งจะไม่พิมพ์ชื่อเนื่องจากผู้ฟัง // ได้รับการลงทะเบียนก่อนหน้านี้ zoo.addanimal (สัตว์ใหม่ ("Lion")); -ผลลัพธ์ผลลัพธ์ในเวลานี้จะเพิ่มสัตว์ใหม่ที่มีชื่อ 'Tiger' เท่านั้นเนื่องจากผู้ฟังถูกยกเลิกก่อนที่จะเพิ่มสัตว์ที่สอง:
เพิ่มสัตว์ใหม่ด้วยชื่อ 'Tiger'
หากมีการใช้โซลูชันที่ซับซ้อนมากขึ้นฟังก์ชั่นการลงทะเบียนยังสามารถส่งคืนคลาสผู้รับเพื่อให้ผู้ฟังที่ไม่ได้ลงทะเบียนถูกเรียกเช่น:
Public Class AnimaladdedListenerreceipt {Private Final AnimaleddedListener Listener; Public AnimaleddedListenerreceipt (AnimaladdedListener Listener) {this.listener = ผู้ฟัง; } Public Final AnimaladdedListener getListener () {return this.listener; -ใบเสร็จรับเงินจะถูกใช้เป็นค่าส่งคืนของฟังก์ชันการลงทะเบียนและพารามิเตอร์อินพุตของฟังก์ชันการลงทะเบียนจะถูกยกเลิก ในเวลานี้การใช้งานสวนสัตว์มีดังนี้:
ชั้นเรียนสาธารณะ zoousingReceipt {// ... คุณลักษณะและตัวสร้างที่มีอยู่ ... AnimaleddedDlistenerreceipt RegisteranimaladdedListener (AnimaladdedListener ผู้ฟัง) {// เพิ่มผู้ฟังลงในรายชื่อผู้ฟังที่ลงทะเบียนนี้ คืนสัตว์ใหม่ imentaddlistenerreceipt (ผู้ฟัง); } โมฆะสาธารณะ unregisteranimaladdedListener (AnimaladdedListenerreceipt แผนกต้อนรับ) {// ลบผู้ฟังออกจากรายชื่อผู้ฟังที่ลงทะเบียน this.listeners.remove (receipt.getListener ()); } // ... วิธีการแจ้งเตือนที่มีอยู่ ... }กลไกการใช้งานที่ได้รับที่อธิบายไว้ข้างต้นช่วยให้การจัดเก็บข้อมูลสำหรับการโทรไปยังผู้ฟังเมื่อเพิกถอนนั่นคือถ้าอัลกอริทึมการลงทะเบียนการเพิกถอนขึ้นอยู่กับสถานะของผู้ฟังเมื่อผู้ลงทะเบียนผู้ฟังสถานะนี้จะถูกบันทึกไว้ หากการลงทะเบียนการเพิกถอนต้องมีการอ้างอิงถึงผู้ฟังที่ลงทะเบียนก่อนหน้านี้เท่านั้นเทคโนโลยีการรับจะปรากฏว่ามีปัญหาและไม่แนะนำ
นอกเหนือจากผู้ฟังที่เฉพาะเจาะจงโดยเฉพาะอย่างยิ่งวิธีที่พบบ่อยที่สุดในการลงทะเบียนผู้ฟังคือผ่านฟังก์ชั่นแลมบ์ดาหรือผ่านชั้นเรียนภายในที่ไม่ระบุชื่อ แน่นอนว่ามีข้อยกเว้นนั่นคือคลาสที่มีหัวเรื่องใช้อินเทอร์เฟซผู้สังเกตการณ์และลงทะเบียนผู้ฟังที่เรียกเป้าหมายอ้างอิง กรณีที่แสดงในรหัสต่อไปนี้:
Zoocontainer คลาสสาธารณะใช้ AnimaladdedListener {Zoo Zoo ส่วนตัว = สวนสัตว์ใหม่ (); zoocontainer สาธารณะ () {// ลงทะเบียนวัตถุนี้เป็นผู้ฟัง this.zoo.registeranimaladdedListener (นี่); } สวนสัตว์สาธารณะ getzoo () {return this.zoo; } @Override โมฆะสาธารณะ updateanimaladded (สัตว์สัตว์) {system.out.println ("เพิ่มสัตว์ด้วยชื่อ '" + iment.getName () + "" "); } โมฆะคงที่สาธารณะหลัก (สตริง [] args) {// สร้าง Zoocontainer Zoocontainer Zoocontainer = zoocontainer ใหม่ (); // เพิ่มสัตว์แจ้งผู้ฟังที่ได้รับการแจ้งเตือนอย่างดี zoocontainer.getzoo (). addanimal (สัตว์ใหม่ ("เสือ")); -วิธีการนี้เหมาะสำหรับกรณีง่ายๆเท่านั้นและรหัสดูเหมือนจะไม่เป็นมืออาชีพและยังคงเป็นที่นิยมมากกับนักพัฒนา Java ที่ทันสมัยดังนั้นจึงจำเป็นต้องเข้าใจว่าตัวอย่างนี้ทำงานอย่างไร เนื่องจาก Zoocontainer ใช้อินเทอร์เฟซ AnimalAddedListener ดังนั้นอินสแตนซ์ (หรือวัตถุ) ของ Zoocontainer จึงสามารถลงทะเบียนเป็น AnimaladdedListener ในคลาส zoocontainer การอ้างอิงนี้แสดงถึงอินสแตนซ์ของวัตถุปัจจุบันคือ zoocontainer และสามารถใช้เป็น AnimaladdedListener
โดยทั่วไปไม่จำเป็นต้องใช้คลาสคอนเทนเนอร์ทั้งหมดในการใช้งานฟังก์ชั่นดังกล่าวและคลาสคอนเทนเนอร์ที่ใช้อินเตอร์เฟสผู้ฟังสามารถเรียกใช้ฟังก์ชันการลงทะเบียนเรื่อง แต่เพียงแค่ผ่านการอ้างอิงไปยังฟังก์ชั่นการลงทะเบียนเป็นวัตถุฟัง ในบทต่อไปนี้คำถามที่พบบ่อยและโซลูชั่นสำหรับสภาพแวดล้อมแบบมัลติเธรดจะได้รับการแนะนำ
การดำเนินการตามความปลอดภัยของเธรด <br /> บทก่อนหน้านี้แนะนำการดำเนินการตามรูปแบบผู้สังเกตการณ์ในสภาพแวดล้อม Java ที่ทันสมัย แม้ว่ามันจะง่าย แต่สมบูรณ์ แต่การใช้งานนี้ไม่สนใจปัญหาสำคัญ: ความปลอดภัยของเธรด แอปพลิเคชัน Java ที่เปิดกว้างส่วนใหญ่มีหลายเธรดและโหมดผู้สังเกตการณ์ส่วนใหญ่จะใช้ในระบบมัลติเธรดหรืออะซิงโครนัส ตัวอย่างเช่นหากบริการภายนอกอัปเดตฐานข้อมูลแอปพลิเคชันจะได้รับข้อความแบบอะซิงโครนัสแล้วแจ้งให้ส่วนประกอบภายในอัปเดตในโหมดผู้สังเกตการณ์แทนที่จะลงทะเบียนโดยตรงและฟังบริการภายนอก
ความปลอดภัยของเธรดในโหมดผู้สังเกตการณ์ส่วนใหญ่มุ่งเน้นไปที่ร่างกายของโหมดเนื่องจากความขัดแย้งของเธรดมีแนวโน้มที่จะเกิดขึ้นเมื่อแก้ไขคอลเลกชันผู้ฟังที่ลงทะเบียน ตัวอย่างเช่นหนึ่งเธรดพยายามเพิ่มผู้ฟังใหม่ในขณะที่เธรดอื่น ๆ พยายามเพิ่มวัตถุสัตว์ใหม่ซึ่งก่อให้เกิดการแจ้งเตือนไปยังผู้ฟังที่ลงทะเบียนทั้งหมด เมื่อพิจารณาจากลำดับของลำดับเธรดแรกอาจหรือไม่อาจเสร็จสิ้นการลงทะเบียนผู้ฟังใหม่ก่อนที่ผู้ฟังที่ลงทะเบียนจะได้รับการแจ้งเตือนของสัตว์ที่เพิ่มเข้ามา นี่เป็นกรณีคลาสสิกของการแข่งขันทรัพยากรเธรดและเป็นปรากฏการณ์นี้ที่บอกนักพัฒนาว่าพวกเขาต้องการกลไกเพื่อความปลอดภัยของเธรด
ทางออกที่ง่ายที่สุดสำหรับปัญหานี้คือ: การดำเนินการทั้งหมดที่เข้าถึงหรือแก้ไขรายการฟังการลงทะเบียนจะต้องเป็นไปตามกลไกการซิงโครไนซ์ Java เช่น::
Public Synchronized AnimaladdedListener RegisteranimaladdedListener (AnimaladdedListener Listener) {/*...*/} โมฆะสาธารณะไม่ได้เป็นโมฆะ unregisteranimaldedlistener (AnimaladdedListener) {/* ..ด้วยวิธีนี้ในเวลาเดียวกันมีเพียงเธรดเดียวเท่านั้นที่สามารถแก้ไขหรือเข้าถึงรายการผู้ฟังที่ลงทะเบียนซึ่งสามารถหลีกเลี่ยงปัญหาการแข่งขันทรัพยากรได้สำเร็จ แต่ปัญหาใหม่เกิดขึ้นและข้อ จำกัด ดังกล่าวเข้มงวดเกินไป (สำหรับข้อมูลเพิ่มเติมเกี่ยวกับคำหลักที่ซิงโครไนซ์ ผ่านการซิงโครไนซ์วิธีการเข้าถึงรายการผู้ฟังพร้อมกันสามารถสังเกตได้ตลอดเวลา การลงทะเบียนและเพิกถอนผู้ฟังเป็นการดำเนินการเขียนสำหรับรายการผู้ฟังในขณะที่แจ้งให้ผู้ฟังเข้าถึงรายการผู้ฟังเป็นการดำเนินการแบบอ่านอย่างเดียว เนื่องจากการเข้าถึงผ่านการแจ้งเตือนเป็นการดำเนินการอ่านการดำเนินการแจ้งเตือนหลายครั้งสามารถดำเนินการพร้อมกันได้
ดังนั้นตราบใดที่ไม่มีการลงทะเบียนผู้ฟังหรือการเพิกถอนตราบใดที่การลงทะเบียนไม่ได้ลงทะเบียนตราบใดที่การแจ้งเตือนพร้อมกันจำนวนมากสามารถดำเนินการพร้อมกันได้โดยไม่ต้องเรียกใช้การแข่งขันทรัพยากรสำหรับรายชื่อผู้ฟังที่ลงทะเบียน แน่นอนว่าการแข่งขันทรัพยากรในสถานการณ์อื่น ๆ มีมานานแล้ว เพื่อแก้ปัญหานี้การล็อคทรัพยากรสำหรับ ReadWriteLock ได้รับการออกแบบมาเพื่อจัดการการดำเนินการอ่านและเขียนแยกต่างหาก รหัสการใช้งาน Threadsafe Safe Safe Safe ของคลาส Zoo Class มีดังนี้:
ชั้นเรียนสาธารณะ Threadsafezoo {ส่วนตัวสุดท้าย readWriteLock readWriteLock = ใหม่ reentRantReadWriteLock (); ได้รับการป้องกันการล็อคสุดท้าย readlock = readWriteLock.readlock (); ได้รับการป้องกันการล็อคสุดท้าย writeLock = readWriteLock.writeLock (); รายการส่วนตัว <Anity> สัตว์ = arrayList ใหม่ <> (); รายการส่วนตัว <AnverAddedListener> Listeners = new ArrayList <> (); โมฆะสาธารณะ addanimal (สัตว์สัตว์) {// เพิ่มสัตว์ลงในรายการสัตว์ this.animals.add (สัตว์); // แจ้งรายชื่อผู้ฟังที่ลงทะเบียนนี้ NotifyanimaladdedListeners (สัตว์); } Public AnimaledDlistener RegisteranimaladdedListener (AnimaladdedListener Listener) {// ล็อครายชื่อผู้ฟังเพื่อเขียน this.writeLock.lock (); ลอง {// เพิ่มผู้ฟังลงในรายการผู้ฟังที่ลงทะเบียน this.listeners.add (ผู้ฟัง); } ในที่สุด {// ปลดล็อก Writer Lock this.writeLock.unlock (); } return Listener; } โมฆะสาธารณะ unregisteranimaladdedListener (AnimalAddedListener Listener) {// ล็อครายชื่อผู้ฟังเพื่อเขียน this.writeLock.lock (); ลอง {// ลบผู้ฟังออกจากรายชื่อผู้ฟังที่ลงทะเบียน this.listeners.remove (ผู้ฟัง); } ในที่สุด {// ปลดล็อก Writer Lock this.writeLock.unlock (); }} โมฆะสาธารณะ NotifyanimaladdedListeners (สัตว์สัตว์) {// ล็อครายการผู้ฟังสำหรับการอ่าน this.readlock.lock (); ลอง {// แจ้งผู้ฟังแต่ละคนในรายการผู้ฟังที่ลงทะเบียน this.listeners.foreach (ผู้ฟัง -> listener.updateanimaladded (สัตว์)); } ในที่สุด {// ปลดล็อกตัวอ่านล็อค this.readlock.unlock (); -ผ่านการปรับใช้ดังกล่าวการใช้งานเรื่องสามารถมั่นใจได้ว่าความปลอดภัยของเธรดและหลายเธรดสามารถออกการแจ้งเตือนในเวลาเดียวกัน แต่ถึงกระนั้นก็ยังมีปัญหาการแข่งขันทรัพยากรสองอย่างที่ไม่สามารถเพิกเฉยได้:
การเข้าถึงผู้ฟังแต่ละคนพร้อมกัน หลายเธรดสามารถแจ้งผู้ฟังได้ว่าจำเป็นต้องใช้สัตว์ใหม่ซึ่งหมายความว่าผู้ฟังอาจถูกเรียกโดยหลายเธรดในเวลาเดียวกัน
การเข้าถึงรายการสัตว์พร้อมกัน หลายเธรดอาจเพิ่มวัตถุในรายการสัตว์ในเวลาเดียวกัน หากคำสั่งของการแจ้งเตือนมีผลกระทบอาจนำไปสู่การแข่งขันของทรัพยากรซึ่งต้องใช้กลไกการประมวลผลการดำเนินการพร้อมกันเพื่อหลีกเลี่ยงปัญหานี้ หากรายชื่อผู้ฟังที่ลงทะเบียนได้รับการแจ้งเตือนเพื่อเพิ่ม Animal2 แล้วได้รับการแจ้งเตือนเพื่อเพิ่ม Animal1 การแข่งขันทรัพยากรจะเกิดขึ้น อย่างไรก็ตามหากการเพิ่ม Animal1 และ Animal2 นั้นดำเนินการโดยเธรดที่แตกต่างกันก็เป็นไปได้ที่จะเพิ่มการเพิ่ม Animal1 ก่อน Animal2 โดยเฉพาะอย่างยิ่งเธรด 1 เพิ่ม Animal1 ก่อนที่จะแจ้งผู้ฟังและล็อคโมดูลเธรด 2 เพิ่ม Animal2 และแจ้งให้ผู้ฟังทราบแล้วเธรด 1 จะแจ้งผู้ฟังว่า Animal1 ได้รับการเพิ่ม แม้ว่าการแข่งขันทรัพยากรจะถูกเพิกเฉยเมื่อไม่ได้รับการพิจารณาลำดับของลำดับ แต่ปัญหานั้นเป็นเรื่องจริง
对监听器的并发访问
并发访问监听器可以通过保证监听器的线程安全来实现。秉承着类的“责任自负”精神,监听器有“义务”确保自身的线程安全。例如,对于前面计数的监听器,多线程的递增或递减动物数量可能导致线程安全问题,要避免这一问题,动物数的计算必须是原子操作(原子变量或方法同步),具体解决代码如下:
public class ThreadSafeCountingAnimalAddedListener implements AnimalAddedListener { private static AtomicLong animalsAddedCount = new AtomicLong(0); @Override public void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount.incrementAndGet(); // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); -方法同步解决方案代码如下:
public class CountingAnimalAddedListener implements AnimalAddedListener { private static int animalsAddedCount = 0; @Override public synchronized void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount++; // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); -要强调的是监听器应该保证自身的线程安全,subject需要理解监听器的内部逻辑,而不是简单确保对监听器的访问和修改的线程安全。否则,如果多个subject共用同一个监听器,那每个subject类都要重写一遍线程安全的代码,显然这样的代码不够简洁,因此需要在监听器类内实现线程安全。
监听器的有序通知当要求监听器有序执行时,读写锁就不能满足需求了,而需要引入一个新的机制,可以保证notify函数的调用顺序和animal添加到zoo的顺序一致。有人尝试过用方法同步来实现,然而根据Oracle文档中的方法同步介绍,可知方法同步并不提供操作执行的顺序管理。它只是保证原子操作,也就是说操作不会被打断,并不能保证先来先执行(FIFO)的线程顺序。ReentrantReadWriteLock可以实现这样的执行顺序,代码如下:
public class OrderedThreadSafeZoo { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); protected final Lock readLock = readWriteLock.readLock(); protected final Lock writeLock = readWriteLock.writeLock(); private List<Animal> animals = new ArrayList<>(); private List<AnimalAddedListener> listeners = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyAnimalAddedListeners(animal); } public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Add the listener to the list of registered listeners this.listeners.add(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } return listener; } public void unregisterAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } } public void notifyAnimalAddedListeners (Animal animal) { // Lock the list of listeners for reading this.readLock.lock(); try { // Notify each of the listeners in the list of registered listeners this.listeners.forEach(listener -> listener.updateAnimalAdded(animal)); } finally { // Unlock the reader lock this.readLock.unlock(); -这样的实现方式,register, unregister和notify函数将按照先进先出(FIFO)的顺序获得读写锁权限。例如,线程1注册一个监听器,线程2在开始执行注册操作后试图通知已注册的监听器,线程3在线程2等待只读锁的时候也试图通知已注册的监听器,采用fair-ordering方式,线程1先完成注册操作,然后线程2可以通知监听器,最后线程3通知监听器。这样保证了action的执行顺序和开始顺序一致。
如果采用方法同步,虽然线程2先排队等待占用资源,线程3仍可能比线程2先获得资源锁,而且不能保证线程2比线程3先通知监听器。问题的关键所在:fair-ordering方式可以保证线程按照申请资源的顺序执行。读写锁的顺序机制很复杂,应参照ReentrantReadWriteLock的官方文档以确保锁的逻辑足够解决问题。
截止目前实现了线程安全,在接下来的章节中将介绍提取主题的逻辑并将其mixin类封装为可重复代码单元的方式优缺点。
主题逻辑封装到Mixin类<br />把上述的观察者模式设计实现封装到目标的mixin类中很具吸引力。通常来说,观察者模式中的观察者包含已注册的监听器的集合;负责注册新的监听器的register函数;负责撤销注册的unregister函数和负责通知监听器的notify函数。对于上述的动物园的例子,zoo类除动物列表是问题所需外,其他所有操作都是为了实现主题的逻辑。
Mixin类的案例如下所示,需要说明的是为使代码更为简洁,此处去掉关于线程安全的代码:
public abstract class ObservableSubjectMixin<ListenerType> { private List<ListenerType> listeners = new ArrayList<>(); public ListenerType registerListener (ListenerType listener) { // Add the listener to the list of registered listeners this.listeners.add(listener); return listener; } public void unregisterAnimalAddedListener (ListenerType listener) { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } public void notifyListeners (Consumer<? super ListenerType> algorithm) { // Execute some function on each of the listeners this.listeners.forEach(algorithm); -正因为没有提供正在注册的监听器类型的接口信息,不能直接通知某个特定的监听器,所以正需要保证通知功能的通用性,允许客户端添加一些功能,如接受泛型参数类型的参数匹配,以适用于每个监听器,具体实现代码如下:
public class ZooUsingMixin extends ObservableSubjectMixin<AnimalAddedListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.updateAnimalAdded(animal)); -Mixin类技术的最大优势是把观察者模式的Subject封装到一个可重复调用的类中,而不是在每个subject类中都重复写这些逻辑。此外,这一方法使得zoo类的实现更为简洁,只需要存储动物信息,而不用再考虑如何存储和通知监听器。
然而,使用mixin类并非只有优点。比如,如果要存储多个类型的监听器怎么办?例如,还需要存储监听器类型AnimalRemovedListener。mixin类是抽象类,Java中不能同时继承多个抽象类,而且mixin类不能改用接口实现,这是因为接口不包含state,而观察者模式中state需要用来保存已经注册的监听器列表。
其中的一个解决方案是创建一个动物增加和减少时都会通知的监听器类型ZooListener,代码如下所示:
public interface ZooListener { public void onAnimalAdded (Animal animal); public void onAnimalRemoved (Animal animal);}这样就可以使用该接口实现利用一个监听器类型对zoo状态各种变化的监听了:
public class ZooUsingMixin extends ObservableSubjectMixin<ZooListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalAdded(animal)); } public void removeAnimal (Animal) animal) { // Remove the animal from the list of animals this.animals.remove(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalRemoved(animal)); -将多个监听器类型合并到一个监听器接口中确实解决了上面提到的问题,但仍旧存在不足之处,接下来的章节会详细讨论。
Multi-Method监听器和适配器
在上述方法,监听器的接口中实现的包含太多函数,接口就过于冗长,例如,Swing MouseListener就包含5个必要的函数。尽管可能只会用到其中一个,但是只要用到鼠标点击事件就必须要添加这5个函数,更多可能是用空函数体来实现剩下的函数,这无疑会给代码带来不必要的混乱。
其中一种解决方案是创建适配器(概念来自GoF提出的适配器模式),适配器中以抽象函数的形式实现监听器接口的操作,供具体监听器类继承。这样一来,具体监听器类就可以选择其需要的函数,对adapter不需要的函数采用默认操作即可。例如上面例子中的ZooListener类,创建ZooAdapter(Adapter的命名规则与监听器一致,只需要把类名中的Listener改为Adapter即可),代码如下:
public class ZooAdapter implements ZooListener { @Override public void onAnimalAdded (Animal animal) {} @Override public void onAnimalRemoved (Animal animal) {}}乍一看,这个适配器类微不足道,然而它所带来的便利却是不可小觑的。比如对于下面的具体类,只需选择对其实现有用的函数即可:
public class NamePrinterZooAdapter extends ZooAdapter { @Override public void onAnimalAdded (Animal animal) { // Print the name of the animal that was added System.out.println("Added animal named " + animal.getName()); -有两种替代方案同样可以实现适配器类的功能:一是使用默认函数;二是把监听器接口和适配器类合并到一个具体类中。默认函数是Java8新提出的,在接口中允许开发者提供默认(防御)的实现方法。
Java库的这一更新主要是方便开发者在不改变老版本代码的情况下,实现程序扩展,因此应该慎用这个方法。部分开发者多次使用后,会感觉这样写的代码不够专业,而又有开发者认为这是Java8的特色,不管怎样,需要明白这个技术提出的初衷是什么,再结合具体问题决定是否要用。使用默认函数实现的ZooListener接口代码如下示:
public interface ZooListener { default public void onAnimalAdded (Animal animal) {} default public void onAnimalRemoved (Animal animal) {}}通过使用默认函数,实现该接口的具体类,无需在接口中实现全部函数,而是选择性实现所需函数。虽然这是接口膨胀问题一个较为简洁的解决方案,开发者在使用时还应多加注意。
第二种方案是简化观察者模式,省略了监听器接口,而是用具体类实现监听器的功能。比如ZooListener接口就变成了下面这样:
public class ZooListener { public void onAnimalAdded (Animal animal) {} public void onAnimalRemoved (Animal animal) {}}这一方案简化了观察者模式的层次结构,但它并非适用于所有情况,因为如果把监听器接口合并到具体类中,具体监听器就不可以实现多个监听接口了。例如,如果AnimalAddedListener和AnimalRemovedListener接口写在同一个具体类中,那么单独一个具体监听器就不可以同时实现这两个接口了。此外,监听器接口的意图比具体类更显而易见,很显然前者就是为其他类提供接口,但后者就并非那么明显了。
如果没有合适的文档说明,开发者并不会知道已经有一个类扮演着接口的角色,实现了其对应的所有函数。此外,类名不包含adapter,因为类并不适配于某一个接口,因此类名并没有特别暗示此意图。综上所述,特定问题需要选择特定的方法,并没有哪个方法是万能的。
在开始下一章前,需要特别提一下,适配器在观察模式中很常见,尤其是在老版本的Java代码中。Swing API正是以适配器为基础实现的,正如很多老应用在Java5和Java6中的观察者模式中所使用的那样。zoo案例中的监听器或许并不需要适配器,但需要了解适配器提出的目的以及其应用,因为我们可以在现有的代码中对其进行使用。下面的章节,将会介绍时间复杂的监听器,该类监听器可能会执行耗时的运算或进行异步调用,不能立即给出返回值。
Complex & Blocking监听器关于观察者模式的一个假设是:执行一个函数时,一系列监听器会被调用,但假定这一过程对调用者而言是完全透明的。例如,客户端代码在Zoo中添加animal时,在返回添加成功之前,并不知道会调用一系列监听器。如果监听器的执行需要时间较长(其时间受监听器的数量、每个监听器执行时间影响),那么客户端代码将会感知这一简单增加动物操作的时间副作用。
本文不能面面俱到的讨论这个话题,下面几条是开发者调用复杂的监听器时应该注意的事项:
监听器启动新线程。新线程启动后,在新线程中执行监听器逻辑的同时,返回监听器函数的处理结果,并运行其他监听器执行。
Subject启动新线程。与传统的线性迭代已注册的监听器列表不同,Subject的notify函数重启一个新的线程,然后在新线程中迭代监听器列表。这样使得notify函数在执行其他监听器操作的同时可以输出其返回值。需要注意的是需要一个线程安全机制来确保监听器列表不会进行并发修改。
队列化监听器调用并采用一组线程执行监听功能。将监听器操作封装在一些函数中并队列化这些函数,而非简单的迭代调用监听器列表。这些监听器存储到队列中后,线程就可以从队列中弹出单个元素并执行其监听逻辑。这类似于生产者-消费者问题,notify过程产生可执行函数队列,然后线程依次从队列中取出并执行这些函数,函数需要存储被创建的时间而非执行的时间供监听器函数调用。例如,监听器被调用时创建的函数,那么该函数就需要存储该时间点,这一功能类似于Java中的如下操作:
public class
如何使用Java8 实现观察者模式?相信通过这篇文章大家都有了大概的了解了吧!