MVMDemo
MVM设计模式 for iOS, Android, H5; 大前端的POM设计模式
设计之初是按照MVM(Mediator, View, ViewModel)的思路设计的,完成后搜索了一下网上有没有类似思路, 最后发现了Page Object Model, also known as POM, is a design pattern in Selenium that creates an object repository for storing all web elements.
; 在iOS, Android, H5中还没搜到这方面的文章
一、MVM简介
本质上就是一个后端和UI的中间件, 向后端请求接口之后对数据解析, 输出一个UI需要的且可以直接使用的数据模型(包含data
和function
),前端直接渲染, UI开发无需关注逻辑,逻辑开发者无需关注UI;
demo中的start
入门版代码简单,初级开发也能快速上手;
具体实现请点击https://github.com/AblerSong/MVMDemo
二、设计思路
- 创建一个Page
(iOS: ViewController; Android: Activity or fragment; Vue:.vue)
- 将页面UI拆分不同组件, 每个组件对应一个
ViewModel
, UI文字定义为ViewModel
变量, UI点击事件定义成ViewModel
闭包, - 创建一个
Mediator
, 接口请求完成后, 根据后端返回的数据,初始化所有ViewModel
的变量和闭包; 具体看 四、代码实现 - 将
Mediator
给UI开发者, 直接根据Mediator
中的数据进行绑定渲染即可
三、具体实现和细节要求(方案参考)
- 1.按照页面拆分,每个页面有一个
PageMediator
- 2.将一个页面按照
行
拆分成不同组件, 每种组件对应一种ViewModel
, 通过PageMediator
管理 - 3.每个
PageMediator
通过MediatorManager Singleton
(demo中h5用Vuex
替代); 这样的话所有的数据 keepAlive; UI没有 keepAlive
MediatorManager Singleton
管理PageMediator
,PageMediator
管理ViewModel
如下:
MediatorManager.getSingleton().mediator = new Mediator
MediatorManager.getSingleton().mediator = null
- 4.当一个页面组件非常多的时候,
PageMediator
肯定会非常复杂; 这个时候可以通过design pattern
对PageMediator
进行拆分; 具体拆分看个人习惯和架构能力 - 5.
PageMediator
的public
变量
和方法
一定要深思熟虑, 如果PageMediator
封装不是很好, 只要对UI暴露的 api 没问题; 后续重构逻辑不会影响UI, 同样修改UI对逻辑影响很小 - 6.实际开发中还需要封装其他模块, 方便后期使用
Unit Test
替代UI Test
; 比如Router, Toast, Network等, e.g.Toast:
class ToastViewModel {
// 在 setter 中 进行Toast, 可以做到全局处理, 后期可以根据 变量值 进行 Unit Test
// 实际开发中建议使用 Rx 系列框架
set toast(value) {
Toast(value)
}
}
四、代码实现
image.png比如实现登录需求如上图
下面代码可以看出, vue通过绑定; iOS 通过tableView, Android通过 Adapter,直接根据list进行渲染即可; 数据绑定放在每个页面的组件中; 逻辑全部在Mediator
中
VUE
Mediator {
username_text = "username"
password_text = "password"
_username_str = ""
_password_str = ""
login_btn_disabled = true
constructor() {
this.init()
}
init() {}
set username_str(value) {
this._username_str = value
this.update_login_btn_disabled()
}
get username_str() {
return this._username_str
}
set password_str(value) {
this._password_str = value
this.update_login_btn_disabled()
}
get password_str() {
return this._password_str
}
update_login_btn_disabled() {
this.login_btn_disabled = !(this.username_str?.length && this.password_str?.length)
}
onSubmit() {
if (this.username_str == "admin" && this.password_str == "123456") {
router.back()
} else {
}
}
}
Android
class ButtonViewModel (
val buttonState: BehaviorSubject<Boolean> = BehaviorSubject.createDefault(false),
var buttonText: BehaviorSubject<String> = BehaviorSubject.createDefault(""),
) {
var clickItem = {}
}
class InputViewModel (
val text: BehaviorSubject<String> = BehaviorSubject.createDefault(""),
val value: BehaviorSubject<String> = BehaviorSubject.createDefault("")
) {}
class Mediator : BaseMediator () {
val usernameViewModel: InputViewModel = InputViewModel()
val passwordViewModel: InputViewModel = InputViewModel()
val buttonViewModel: ButtonViewModel = ButtonViewModel()
init {
usernameViewModel.text.onNext("username")
passwordViewModel.text.onNext("password")
val isNotEmpty: (String, String) -> Boolean = { name: String, age: String ->
name.isNotEmpty() && age.isNotEmpty()
}
val d1 = Observable.combineLatest(usernameViewModel.value, passwordViewModel.value, isNotEmpty).subscribe {
buttonViewModel.buttonState.onNext(it)
}
buttonViewModel.clickItem = {
val username = usernameViewModel.value.value
val password = passwordViewModel.value.value
if (username == "admin" && password == "123456") {
routerSubject.onNext(R.layout.activity_main)
} else {
ToastManager.toastSubject.onNext("input error")
}
}
compositeDisposable.add(d1)
}
val dataList by lazy { initList() }
private fun initList(): List<Map<String, Any>> {
val m1 = mapOf(Pair("viewType", R.layout.input_item), Pair("viewHolder", InputViewHolder::class.java), Pair("ViewModel", usernameViewModel))
val m2 = mapOf(Pair("viewType", R.layout.input_item), Pair("viewHolder", InputViewHolder::class.java), Pair("ViewModel", passwordViewModel))
val m3 = mapOf(Pair("viewType", R.layout.button_item), Pair("viewHolder", ButtonViewHolder::class.java), Pair("ViewModel", buttonViewModel))
return listOf<Map<String, Any>>(m1, m2, m3)
}
}
iOS
class ButtonCellViewModel: BaseViewModel {
let login_btn_disabled = BehaviorRelay(value: false)
var onSubmit = {}
}
class TextFieldCellViewModel: BaseViewModel {
let text = BehaviorRelay(value: "")
let value = BehaviorRelay(value: "")
}
class Mediator: BaseMediator {
let usernameViewModel = TextFieldCellViewModel()
let passwordViewModel = TextFieldCellViewModel()
let buttonCellViewModel = ButtonCellViewModel()
lazy var list: [[[String : Any]]] = {
let arr: [[[String : Any]]] = [
[
["model":usernameViewModel,"reuseIdentifier":textFieldCellReuseIdentifier],
["model":passwordViewModel,"reuseIdentifier":textFieldCellReuseIdentifier],
],
[
["model":buttonCellViewModel,"reuseIdentifier":buttonCellReuseIdentifier]
]
]
return arr
}()
override init() {
super.init()
initPasswordViewModel()
initUsernameViewModel()
initButtonCellViewModel()
}
func initUsernameViewModel() {
usernameViewModel.text.accept("username")
}
func initPasswordViewModel() {
passwordViewModel.text.accept("password")
}
func initButtonCellViewModel() {
let combineLatest = Observable.combineLatest(usernameViewModel.value, passwordViewModel.value)
combineLatest.map { (username: String, password: String) -> Bool in
return username.count > 0 && password.count > 0
}.bind(to: buttonCellViewModel.login_btn_disabled).disposed(by: disposeBag)
buttonCellViewModel.onSubmit = {
combineLatest.subscribe( onNext: { (username: String, password: String) in
if username == "Admin", password == "123456" {
RouterBehaviorSubject.onNext(RouterModel(type: .pop))
} else {
ToastBehaviorSubject.onNext("input error")
}
}).dispose()
}
}
}
五、优缺点
优点 :
- 相比VIPER, MVI 等框架, 核心思想简单, 方便理解
- UI 和 逻辑拆分, 方便任务拆解组合, 提高代码复用性
- 理论上拆解合适, 可以通过对
Mediator
进行unit test
替代UI test
; 非常容易进行白盒自动化测试 - 由于
MediatorManager Singleton
存在; 相当于所有的数据 keepAlive; UI没有keepAlive; 数据唯一, 方便管理 - 业务代码结构统一, 开发人员可以快速接手其他人的代码
缺点 :
- 开发者不注意容易内存泄露,且不易定位
六、总结
从实际开发来看, 该框架非常非常适合h5; 比如demo(vue)中拆分成.vue
, .scss
, .js
; 由于css文件的独立性, 极大的提高了代码的复用率;
对于h5,iOS和Android, 可以通过 Mediator
进行 Unit test
替代 UI test
, 减少错误提高测试效率;
本人非常喜欢, 可以大幅提高自动化测试效率, 写UI Test太麻烦,还是 Unit Test方便;
网友评论