[Android Architecture the series] Ep.1 MVC and MVP Concepts
สวัสดีครับผมบาสครับ วันนี้เราจะเปิดเรื่องเข้าสู่หัวข้อ Android Architecture สืบเนื่องมาจากพึ่งได้มาเขียน Android เลยอยากจะเข้าใจวิธีการเขียน android มากขึ้นเพื่อตอบโจทย์เรื่องการจัดการ Separation of Concern และการทำให้เขียน Unit Test ได้ง่าย ๆ
ตอนเริ่มที่มาศึกษาเรื่องนี้ก็มาจากปัญหาว่ายังไม่รู้จะเริ่มต้นทดสอบ Android อย่างไร และโปรเจกต์มีการวางโครงสร้างมาอยู่แล้ว สิ่งที่น่าจะช่วยให้เราเข้าใจโครงสร้างของโปรเจกต์ Android นั่นก็คือ ศึกษา Architecture ที่ถูกนำมาใช้กับ Android
ไปนั่งคุยกับ Frame Doungdee เรื่องนี้ ก็ได้หนังสือเล่มนึงมา ชื่อว่า Advanced Android App Architecture เห้ย ! หนังสือเล่มนี้ตรงประเด็นที่เราต้องการเลยนี่หว่า ก็เลยอยากมาแชร์ต่อ
Separation of Concerns ไปทำไม?
กล่าวคือแต่ละส่วนของแอปพลิเคชันควรจะแยกหน้าที่ความรับผิดชอบออกจากกัน เพื่อที่ว่า หากมีการแก้ไขในเรื่องหนึ่ง จะไม่ไปกระทบกับความรับผิดชอบของคนอื่น ตัวอย่างเช่น MVC ที่แยกหน้าที่ระหว่าง Model View และ Controller ออกเป็น 3 ความรับผิดชอบ หากวันนึงอยากปรับหน้าตาการแสดงผลของ View เราก็ไม่ควรที่จะต้องไปแตะส่วนที่เป็น Controller นั่นทำให้เราจัดการกับโปรแกรมได้ดีมากยิ่งขึ้น เพราะจะลดโอกาสที่จะต้องไปแก้ไขโค้ดส่วนอื่น
Unit Testing ไปทำไม?
ยิ่งแอปยิ่งใหญ่ขึ้นมากเท่าไร จำนวนบรรทัดยิ่งมากขึ้นไปเรื่อย ๆ แล้วเราจะมั่นใจได้อย่างไรว่าโปรแกรมทุกส่วนยังคงทำงานได้ตามที่คาดหวังไว้ หากต้องทดสอบแบบนั่งกดมือเอง (Manual Test) เราจะต้องใช้เวลามากขนาดไหน ที่จะต้องทดสอบซ้ำแล้วซ้ำเล่า ทดสอบบางครั้งก็มีข้อผิดพลาดจากมือมนุษย์เอง (human error) ส่งผลให้นานวันเข้าเราเริ่มไม่แน่ใจว่าโปรแกรมทุกส่วนยังคงทำงานได้ตามความคาดหวัง สิ่งที่จะมาช่วยให้เขียนโค้ดต่อไปอย่างมั่นใจได้ว่าโค้ดที่เรา เพิ่ม/แก้ไข/ลบ จะไม่ไปทำลายส่วนที่ควรจะใช้งานได้ ก็คือ Unit Tests เนื่องจากมันทำให้เรารู้ Feedback ได้เร็ว เราแก้ไขอะไรปุ๊ป เรารัน Unit Tests เราก็จะรู้ได้ทันทีว่าโค้ดที่แก้ไขไป มันไปทำลายสิ่งที่เคยทำงานได้อยู่หรือไม่
เมื่อเข้าใจจุดมุ่งหมายเราลองเริ่มไปดูในแต่ละ Architecture กัน
MVC Concept
เป็นแนวคิดที่ถูกนำมาใช้กับการพัฒนา Desktop App พวก GUI และยังนิยมในการนำมาใช้กับ web application ต่าง ๆ ซึ่งใน Architecture ตัวนี้ถูกแบ่งออกเป็น 3 ส่วน คือ
- Model เป็นส่วน data layer ที่เก็บข้อมูล object ต่าง ๆ มีทั้ง Business Logic รวมทั้ง concern เกี่ยวกับการเก็บข้อมูล ดึงข้อมูล อัปเดตข้อมูล เป็นต้น
- View แสดงข้อมูลจาก Model เพื่อให้ผู้ใช้เห็นผ่าน User Interface
- Controller เป็นมันสมองของระบบนี้ ทำหน้าที่เชื่อมระหว่าง View และ Model เป็นด่านหน้าเวลาผู้ใช้อยากจะทำ action ใด ๆ กับระบบ
โดยทั่วไปแล้ว Model ควรจะไม่มี Concern ว่าจะแสดงผลไปให้ผู้ใช้อย่างไร และ View เองก็ไม่ควร Concern ว่าค่าที่จะแสดงผลออกมาเป็นอย่างไร แค่แสดงผลลัพธ์ที่มีคนส่งมาให้ก็พอ ส่วน Controller เป็นกาวเชื่อมระหว่าง Model และ View ที่จะจัดการว่าข้อมูลควรแสดงผลออกมาอย่างไร
โดยในแนวทางนี้ (MVC) จะช่วยให้เรา
- จัดการให้ Separation of Concerns ออกเป็น 3 ส่วน คือ Model View และ Controller แต่ละส่วนมีหน้าที่รับผิดชอบเพียงแค่ 1 เรื่อง มีความยืดหยุ่นกับระบบ
- ทำให้ Unit Tests ได้
เราลองมาประยุกต์ MVC ใช้กับ Android Project ดู ก็ได้จะออกมาเป็น
- Model เป็น class ธรรมดาที่เราเรียกว่า POJO (Plain Old Java Object) และส่วนที่ติดต่อ Database หรือ API
- View เป็นไฟล์ XML Layouts ต่าง ๆ
- Controller เป็น Android Activity Class
แต่การนำมาประยุกต์ใช้กับ Android ดูเหมือนจะไม่ราบลื่น เนื่องจากเมื่อมาดูตัว Controller หรือ Android Activity Class ดี ๆ แล้วจะไม่ตอบโจทย์ที่เราตั้งไว้ทั้ง 2 เรื่อง คือ Separation of Concerns และ Unit Testing เราไปดูกันดีว่าว่าเกิดอะไรขึ้น
MVC Problems
- ในประเด็นแรกคือ Separation of Concerns ตัว XML Layouts ที่ตอนแรกเรามองว่ามันเป็น View แต่มาดู ๆ แล้ว XML มันเป็น static file ธรรมดา ส่วนคนที่จะทำให้ XML นี้เกิดการเปลี่ยนแปลงคือ Activity ต่างหาก นั่นหมายความว่า Activity จะได้รับ 2 บทบาท ก็คือ View และ Controller นั่นก็เป็นข้อแรกที่ขัดกับ Separation of Concerns
- แล้ว Unit Testing ละ? Model ก็ดูจะทำได้ไม่มีปัญหา แต่ View และ Controller ละ? ดูแล้วคงทำ Unit Tests ยากน่าดูเนื่องจาก View และ Controller ยึดติดกับ Android Framework Class อย่าง Activity ที่มี Android lifecycle ผูกติดกับ Android OS ไม่สามารถสร้าง object ขึ้นมาได้ตรง ๆ เพราะไม่มี Constructor ให้เรียกง่าย ๆ และอีกเหตุผลคือ Android Dependencies ที่ Activity Class อาจต้องมีการเรียกใช้ Android Framework Class เข่น Context, RecycleView, Toast เป็นต้น
แม้อาจจะมีเครื่องมือที่ช่วยทดสอบอย่าง Robolectric แต่ว่าการทดสอบครั้งนึง Feedback ช้ากว่า JUnit Test อยู่ดี
เราจึงเลี่ยงที่จะนำ MVC มาใช้กับ Android Project ไม่ได้หมายความว่าไม่ดี เพียงอาจจะไม่เหมาะกับ Android
แล้วอะไรที่จะมาช่วยเราบรรลุเป้าหมาย 2 อย่างที่ว่าได้ละ? หากดูหัวข้อบทความก็คงจะทราบกัน นั่นก็คือ MVP ที่ย่อมาจาก Model View Presenter
MVP Concept
เป็นแนวคิดที่มาช่วยแก้ปัญหาต่อจาก MVC ที่ View กับ Controller ผูกติดกันโดยแบ่ง Separation of Concerns ออกเป็น 3 ส่วนดังนี้
- Model เป็น data layer จัดการเกี่ยวกับ Business Logic
- View คอยจัดการการแสดงผล UI ให้ผู้ใช้ และคอยรับคำสั่งต่าง ๆ เช่น การพิมพ์ข้อความ การกดปุ่มต่าง ๆ โดยจะให้ Android Activity (หรือ Android Fragment) รับผิดชอบ
- Presenter เป็นกาวเชื่อมคุยกับทั้ง Model และ View และยังจัดการเรื่องที่เกี่ยวกับ Presentation Logic
จะมีมุมมองนึงที่เปลี่ยนไปคือด่านหน้า จากเดิมหน้าที่รับผิดชอบการรับ action จากผู้ใช้ใน MVC คือ Controller แต่ใน MVP จะเป็น View แทน
คำถามคือ แล้วมันต่างจาก MVC ยังไงละ?
เมื่อมาดูไปทีละตัว เริ่มจาก Model ก็ยังคงเหมือนกับ MVC เป๊ะ ๆ เลย สิ่งที่แตกต่างคือ 2 ตัวที่เหลือ
View ที่รับหน้าที่ในการแสดงผลเหมือนกับ MVC ที่เพิ่มเติ่มคือ ยังจัดการซ่อน/แสดงผลของ UI, การเปลี่ยนหน้าจอ และคอยรับ user input หากเกิด event ต่าง ๆ เช่น การพิมพ์ การกดปุ่ม และอีกตัว
Presenter ทำหน้าที่เป็นเพียงตัวเชื่อมระหว่าง View และ Model โดยที่ไม่ได้จัดการการแสดงผลของ UI โดยตรง แต่เป็นการสั่ง View ให้จัดการต่อแทน ทำให้ส่วนที่เป็น Presentation Logic จะไม่ยึดติดกับ Android Framework Class แล้ว เนื่องจากยกความรับผิดชอบนี้ให้ View ไปจัดการต่อ
ตัวอย่างโปรแกรมที่ถูกเขียน ผมจะยกตัวอย่างโปรแกรม TodoList
หากเกิด event การคลิกใน View จะเป็นลักษณะประมาณนี้คือส่งไปให้ presenter ทำงานต่อ
ถัดมาในส่วนของ Presenter ก็ทำการ save ลง Database ก่อนที่จะให้ view แสดงผล TodoList ใหม่อีกครั้ง
Separation of Concerns and Unit Testing in MVP
ตัวอย่างที่แสดงด้านบนจะเห็นว่า Presentation ทำหน้าที่เป็นตัวกลางระหว่าง Model และ View ทำให้ Model และ View ไม่จำเป็นต้องรู้จักกัน ทำให้เกิด Separation of Concerns ที่ดีขึ้น
ในประเด็นของการทำ Unit Testing ตัว presenter สามารถ new Object ได้ตรง ๆ เลย var presenter = Presenter()
ทำให้ง่ายต่อการทำ Unit Testing มาก ๆ
จากที่ดูยังมีสิ่งหนึ่งที่ปรับปรุงให้ดีขึ้น เพื่อทดสอบง่ายขึ้น หากดูจากโค้ดตัวอย่างจะเห็นว่า constructor ของ TodoListPresenter ยังยึดติดกับ view: TodoListActivity
ที่เป็น Android Framework Class เรามักจะแก้ปัญหานี้ด้วยการใช้ interface ซึ่งเป็นแนวคิดที่ชื่อ Depdencency Inversion ประโยชน์ที่ได้รับคือ Presenter จะไม่ยึดติดกับ TodoListActivity แล้ว ทำให้เวลาทดสอบก็สามารถสร้าง Mock สำหรับ view ได้เลย
วิธีการทำก็คือสร้าง interface ของ Presenter และ View มาเก็บไว้ใน interface ตัวนึงชื่อ Contract โดยตัว Contract นี้ทำหน้าที่เป็นเสมือน document และการจัดกลุ่มว่า View และ Presenter 2 ตัวนี้มีการติดต่อหากัน จากนั้นก็ให้ TodoListActivity implements View และ TodoListPresenter implements Presenter
โดยหน้าตาของ Contract จะเป็นประมาณนี้
ตอนที่เราเขียน Unit Tests ชีวิตก็จะดีขึ้นมากเราสามารถทดสอบได้ทั้ง Model และ Presenter ในตอนที่เราทดสอบ Presenter เราก็ให้ Model และ View mock ขึ้นมา และส่งผ่าน constructor ที่เราเขียน เรียกเทคนิคนั้นว่า dependency injection
สำหรับเนื้อหาในบทความนี้จะประมาณนี้ก่อน แล้วเดี๋ยวเรามาดูกันต่อกับการทดสอบ MVP และปัญหาที่เกิดขึ้นใน Ep. ถัดไปครับ
หากอ่านมาถึงตรงนี้มีอะไรแล้วคิดว่ามีอะไรผิดพลาด หรือตกหล่นอะไร อยากรบกวนช่วยกันคอมเมนต์เข้ามาได้เลยนะครับ หรือสงสัยอะไรอยากถามก็ได้เช่นกัน เพื่อที่เพื่อน ๆ คนอื่นที่มาอ่านบทความนี้จะได้เนื้อหาที่ถูกต้องและสมบูรณ์ครับ 🙂 แล้วเจอกัน EP หน้าครับ สวัสดี 🙏
ขอบคุณข้อมูลจาก
หนังสือ Advanced Android App Architecture (First Edition): Real-world app architecture in Kotlin 1.3
Special Thanks
ขอขอบคุณ “สำนักงานส่งเสริมเศรษฐกิจดิจิทัล (depa)” และคณาจารย์ “คณะเทคโนโลยีสารสนเทศ มจธ. (SIT)” ที่ให้การสนับสนุน “ทุนเพชรพระจอมเกล้าเพื่อพัฒนาเทคโนโลยีและนวัตกรรมดิจิทัล (KMUTT-depa)” ซึ่งเป็นทุนที่มอบความรู้ ทักษะและโอกาสดีในการฝึกฝนพัฒนาทักษะที่มีอยู่ให้เฉียบคมมากยิ่งขึ้นครับ ❤️