美文网首页
MVM设计模式 for iOS, Android, H5; 大前

MVM设计模式 for iOS, Android, H5; 大前

作者: Abler | 来源:发表于2023-07-21 05:19 被阅读0次

    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需要的且可以直接使用的数据模型(包含datafunction),前端直接渲染, 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 patternPageMediator 进行拆分; 具体拆分看个人习惯和架构能力
    • 5.PageMediatorpublic 变量方法 一定要深思熟虑, 如果 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方便;

    相关文章

      网友评论

          本文标题:MVM设计模式 for iOS, Android, H5; 大前

          本文链接:https://www.haomeiwen.com/subject/poxtpdtx.html