简单易懂的MVM设计模式for H5, iOS,Android,自动化测试效率极高

MVMDemo

h5,iOS,Android, software architecture, MVM (Mediator, View, ViewModel)

相比MVC, MVVM, MVP等一些框架, MVM (Mediator, View, ViewModel)框架的思路非常简单, 基本上知道原理自己可以搭建,
核心思想是:

  • 每个页面对应一个Model, Model包含 data & function;
  • 将页面UI和 data 双向绑定;
  • 页面click event调用对应 function;
  • toast, 页面跳转等等UI事件, 则根据subject进行跳转;(需要Rx框架基础,当然可以自己设计类似的方案)
一、MVM简介

本质上就是一个后端和UI的中间件, 向后端请求接口之后对数据解析, 输出一个UI需要的且可以直接使用的数据模型(包含datafunction),前端直接渲染, UI开发无需关注逻辑,逻辑开发者无需关注UI;

demo中的 start 入门版,代码简单,初级开发也能轻易看懂

let ToastSubject = PublishSubject<String>
inputView.text <-bind-> mediator.text

button.click = function() {
    mediator.click()
}

// vue
created() {
    mediator.created()
    ToastSubject.subscribe((value) => {
        Toast(value)
    })
}
// Android
fun onCreate(savedInstanceState: Bundle?) {
    mediator.onCreate()
    ToastSubject.subscribe {
        Toast(value)
    }
}
// iOS
func viewDidLoad() {
    mediator.viewDidLoad()
    ToastSubject.subscribe(onNext: {value in
        Toast(value)
    })
}




二、设计思路
    1. 创建一个Page(iOS: ViewController; Android: Activity or fragment; Vue:.vue)
    1. 将页面UI拆分不同组件, 每个组件对应一个 viewModel,可以将UI需要的点击事件定义成 ViewModel中的闭包,
    1. 创建一个Mediator, 向后端请求网络数据, 请求完成后, 初始化所有的ViewModel,给对应的变量赋值; 在Mediator中对ViewModel中的闭包,进行初始化
    1. Mediator给UI开发者, 直接根据Mediator中的数据进行绑定渲染即可
三、具体实现和细节要求(方案参考)
    1. 按照页面拆分,每个页面有一个 Mediator , 每个 Mediator 通过 MediatorManager Singleton(demo中用 Vuex 替代)
    1. 将一个页面按照行拆分成不同组件, 每种组件对应一种 ViewModel, 通过 Mediator 管理
    1. 当一个页面组件非常多的时候, Mediator 肯定会非常复杂; 这个时候可以通过 Design patternMediator 进行拆分; 具体拆分看个人习惯和架构能力
    1. public的 变量方法 一定要深思熟虑, 如果 Mediator 封装不是很好, 只要对UI暴露的 api 没问题; 后续重构逻辑不会影响UI, 同样修改UI对逻辑影响很小
    1. 实际开发中还需要封装其他模块, 比如Router, Toast, Network等

根据需要进行

MediatorManager.getSingleton().mediator = new Mediator
MediatorManager.getSingleton().mediator = null
四、代码实现

下面代码可以看出, vue通过v-for; iOS 通过tableView; Android通过 Adapter直接进行渲染即可; 数据绑定放在每个页面的组件中; 逻辑全部在Mediator

具体实现请点击demo; github.com/AblerSong/M…

vue

<template>
  <div class="advanced">
    <van-cell-group>
      <component :is="item.name" :model="item.model" v-for="(item, index) in list" :key="index" :class="item.cls" />
    </van-cell-group>
  </div>
</template>
<script>
import { mapState } from "vuex"
import Checkbox from "./components/Checkbox"
import Radio from "./components/Radio"
import Province from "./components/Province"
import Calculator from "./components/Calculator"
import Submit from "./components/Submit"
import Expand from "./components/Expand"
import Stretch from "./components/Stretch"


