RxJava series - part 8 - Subject คืออะไรหนอ แล้วจะใช้มันอย่างไร

บทความนี้ เป็นส่วนหนึ่งของชุดบทความที่พูดถึง RxJava2 โดยบทความชุดนี้จะประกอบด้วยหัวข้อต่างๆดังต่อไปนี้

และแล้วก็มาถึงบทความสุดท้ายของ series RxJava ชุดนี้กันแล้วนะครับ โดยในบทความที่ผ่านมาเราได้รู้จักกับ Observable Observer รวมถึง Operator ต่างๆ ที่มีให้ใช้ใน Rxjava2 กันไปแล้ว มาในบทความนี้ เราจะมารู้จักกับ Subject กันว่า มันคืออะไร แล้วเราจะใช้มันอย่างไร

Subject

Subject เป็นเหมือนสะพานที่เชื่อมระหว่าง Observable แล้ว Observer (ต่อไปจะขอเรียกว่า subscriber) คือ Subject จะทำหน้าที่เป็นทั้ง Observable และ Subscriber ในตัวเดียวกัน นั้นหมายความว่าเราสามารถใช้ Subject แทน Subscriber ในการ subscribe Observable ได้ และยังสามารถใช้มันในการส่งต่อ Item ที่ได้รับมาไปยัง Subscriber ตัวอื่นที่ subscribe มันอยู่ หรือแม้กระทั้งสร้าง Item ใหม่ขึ้นมาได้ เพราะเนื่องจากมันเป็น Observable ในตัวมันเองอยู่แล้ว

Subject เป็น Hot observable (ที่กล่าวถึงในบทความที่ 4) นั้นหมายความว่า มันยังคงทำงานต่อไปได้ถึงแม้ไม่มีใคร subscribe มันอยู่ ให้เราลองนึกถึงสถานีวิทยุที่ยังคงเปิดเพลงต่อไป ถึงแม้ว่าเราจะปิดวิทยุของเราไปแล้วก็ตาม และเนื่องจากมันเป็น Hot observable ดังนั้นมันจะไม่ execute operation ใหม่ทุกครั้งที่เกิดการ subscribe นั้นหมายความว่า Subscriber ทุกตัวที่ subscribe มันอยู่จะได้รับข้อมูลชุดเดียวกัน โดยไม่มีการรันโค้ดภายใน Subject ใหม่ ซึ่งผิดกับ Cold Observable ตรงที่จะรันโค้ดภายใน Observable ใหม่ทุกครั้งที่เกิดการ subscribe ขึ้น

Varieties of Subject

Subject ที่มีให้เราใช้ใน RxJava2 มีอยู่ 4 ตัวด้วยกัน โดยแต่ละตัวก็จะมีพฤติกรรมการทำงานที่แตกต่างกัน โดยเราจะมาดูกันว่าแต่ละตัวทำงานแตกต่างกันอย่างไร

PublishSubject

PublishSubject คือ Subject ที่ปล่อย Item ออกมาตามช่วงเวลาที่ Source Observable (Observable ที่ผลิตข้อมูล) ปล่อยข้อมูลนั้นๆออกมา โดยที่ Subscriber จะได้รับข้อมูลตามช่วงเวลาที่เข้ามา subscribe นั่นหมายความว่า หาก Subscriber เข้าไป subscribe ตั้งแต่ Subject เริ่มปล่อย Item ออกมา Subscriber นั้นก็จะได้รับ item ที่ปล่อยออกมาตั้งแต่เริ่ม แต่หากไป subscribe ตอนช่วงกลางๆ หลังจากที่ Subject มีการปล่อย Item ออกไปบ้างแล้ว Subscriber ตัวนั้นก็จะได้รับแค่เฉพาะ Item ที่เกิดขึ้นหลังจากการ subscribe เท่านั้น ลองดูแผนภาพ Marble ด้านบนประกอบครับ

หากเปรียบเป็นนักเรียนกับห้องเรียน PublishSubject ก็จะเหมือนกับตอนที่นักเรียนเข้าเรียนตอนช่วงเวลาใดๆ ของ class นักเรียนคนนั้นก็จะได้เรียนแต่สิ่งที่อาจารย์สอนนับตั้งแต่ช่วงเวลาที่เข้าเรียน โดยจะไม่รู้เลยว่าก่อนหน้านั้นอาจารย์สอนอะไรไปบ้าง

ลองดูตัวอย่างการทำงานของมันกันครับ

BehaviorSubject

