เป้าหมายคืออธิบายว่าทำไม Model-View-ViewModel architectural pattern ถึงมี separation of concerns ที่别扭ในบางสถานการณ์เกี่ยวกับ presentation logic ของ GUI architecture
จะสำรวจสอง variants ของ MVVM (ไม่มี เพียงหนึ่งทาง ทำ MVVM) และเหตุผลที่อาจชอบ variant หนึ่งมากกว่าอีกตัว ตาม project requirements
MVVM vs MVP/MVC?
คำถามที่ถามบ่อยที่สุดใน Sunday Q&A sessions:
MVVM vs MVP/MVC?
ตอบเร็ว: ไม่มี GUI architecture ไหน great ในทุกสถานการณ์
เพราะ? Architecture ที่ดี (หรืออย่างน้อย good choice) ขึ้นอยู่กับ requirements
คิดสั้นๆ เกี่ยว requirements:
- UI complex แค่ไหน? Simple UI ไม่ต้อง logic ซับซ้อน Complex UI อาจต้อง logic มากและ fine-grained control
- สนใจ testing มากแค่ไหน? Classes ที่ tightly coupled กับ frameworks และ OS (โดยเฉพาะ user interface) ต้อง extra work เพื่อ test
- ต้องการ re-usability และ abstraction มากแค่ไหน? ถ้าอยาก share backend, domain, และ presentation logic across platforms?
- คุณเป็นคน pragmatic, perfectionist, lazy หรือทั้งสามในเวลา/สถานการณ์ต่างๆ?
อยากเขียนบทความลึกๆ เกี่ยว MVVM กับ requirements ข้างต้น แต่บางคนเข้าใจผิดว่ามีเพียงหนึ่งทางทำ MVVM
แทนจะพูดถึงสอง approaches ต่างๆ ของ MVVM ที่มี benefits และ disadvantages ชัดเจน แต่ก่อนเริ่มกับ general idea
Thou Shalt Not Reference Thy View Classes
เพื่อนที่อ่าน old English ไม่ได้: “ห้าม reference view classes.”
นอกจากชื่อ ViewModel (ซับซ้อนถ้า class เต็มไปด้วย logic) กฎเหล็กของ MVVM คือ ห้าม reference View จาก ViewModel
พื้นที่สับสนแรกจากคำว่า "reference" จะกล่าวซ้ำหลายระดับ jargon:
- ViewModels ห้ามมี references (member variables, properties, mutable/immutable fields) ไปยัง Views ใดๆ
- ViewModels ห้าม depend on Views ใดๆ
- ViewModels ห้ามพูดกับ Views โดยตรง
บน Android platform กฎนี้ไม่ใช่แค่ "แย่เพราะคนรู้ architecture บอก" เวลาใช้ ViewModel class จาก Architecture Components (designed ให้ instance persist นานกว่า Fragment/Activity lifecycle เมื่อเหมาะสม) reference View ขอ SERIOUS MEMORY LEAKS
เหตุผล MVVM ห้าม reference คือ hypothetically ทำให้ View และ ViewModel ทดสอบและเขียนง่ายขึ้น
บางคนว่ากล่าวว่า promote reusability ของ ViewModels แต่ นี่คือจุดที่ pattern พัง
ก่อนดูโค้ด โปรดทราบ ผมไม่ใช้ LiveData ใน production code ชอบเขียน Publisher-Subscriber Pattern เอง แต่ว่าด้านล่าง apply ได้กับ library ใดๆ ที่ allow PubSub/Observer pattern link จาก ViewModel ไป View
บทความนี้มี video tutorial คลุมหลายไอเดียที่นี่:
ViewLogic + ViewModel หรือ View + ViewModelController?
พูด "break down" ไม่ได้ว่า pattern แตก แต่มันแยกเป็น (อย่างน้อย) สอง approaches ที่แตกต่างชัดเจน
พิจารณาสอง approaches และเมื่อไหร่ควรชอบอันไหน