export default {
  created() {
    this.$store.dispatch("initAdvancedMediator")
    import("./css/style.scss")
  },
  components: {
    Radio,
    Checkbox,
    Province,
    Calculator,
    Submit,
    Expand,
    Stretch,
  },
  computed: {
    ...mapState({
      advancedMediator: (state) => state.advancedMediator,
    }),
    list() {
      let list = this.advancedMediator.list
      return list
    },
  },
}
</script>

iOS

class AdvancedVC: UITableViewController {
    let disposeBag = DisposeBag()
    
    var mediator: AdvancedMediator {
        get {
            guard let mediator = MediatorManager.shared.advancedMediator else {
                let mediator = AdvancedMediator()
                MediatorManager.shared.advancedMediator = mediator
                return mediator
            }
            return mediator
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()


        tableView.register(TextFieldCell.self, forCellReuseIdentifier: textFieldCellReuseIdentifier)
        tableView.register(ExpandCell.self, forCellReuseIdentifier: "ExpandCell")
        tableView.register(MoreCell.self, forCellReuseIdentifier: "MoreCell")
        tableView.register(ProvinceCell.self, forCellReuseIdentifier: "ProvinceCell")
        
        mediator.reloadTableView.subscribe(onNext: {[unowned self] value in
            if value {
                tableView.reloadData()
            }
        }).disposed(by: disposeBag)
        
    }


    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        return mediator.list.count
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        let list = mediator.list[section]
        return list.count
    }
    
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let list = mediator.list[indexPath.section]
        if let model = list[indexPath.row]["model"] as? ExpandCellViewModel {
            return model.height
        }
        return 80
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let list = mediator.list[indexPath.section]
        let reuseIdentifier = list[indexPath.row]["reuseIdentifier"] as! String
        let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! MyTableViewCell
        let model = list[indexPath.row]["model"]
        cell.viewModel = model
        return cell
    }

}

Android

class MyAdapter(var dataList: List<Map<String, Any>>) : RecyclerView.Adapter<BaseViewHolder>() {

    private fun initViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val itemView = inflater.inflate(viewType, parent, false)
        return when (viewType) {
            R.layout.button_item -> ButtonViewHolder(itemView)
            R.layout.input_item -> InputViewHolder(itemView)
            R.layout.expand_item -> ExpandViewHolder(itemView)
            R.layout.more_item -> MoreViewHolder(itemView)
            R.layout.province_item -> ProvinceViewHolder(itemView)
            else -> BaseViewHolder(itemView)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
        return initViewHolder(parent, viewType)
    }

    override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
        val data = dataList[position]
        holder.bind(data)
    }

    override fun getItemViewType(position: Int): Int {
        val data = dataList[position]
        return data["viewType"] as Int
    }


    override fun getItemCount(): Int {
        return dataList.size
    }
}
五、优缺点

优点 :

    1. 相比VIPER, MVI 等框架, 核心思想简单, 方便理解
    1. UI 和 逻辑拆分, 方便任务拆解组合, 提高代码复用性;
    1. 理论上拆解合适, 可以通过对 Mediator 进行 unit test 替代 UI test; 非常容易进行白盒自动化测试
    1. 由于 MediatorManager Singleton 存在; 相当于所有的数据 keepAlive; UI没有keepAlive; 数据唯一, 方便管理,

缺点 :

    1. 开发者不注意容易内存泄露,且不易定位
六、总结

从实际开发来看, 该框架非常非常适合h5; 比如demo(vue)中拆分成.vue, .scss, .js; 由于css的独立性, 极大的提高了代码的复用率;

对于h5,iOS和Android, 可以通过 Mediator 进行 unit test 替代 UI test, 减少错误提高开发效率

本人非常喜欢, 可以大幅提高自动化测试效率, 写UI Test太麻烦,还是 Unit Test方便


网上搜索了一下, 和MVM思想最近的应该是POM设计模式, 在iOS, Android, H5中还没搜到这方便的文章;
POM(Page Object Model):页面对象模型

Page Object Model, also known as POM, is a design pattern in Selenium that creates an object repository for storing all web elements.

  • 在POM下,应用程序的每一个页面都有一个对应的model
  • 每个model都维护着该web页面的元素集和操作这些元素的方法

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYvztPG9' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片