BehaviorSubject คือ Subject ที่เริ่มต้นด้วยการปล่อย Item ตัวล่าสุด (most recent item) ที่ถูกปล่อยออกมาจาก Source Observable จากนั้นจึงค่อยปล่อย Item ที่เกิดขึ้นหลังจากการ subscribe ไปแล้วให้กับ Subscriber ตามปกติเหมือน PublishSubject ดังนั้น BehaviorSubject จึงมีความแตกต่างกับ PublishSubject ตรงที่ BehaviorSubject จะมีค่า Default หรือ ค่าล่าสุด ปล่อยออกไปให้กับ Subscriber ก่อน แต่หาก Source Observable ไม่ได้ปล่อย Item ใดๆ ออกมาก่อนหน้าการ subscribe หรือ เราไม่ได้กำหนดค่า Default ใดๆให้กับ BehaviorSubject ตัว Subscriber ก็จะไม่ได้รับ Item ใดๆ ออกมาเช่นกัน

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

ลองดูตัวอย่างการทำงานของมันกันครับ

ReplaySubject

ReplaySubject คือ Subject ที่จะปล่อย Item ทั้งหมดตั้งแต่ Item แรกให้กับ Subscriber ที่เข้ามา subscribe โดยที่ไม่สนใจว่า Subscriber นั้นจะเข้ามา subscribe ตอนไหน จากนั้นจึงปล่อย Item ที่เกิดขึ้น (จาก Source Observable) หลังจากการ subscribe ตามปกติเหมือน PublishSubject

หากเปรียบเป็นนักเรียนกับห้องเรียน ReplaySubject ก็จะเหมือนกับการที่นักเรียนเรียนออนไลน์ผ่านทาง Youtube channel ซึ่งหากนักเรียนเปิดเข้ามาเรียนไม่ทันตอนเริ่ม class นักเรียนก็สามารถย้อนกลับไปดู replay ที่อาจารย์ได้สอนไปในช่วงต้นชั่วโมงได้นั้นเอง

ลองดูตัวอย่างการทำงานกันครับ

AsyncSubject

AsyncSubject คือ Subject ที่จะปล่อย Item ออกมาแค่ค่าสุดท้ายค่าเดียวเท่านั้น และจะปล่อยออกมาเมื่อ Source Observable ทำงานเสร็จแล้วเท่านั้น ดังนั้น หาก Source Observable ไม่ปล่อยค่าใดๆ ออกมา หรือ เกิด Error ขึ้นก่อน จะทำให้ AsyncSubject หยุดการทำงานโดยที่ไม่ปล่อย Item ใดๆ ออกมาได้เช่นกัน (แต่จะเรียก onError() แทนในกรณีที่เกิด Error ขึ้น)

หากเปรียบเป็นนักเรียนกับห้องเรียน ก็คงเปรียบได้กับกรณีที่นักเรียนเข้าห้องเรียนไม่ว่าช่วงเวลาใดก็ตาม แต่รอฟังแค่บทสรุป (หรือสิ่งที่อาจารย์สอนเป็นอย่างสุดท้าย) ของ class เรียนนั้นเท่านั้น

ลองดูตัวอย่างการทำงานของมันกันครับ

แล้วเราจะใช้มันอย่างไร

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

ตัวอย่างของการเลือกใช้ BehaviorSubject ได้แก่ กรณี Location update แน่นอนว่าเราคงไม่อยากได้ข้อมูล Location ทั้งหมดตั้งแต่เริ่มทำงาน แต่เราอยากได้แค่ Location ล่าสุดเท่านั้น และควรจะได้ Location ทันทีที่ subscribe ด้วย ดังนั้น BehaviorSubject จึงดูเหมาะสมที่สุดครับ ลองดูตัวอย่างการใช้งานครับ

จากโค้ดจะเห็นว่าเราสร้าง locationUpdateSubject ขึ้นมาเป็น BehaviorSubject โดยทุกครั้งที่ได้รับ Location update ก็จะทำการเรียก onNext(location) เพื่อส่ง update location ไปยัง Subscriber ซึ่งทางฝั่ง Subscriber ที่อยู่ใน Fragment ก็ทำหน้าที่ subscribe เจ้า locationUpdateSubject และเมื่อได้ผลลัพธ์ ก็จะนำ Location ที่ได้ไปใช้งานต่อไป โดยเราจะเห็นว่า เราสามาถ subscribe ได้มากกว่าหนึ่งตัว โดยที่ทุกตัวจะได้ข้อมูลเดียวกัน