Approach แรก: ให้ความสำคัญ Reusable ViewModels
คนส่วนใหญ่ implement MVVM เป้าหมาย promote re-usability ของ ViewModels ให้ reuse สำหรับ n Views (many-to-one ratio)
สองวิธีทำ re-usability:
- ไม่ reference specific View (หวังว่าไม่ใช่ข่าวใหม่)
- รู้ น้อยที่สุดเกี่ยวกับรายละเอียด UI โดยทั่วไป
จุดที่สองอาจ vague หรือ counter-intuitive (รู้ได้ยังไงถ้าไม่ reference?) ดูโค้ด:
class NoteViewModel(val repo: NoteRepo): ViewModel(){ //Note: อาจ publish data ไป View ผ่าน Databinding, RxJava Observables, และ approaches อื่นๆ แม้ผมไม่ชอบ LiveData ใน back end classes แต่มันทำงานดีกับ Android front end กับ AAC val noteState: MutableLiveData<Note>() //... fun handleEvent(event: NoteEvent) { when (event) { is NoteEvent.OnStart -> getNote(event.noteId) //... } } private fun getNote(noteId: String){ noteState.value = repo.getNote(noteId) } }ตัวอย่าง simplified จุดคือ ViewModel นี้ expose สาธารณะ (นอกจาก handleEvent) แค่ Note object เรียบง่าย:
data class Note(val creationDate:String, val contents:String, val imageUrl: String, val creator: User?)ด้วย approach นี้ ViewModel decoupled จากไม่ใช่แค่ View เฉพาะ แต่ details และ presentation logic ของ View ใดๆ
ยัง vague? สัญญาจะชัดเจนตอนอธิบาย approach ถัดไป
heading ก่อนหน้า "ViewLogic + ViewModel…" ไม่ใช่ชื่อจริง แต่นี้: ด้วย ViewModels ที่ decoupled และ reusable เรา depend on View เองให้ทำ rendering/binding Note object บนหน้าจอ
บางคนไม่ชอบเติม logic ใน View classes
จุดนี้ ambiguous ขึ้นกับ project requirements ไม่ได้ว่าเติม logic ใน View classes เช่น…:
private fun observeViewModel() { viewModel.notes.observe( viewLifecycleOwner, Observer { notes: List<Note> -> if (notes.isEmpty()) showEmptyState() else showNoteList(notes) } ) //.. }…เป็น เสมอไป แต่ classes ที่ tightly coupled กับ platform (เช่น Fragments) ยากต่อการ test และ classes ที่มี logic เป็น classes สำคัญที่สุดที่ต้อง test!
ในคำเดียว: ล้มเหลวในการ apply golden principle ของ architecture ใดๆ: Separation of concerns.
ความคิดเห็นส่วนตัว: คุ้มค่า apply separation of concerns ระดับสูง แต่จริงๆ cash cow apps หลายตัวเขียนโดยคนไม่รู้เรื่องนี้
Approach ถัดไป แม้ มี side effects ของตัวเอง ก็เอา presentation logic ออกจาก View อีกครั้ง (ส่วนใหญ่)
Approach ที่สอง: Humble View, Control-Freak ViewModel
บางครั้งไม่มี fine-grained control over Views (consequence ของ prioritizing reusable ViewModels) จริงๆ แย่
ทำให้ผมไม่ชอบ apply approach ก่อนหน้า indiscriminately เพราะ บ่อยครั้ง ไม่ ต้อง reuse ViewModel
Ironic: "too much abstraction" เป็น critique ของ MVP over MVVM
ไม่สามารถเพิ่ม reference กลับเข้า ViewModel เพื่อ fine-grained control ได้ เพราะจะกลายเป็น MVP + memory leaks (สมมติยังใช้ ViewModel จาก AAC)
ทางเลือก: สร้าง ViewModels ให้มีเกือบ behaviour, state, และ presentation logic ทั้งหมดของ View View ยัง bind กับ ViewModel แต่ details เรื่อง View ใน ViewModel พอทำให้ View functions กลายเป็น one-liners (ยกเว้นเล็กน้อย)
ใน naming conventions ของ Martin Fowler เรียกว่า Passive View/Screen ชื่อทั่วไปคือ Humble Object Pattern
เพื่อ achieve นี้ ViewModel ต้องมี observable field (databinding, Rx, LiveData, อะไรก็ได้) สำหรับทุก control/widget ใน View:
class UserViewModel( val repo: IUserRepository, ){ // Actual data model private เพื่อกัน unwanted tampering private val userState = MutableLiveData<User>() // Control Logic internal val authAttemptState = MutableLiveData<Unit>() internal val startAnimation = MutableLiveData<Unit>() // UI Binding internal val signInStatusText = MutableLiveData<String>() internal val authButtonText = MutableLiveData<String>() internal val satelliteDrawable = MutableLiveData<String>() private fun showErrorState() { signInStatusText.value = LOGIN_ERROR authButtonText.value = SIGN_IN satelliteDrawable.value = ANTENNA_EMPTY } //... }ต่อมา View wire เองกับ ViewModel แต่ functions ทำได้ trivial:
class LoginView : Fragment() { private lateinit var viewModel: UserViewModel //... // Create and bind to ViewModel override fun onStart() { super.onStart() viewModel = ViewModelProviders.of( //... ).get(UserViewModel::class.java) // start background anim (root_fragment_login.background as AnimationDrawable).startWithFade() setUpClickListeners() observeViewModel() viewModel.handleEvent(LoginEvent.OnStart) } private fun setUpClickListeners() { //... } private fun observeViewModel() { viewModel.signInStatusText.observe( viewLifecycleOwner, Observer { // "it" คือค่า MutableLiveData object inferred เป็น String automatically lbl_login_status_display.text = it } ) viewModel.authButtonText.observe( viewLifecycleOwner, Observer { btn_auth_attempt.text = it } ) viewModel.startAnimation.observe( viewLifecycleOwner, Observer { imv_antenna_animation.setImageResource( resources.getIdentifier(ANTENNA_LOOP, "drawable", activity?.packageName) ) (imv_antenna_animation.drawable as AnimationDrawable).start() } ) viewModel.authAttemptState.observe( viewLifecycleOwner, Observer { startSignInFlow() } ) viewModel.satelliteDrawable.observe( viewLifecycleOwner, Observer { imv_antenna_animation.setImageResource( resources.getIdentifier(it, "drawable", activity?.packageName) ) } ) }โค้ดเต็มตัวอย่างที่นี่
สังเกต: ViewModel นี้ น่าจะไม่ reuse ที่อื่น View กลายเป็น sufficiently humble (ขึ้นอยู่กับ standards และ preferences code coverage) และเขียนง่ายมาก
บางครั้งเจอ half-measure ระหว่างการกระจาย presentation logic ระหว่าง Views และ ViewModels ไม่ได้ follow approaches ไหนๆ อย่างเคร่งครัด
ไม่ได้ advocate approach ไหนมากกว่า แต่勉勵ให้ flexible ตาม requirements
เลือก Architecture ตาม Preferences และ Requirements
จุดของบทความ: ดูสอง approaches ที่ developer 採用ได้ในการสร้าง MVVM style GUI architecture บน Android Platform (carry over ไป platform อื่นได้)
จริงๆ อาจ specific มากขึ้นถึง small differences ภายใน approaches นี้
- View ควร observe field สำหรับแต่ละ widget/control หรือ observe one field ที่ publish single model render ทั้ง View ใหม่ทุกครั้ง?
- อาจ avoid one-to-one ViewModels พร้อม Humble Views โดยเพิ่ม Presenter หรือ Controller?
Talk is cheap แนะนำลองเรียนรู้ ในโค้ด ให้ไม่ต้องพึ่งคนแบบผมบอก
สุดท้าย สอง elements ทำ great architecture:
หนึ่ง ลองหลาย approaches จนหาที่ ชอบ ทำได้โดย build app (simple ก็ได้) ในแต่ละ style ดูว่า feels right อะไร
สอง เข้าใจว่า preferences aside styles ต่างๆ เน้น benefits ต่างๆ แลกกับ deficits ต่างๆ จนสุดท้ายเลือกได้จาก understanding project requirements ไม่ใช่ blind faith
Learn More About Software Architecture:
Social
https://www.instagram.com/rkay301/
https://www.facebook.com/wiseassblog/
https://twitter.com/wiseass301
https://wiseassblog.com/