เป้าหมายของฉันในบทความนี้คือการอธิบายว่าทำไมรูปแบบสถาปัตยกรรม Model-View-ViewModel จึงนำเสนอการแยกข้อกังวลที่น่าอึดอัดใจในบางสถานการณ์เกี่ยวกับตรรกะการนำเสนอของสถาปัตยกรรม GUI
เราจะสำรวจ MVVM สองรูปแบบ (มี ไม่ เพียงวิธีเดียวเท่านั้น) และเหตุผลที่คุณอาจชอบตัวแปรหนึ่งมากกว่าตัวแปรอื่น โดยขึ้นอยู่กับข้อกำหนดของโครงการ
MVVM เทียบกับ MVP/MVC หรือไม่
มีความเป็นไปได้ค่อนข้างมากที่คำถามที่พบบ่อยที่สุดที่ฉันถูกถามระหว่างเซสชันถาม &ตอบในวันอาทิตย์แบบสดของฉันคือ:
MVVM เทียบกับ MVP/MVC?
เมื่อใดก็ตามที่ฉันถูกถามคำถามนี้ ฉันเน้นอย่างรวดเร็วว่าไม่มีสถาปัตยกรรม GUI ใดที่ทำงานได้ดีในทุกสถานการณ์
ทำไมคุณอาจถาม? สถาปัตยกรรมที่ดีที่สุด (หรืออย่างน้อยก็เป็นทางเลือกที่ดี) สำหรับแอปพลิเคชันที่กำหนดขึ้นอยู่กับข้อกำหนดที่มีอยู่
ให้เราคิดสั้น ๆ เกี่ยวกับคำนี้ว่า ข้อกำหนด จริงๆแล้วหมายถึง:
- UI ของคุณซับซ้อนแค่ไหน โดยทั่วไป UI แบบธรรมดาไม่ต้องการตรรกะที่ซับซ้อนในการประสานงาน ในขณะที่ UI ที่ซับซ้อนอาจต้องการตรรกะที่กว้างขวางและการควบคุมที่ละเอียดเพื่อให้ทำงานได้อย่างราบรื่น
- คุณสนใจเรื่องการทดสอบมากแค่ไหน โดยทั่วไป คลาสที่เชื่อมต่อกับเฟรมเวิร์กและ OS อย่างแน่นหนา (โดยเฉพาะ อินเทอร์เฟซผู้ใช้ ) ต้องการงานพิเศษเพื่อทดสอบ
- คุณต้องการส่งเสริมการใช้งานซ้ำและนามธรรมมากน้อยเพียงใด จะเป็นอย่างไรถ้าคุณต้องการแชร์แบ็คเอนด์ โดเมน และแม้แต่ตรรกะการนำเสนอของแอปพลิเคชันของคุณในแพลตฟอร์มต่างๆ
- คุณโดยธรรมชาติหรือเปล่า ในทางปฏิบัติ , พวกชอบความสมบูรณ์แบบ , ขี้เกียจ หรือทั้งหมดข้างต้น ในเวลาต่างกัน ในสถานการณ์ต่างกัน
ฉันชอบที่จะเขียนบทความที่ฉันพูดคุยอย่างละเอียดว่า MVVM ทำงานอย่างไรโดยคำนึงถึงข้อกำหนดและข้อกังวลที่ระบุไว้ข้างต้น น่าเสียดาย ที่พวกคุณบางคนอาจเข้าใจผิดคิดว่ามีทางเดียวเท่านั้นที่จะทำ MVVM
แต่ฉันจะพูดถึงสองแนวทางที่แตกต่างกันในแนวคิดทั่วไปของ MVVM ซึ่งมีข้อดีและข้อเสียที่แตกต่างกันมาก แต่ก่อนอื่น เรามาเริ่มด้วยแนวคิดทั่วไปกันก่อน
คุณจะไม่อ้างอิงถึงคลาสของมุมมองของคุณ
สำหรับเพื่อนๆ ที่ไม่สามารถอ่านภาษาอังกฤษแบบเก่าได้: “คุณไม่สามารถอ้างอิงคลาสการดู ."
นอกเหนือจากการใช้ชื่อ ViewModel (ซึ่งทำให้สับสนหากคลาสเต็มไปด้วย ตรรกะ ) กฎเหล็กหุ้มข้อหนึ่งของสถาปัตยกรรม MVVM คือคุณไม่สามารถอ้างอิง View จาก ViewModel ได้
ตอนนี้ ความสับสนช่วงแรกอาจเกิดขึ้นจากคำว่า "การอ้างอิง" ซึ่งฉันจะพูดใหม่โดยใช้ศัพท์แสงที่แตกต่างกันหลายระดับ:
- ViewModels ของคุณต้องไม่มีการอ้างอิงใดๆ (ตัวแปรสมาชิก คุณสมบัติ ฟิลด์ที่เปลี่ยนแปลง/เปลี่ยนไม่ได้) ไปยัง Views ใดๆ
- ViewModels ของคุณอาจไม่ขึ้นอยู่กับการดูใดๆ
- ViewModels ของคุณอาจไม่พูดคุยกับ Views ของคุณโดยตรง
ตอนนี้ บนแพลตฟอร์ม Android สาเหตุของกฎนี้ไม่ใช่แค่การฝ่าฝืนกฎเท่านั้นที่แย่ เพราะคนที่ดูเหมือนจะรู้เกี่ยวกับสถาปัตยกรรมซอฟต์แวร์บอกคุณว่ากฎนี้ไม่ดี
เมื่อใช้คลาส ViewModel จาก Architecture Components (ซึ่งได้รับการออกแบบให้มีอินสแตนซ์ คงอยู่ นานกว่าวงจรชีวิตของ Fragment/Activity ตามความเหมาะสม ) การอ้างอิง View กำลังขอการรั่วไหลของหน่วยความจำที่ร้ายแรง .
สำหรับสาเหตุที่ MVVM โดยทั่วไปไม่อนุญาตให้มีการอ้างอิงดังกล่าว เป้าหมายคือ สมมุติ เพื่อให้ทั้ง View และ ViewModel ง่ายต่อการทดสอบและเขียน
คนอื่นๆ อาจชี้ให้เห็นด้วยว่าคุณลักษณะนี้ส่งเสริมความสามารถในการใช้ซ้ำของ ViewModels แต่นี่เป็นตรงที่สิ่งต่างๆ แตกสลายด้วยรูปแบบนี้ .
ก่อนที่เราจะดูโค้ดนี้ โปรดทราบว่า โดยส่วนตัวแล้วฉันไม่ได้ใช้ LiveData ในรหัสการผลิตของฉันเอง ฉันชอบที่จะเขียนรูปแบบผู้เผยแพร่-สมาชิกของฉันเองในทุกวันนี้ แต่สิ่งที่ฉันพูดด้านล่างนี้ใช้ได้กับไลบรารีใดๆ ที่อนุญาตให้ลิงก์รูปแบบ PubSub/Observer จาก ViewModel ไปยังมุมมอง
บทความนี้มาพร้อมกับวิดีโอแนะนำที่ครอบคลุมแนวคิดเดียวกันมากมายที่นี่:
ViewLogic + ViewModel หรือ View + ViewModelController?
เมื่อฉันพูดว่า "พัง" ในส่วนก่อนหน้า ฉันไม่ได้หมายถึงว่ารูปแบบนั้นแตกจริงๆ ฉันหมายความว่ามันแบ่งออกเป็น (อย่างน้อย) สองแนวทางที่แตกต่างกันซึ่งมีรูปลักษณ์ ประโยชน์ และผลที่ตามมาที่แตกต่างกันมาก
ให้เราพิจารณาสองแนวทางนี้ และเมื่อคุณต้องการจะเลือกวิธีใดวิธีหนึ่งมากกว่ากัน
แนวทางแรก:จัดลำดับความสำคัญของ ViewModels ที่นำกลับมาใช้ใหม่ได้
เท่าที่ฉันสามารถบอกได้ คนส่วนใหญ่ที่ใช้ MVVM ตั้งเป้าที่จะส่งเสริมความสามารถในการใช้งานซ้ำของ ViewModels เพื่อให้สามารถนำมาใช้ซ้ำสำหรับ n จำนวนการดูที่แตกต่างกัน (อัตราส่วนหลายต่อหนึ่ง)
พูดง่ายๆ ก็คือ มีสองวิธีที่คุณสามารถใช้ซ้ำได้:
- โดยไม่ได้อ้างอิงข้อมูลพร็อพเพอร์ตี้เฉพาะ หวังว่านี่ไม่ใช่ข่าวสำหรับคุณในตอนนี้
- โดย รู้ ให้น้อยที่สุดเกี่ยวกับรายละเอียดของ UI โดยทั่วไป
จุดที่สองอาจฟังดูคลุมเครือหรือขัดกับสัญชาตญาณ (มันจะรู้อะไรเกี่ยวกับสิ่งที่ไม่ได้อ้างอิงได้อย่างไร) ดังนั้นฉันคิดว่าถึงเวลาต้องดูโค้ดแล้ว:
class NoteViewModel(val repo: NoteRepo): ViewModel(){
//Note: you may also publish data to the View via Databinding, RxJava Observables, and other approaches. Although I do not like to use LiveData in back end classes, it works great with Android front end with 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)
}
}
แม้ว่านี่จะเป็นตัวอย่างที่ง่ายมาก แต่ประเด็นก็คือสิ่งเดียวที่ ViewModel นี้เปิดเผยต่อสาธารณะ (นอกเหนือจากฟังก์ชัน handleEvent) เป็นวัตถุ Note แบบธรรมดา:
data class Note(val creationDate:String,
val contents:String,
val imageUrl: String,
val creator: User?)
ด้วยวิธีการเฉพาะนี้ ViewModel จึงสามารถแยกออกจาก View ใด View หนึ่งได้อย่างแท้จริง แต่ยังรวมถึงรายละเอียด และโดยการขยาย ตรรกะการนำเสนอ ของมุมมองเฉพาะใดๆ
หากสิ่งที่ฉันพูดยังคงคลุมเครือ ฉันสัญญาว่ามันจะชัดเจนเมื่อฉันอธิบายวิธีอื่น
แม้ว่าหัวข้อก่อนหน้าของฉัน “ViewLogic + ViewModel… ” ไม่ได้มีไว้เพื่อใช้หรือดำเนินการอย่างจริงจัง ฉันหมายความว่าโดยการมี ViewModels ที่แยกส่วนและนำกลับมาใช้ใหม่ได้มาก ตอนนี้เราจึงขึ้นอยู่กับตัว View เองในการหาวิธีแสดงผล/ผูกวัตถุ Note นี้บนหน้าจอ
พวกเราบางคนไม่ชอบเติม View class ด้วย Logic
นี่คือจุดที่สิ่งต่างๆ กลายเป็นโคลนและขึ้นอยู่กับข้อกำหนดของโครงการ . ฉันไม่ได้บอกว่าการเติมดูคลาสด้วยตรรกะเช่น…:
private fun observeViewModel() {
viewModel.notes.observe(
viewLifecycleOwner,
Observer { notes: List<Note> ->
if (notes.isEmpty()) showEmptyState()
else showNoteList(notes)
}
)
//..
}
…คือ เสมอ เป็นสิ่งที่ไม่ดี แต่คลาสที่เชื่อมต่อกับแพลตฟอร์มอย่างแน่นหนา (เช่น Fragments) นั้นยากต่อการทดสอบ และคลาสที่มีตรรกะในนั้นคือคลาสที่สำคัญที่สุดในการทดสอบ!
พูดง่ายๆ ก็คือ การนำสิ่งที่ฉันคิดว่าเป็นหลักการสีทองของสถาปัตยกรรมที่ดีมาใช้นั้น ถือเป็นความล้มเหลว:การแยกข้อกังวล
ความคิดเห็นส่วนตัวของฉันคือควรใช้การแยกข้อกังวลในระดับที่สูงมาก แต่อย่าพลาดว่าแอปพลิเคชั่น Cash cow จำนวนมากเขียนขึ้นโดยผู้ที่ไม่มีเงื่อนงำน้อยที่สุดเกี่ยวกับความหมาย
แนวทางที่เราจะพูดถึงต่อไปในขณะที่มีผลข้างเคียงของตัวเอง อีกครั้งจะลบตรรกะการนำเสนอออกจากมุมมอง
ส่วนใหญ่ก็แล้วแต่
แนวทางที่สอง:Humble View, Control-Freak ViewModel
บางครั้งไม่มีการควบคุมการดูของคุณอย่างละเอียด (ซึ่งเป็นผลมาจากการจัดลำดับความสำคัญของความสามารถในการใช้งานซ้ำของ ViewModels) จริง ๆ แล้วแย่จัง
เพื่อทำให้ฉันไม่ค่อยกระตือรือร้นที่จะใช้วิธีการก่อนหน้านี้โดยไม่เลือกปฏิบัติ ฉันพบว่าฉันบ่อยครั้ง อย่า จำเป็นต้องใช้ ViewModel ซ้ำ .
แดกดัน “สิ่งที่เป็นนามธรรมมากเกินไป” เป็นการวิจารณ์ทั่วไปของ MVP เหนือ MVVM
อย่างที่กล่าวไปแล้ว เราไม่สามารถเพิ่มการอ้างอิงกลับเข้าไปใน ViewModel เพื่อที่จะได้กลับมาควบคุมมุมมองที่ละเอียดนี้อีกครั้ง โดยพื้นฐานแล้วจะเป็นเพียงแค่ MVP + หน่วยความจำรั่ว (สมมติว่าคุณยังคงใช้ ViewModel จาก AAC)
ทางเลือกอื่นคือสร้าง ViewModels ของคุณให้มีพฤติกรรมเกือบทั้งหมด , สถานะ และ ตรรกะการนำเสนอ ของมุมมองที่กำหนด มุมมองจะต้องยังคงผูกกับ ViewModel แน่นอน แต่มีรายละเอียดเพียงพอเกี่ยวกับ View อยู่ใน ViewModel ที่ฟังก์ชั่นของ View จะลดลงเหลือหนึ่งซับ (มีข้อยกเว้นเล็กน้อย)
ในการตั้งชื่อแบบแผนการตั้งชื่อของ Martin Fowler สิ่งนี้เรียกว่า Passive View/Screen ชื่อที่ใช้กันโดยทั่วไปสำหรับแนวทางนี้คือ รูปแบบวัตถุที่ต่ำต้อย .
เพื่อให้บรรลุสิ่งนี้ คุณต้องให้ ViewModel ของคุณมีฟิลด์ที่สังเกตได้ (อย่างไรก็ตาม คุณทำได้ – การผูกข้อมูล, Rx, LiveData, อะไรก็ตาม) สำหรับทุกการควบคุมหรือวิดเจ็ตที่มีอยู่ในมุมมอง:
class UserViewModel(
val repo: IUserRepository,
){
//The actual data model is kept private to avoid 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 ยังคงต้องเชื่อมต่อกับ ViewModel แต่ฟังก์ชันที่จำเป็นในการทำเช่นนั้นจะกลายเป็นเรื่องง่ายในการเขียน:
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" is the value of the MutableLiveData object, which is inferred to be a 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 นี้ซ้ำ ที่อื่น . นอกจากนี้ มุมมองของเรายังถ่อมตัวมากพอ (ขึ้นอยู่กับมาตรฐานและความชอบของคุณสำหรับการครอบคลุมโค้ด) และเขียนได้ง่ายมาก
บางครั้ง คุณอาจเจอสถานการณ์ที่คุณต้องหามาตรการครึ่งหนึ่งระหว่างการแจกแจงตรรกะของการนำเสนอ ระหว่าง Views และ ViewModels ซึ่งไม่ปฏิบัติตามแนวทางใดแนวทางหนึ่งเหล่านี้อย่างเคร่งครัด
ฉันไม่ได้สนับสนุนแนวทางหนึ่งมากกว่าวิธีอื่น แต่สนับสนุนให้คุณมีความยืดหยุ่นในแนวทางของคุณ โดยพิจารณาจากข้อกำหนดที่มีอยู่
เลือกสถาปัตยกรรมของคุณตามการตั้งค่าและข้อกำหนด
ประเด็นของบทความนี้คือการดูสองแนวทางที่แตกต่างกันซึ่งนักพัฒนาซอฟต์แวร์สามารถใช้ในแง่ของการสร้างสถาปัตยกรรม GUI สไตล์ MVVM บนแพลตฟอร์ม Android (โดยบางส่วนจะส่งต่อไปยังแพลตฟอร์มอื่น)
อันที่จริง เราอาจเจาะจงมากขึ้นเกี่ยวกับความแตกต่างเล็กๆ น้อยๆ แม้จะอยู่ในสองแนวทางนี้
- หากมุมมองสังเกตฟิลด์สำหรับวิดเจ็ต/การควบคุมแต่ละรายการที่มี หรือควรสังเกตฟิลด์หนึ่งซึ่งเผยแพร่ โมเดล เดียว เพื่อแสดงมุมมองใหม่ทุกครั้งหรือไม่
- บางทีเราอาจไม่ต้องทำให้ ViewModels ของเราเป็นแบบตัวต่อตัว ในขณะที่รักษามุมมองของเราให้เป็นวัตถุที่อ่อนน้อมถ่อมตน เพียงแค่เพิ่มบางอย่าง เช่น ผู้นำเสนอหรือตัวควบคุมลงในมิกซ์
การพูดคุยมีราคาถูก และฉันขอแนะนำอย่างยิ่งให้คุณลองเรียนรู้สิ่งเหล่านี้ ในโค้ด เพื่อจะได้ไม่ต้องพึ่งคนอย่างผมมาบอกว่าต้องทำยังไง
ท้ายที่สุด ฉันคิดว่าสององค์ประกอบที่สร้างสถาปัตยกรรมที่ยอดเยี่ยมนั้นมาจากข้อควรพิจารณาต่อไปนี้:
ประการแรก เล่นกับหลายวิธีจนกว่าคุณจะพบวิธีที่คุณชอบ . ทำได้ดีที่สุดโดยการสร้างแอปพลิเคชันจริง (อาจเป็นเรื่องง่าย) ในแต่ละสไตล์ และดูว่ารู้สึกใช่อย่างไร .
ประการที่สอง เข้าใจว่าการตั้งค่านอกเหนือจากความชอบ สไตล์ที่แตกต่างกันมักจะเน้นถึงประโยชน์ที่แตกต่างกันเพื่อแลกกับการขาดดุลที่แตกต่างกัน ในที่สุด คุณจะสามารถเลือกทางเลือกที่ดีโดยพิจารณาจากความเข้าใจในข้อกำหนดของโครงการมากกว่าความเชื่อที่มองไม่เห็น .
เรียนรู้เพิ่มเติมเกี่ยวกับสถาปัตยกรรมซอฟต์แวร์:
โซเชียล
https://www.instagram.com/rkay301/
https://www.facebook.com/wiseassblog/
https://twitter.com/wiseass301
https://wiseassblog.com/