ส่วน PublishSubject ที่ผมใช้บ่อยนั้น จะใช้ในกรณีที่เราไม่อาจ subscribe ระหว่าง Source Observable และ Subscriber กันได้โดยตรง ตัวอย่างเช่น หากใครใช้ MVVM อยู่ ก็คงไม่อยากให้ ViewModel รู้จัก View มากสักเท่าไหร่หากไม่จำเป็น ซึ่งถ้าเราต้องการดักฟังเหตุการณ์การกดปุ่มของผู้ใช้ที่เกิดขึ้นในฝั่ง View แล้วส่งต่อเหตุการณ์นั้นให้กับ Subscriber ที่อยู่ในฝั่ง ViewModel แล้วละก้อ หากเราให้ Subscriber เหล่านั้นมา subscribe View โดยตรงก็จะเกิด Dependency ขึ้น ดังนั้นเราจึงนำ PublishSubject มาช่วยในกรณีนี้ได้ ซึ่งสาเหตุที่เลือกใช้ PublishSubject กับกรณีการกดปุ่มนั้นเพราะว่า Subscriber ไม่จำเป็นต้องสนใจว่าก่อนหน้าที่จะ subscribe นั้น ผู้ใช้กดปุ่มไปแล้วกี่ครั้ง แต่จะยึดตามเหตุการณ์ที่เกิดขึ้นหลังจาก subscribe แล้วเป็นสำคัญ และตอบสนองต่อเหตุการณ์ที่เกิดขึ้นเหล่านั้น ดังนั้นการเลือกใช้ PublishSubject จึงเหมาะกับเหตุการณ์นี้ ลองดูตัวอย่างครับ

จากโค้ดจะเห็นว่าเราสร้าง Interface ของ ViewModel ไว้ โดยมีทั้ง Input และ Output อยู่ภายใน ให้เราสังเกตุที่ Input ว่าเราประกาศ refresh เป็น Observer แต่เมื่อ Implement จริงใน SampleViewModelImpl เราประกาศให้เป็น PublishSubject แทน สาเหตุที่ทำแบบนี้เนื่องจาก PublishSubject นั้นเป็นได้ทั้ง Observable และ Observer ดังนั้นเราจึงสามารถประกาศ Interface เป็น Observer ได้ อีกเหตุผลหนึ่งก็คือ เราไม่อยากให้ฝั่ง View สามารถเรียก onNext() ได้เองโดยตรง ดังนั้นการประกาศเป็น Observer จะช่วยป้องกันไม่ให้ฝั่ง View เรียก onNext() โดยพลการได้นั้นเอง ต่อไปให้เราสังเกตที่ฟังก์ชั่น initInput ใน SampleFragment ซึ่งจะเห็นว่าตอนนี้ refresh ทำหน้าที่เป็น Observer ที่รอรับข้อมูลจาก RxView.clicks() และเมื่อเราไปดูฟังก์ชั่น init ใน SampleViewModelImpl เราก็จะเห็นว่า refresh จะทำหน้าที่เป็น Observable ที่นำข้อมูลที่ได้รับมา ไปทำอะไรบางอย่างเพื่อให้ได้ผลลัพธ์ออกมานั้นเอง

หากใครสนใจดูตัวอย่างโปรเจคของการเขียนโค้ดแบบ MVVM ที่ใช้ Rxjava สามารถ check out โค้ดออกมาดูได้จากโค้ดตัวอย่างนี้ด้านล่างเลยครับ

Conclusion

ในบทความนี้ เราได้เรียนรู้แล้วว่า Subject คือตัวที่ทำหน้าที่เป็นทั้ง Observable และ Subscriber ในตัวเดียวกัน ซึ่งมีถึง 4 แบบให้เราเลือกใช้ได้แก่ PublishSubject, BehaviorSubject, ReplaySubject และ AsyncSubject โดยที่แต่ละแบบมีลักษณะที่แตกต่างกัน ซึ่งในบทความเราได้ยกตัวอย่างของการนำ Subject ไปใช้กันให้เห็นไปบ้างแล้ว ซึ่งการจะเลือกใช้ Subject แบบไหนนั้น ก็ขึ้นอยู่กับงานที่ผู้ใช้จะทำ หากเราต้องการให้ทุกครั้งที่ subscribe จะต้องได้ Item มาเสมอ ก็ให้เลือกใช้ระหว่าง BehaviorSubject หรือ ReplaySubject แต่หากเราต้องการจัดการกับ Item ล่าสุดเสมอโดยไม่สนใจ Item ที่ปล่อยออกมาก่อนหน้า ก็ให้เลือกใช้ PublishSubject แทนครับ และอย่างสุดท้ายหากต้องการแค่ Item ตัวสุดท้าย ก็เลือกใช้เป็น AsyncSubject ครับ

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

สุดท้ายนี้ก็เช่นเคยครับ หากผู้อ่านคิดว่าบทความนี้มีประโยชน์ ก็ฝากกด 👏 เป็นกำลังใจให้ด้วยนะครับ

Reference