phpmv设计_聊聊GUI开发中的MV*设计模式(一)

在GUI应用开发中,开发者关注的重点模块主要有3个:UI的展示

UI和用户的交互逻辑

业务逻辑

在早期,人们还未针对这种开发场景做优化时,可能习惯于将这三者杂糅在一起。虽然开发起来很快,但是后期维护麻烦,对于大型项目也不利于多人并行开发。 到了70年代,挪威计算机科学家Trygve Reenskaug在SmallTalk-76中引入了一种新的模式,MVC,确切说因该是MVC-Model-1,因为后来为了适应Web开发,该模式又衍生出了MVC-Model-2,也就是我们现在后端常用的各种MVC框架。如果没有明确说明,下面所说的MVC指代的都只是MVC-Model-1

MVC(Model-View-Controller)

MVC贯彻了SoC(Separation of Concerns),将UI展示抽为View层,业务逻辑抽为Model层,UI和用户的交互逻辑则抽为了Controller层。 3者的交互流程大致如下:MVC模式View层订阅了Model的修改事件,同时将用户交互代理到Controller上

Controller对交互做一些处理,如果有需要,修改Model数据

Model层数据修改之后,通知View层响应更改

优点关注分离,并且交互逻辑基本都抽到了Controller,对于Model层来说可以提高代码的复用性

分模块之后各个模块可以并行开发

View的更新只能来自于Model的修改,单向的数据流提高了可回溯性

缺点View层过度依赖Model层,不利于测试和代码复用

Controller逻辑过重,不利于后期维护

Controller层依赖View,不利于测试

MVP(Model-View-Presenter)

MVP是MVC模式的一个变种,主要提高了代码的可测试性。

MVP中View需要定义好一个抽象接口,View和Presenter都需要依赖这个抽象接口来开发,所以Presenter就能通过自行mock一个View对象来提高可测试性。

同时,Presenter作为衔接View和Model的桥梁,负责修改Model以及更新View,这样就可以移除View对Model层的直接依赖,进一步提高了View层的可测试性。

MVP的交互流程如下:MVP模式

优点分离View和Model,提高代码可复用性和可测试性

为View和Presenter定义明确的接口,利于View和Presenter测试

缺点View的更新全由Presenter来维护,会导致presenter过于庞大

MVVM(Model-View-ViewModel)

最后一个就是老生常谈的MVVM了。最早(不一定准确)应该是由我软在2005年提出的,随着WPF和SilverLight被发扬光大(其实死的很早)。相比较于MVP,Presenter被换成了ViewModel。

从View层考虑,ViewModel其实就是View的一个抽象,包含了他所需的数据和行为。 从Model层来看,ViewModel又像是一个值转换器,将Model转换成View层需要的一种格式,所以有时候MVVM又被称为Model-View-Binder模式。

借助于Data-Binding技术,MVVM完成了Model和View的双向绑定,也不再包含对View层的依赖,让开发者可以更专注于和状态和行为的管理。

在我软技术栈上,Data-Binding主要由XACML来实现,而前端的主流框架,例如React,Vue则是借助于单向数据流+V-DOM diff实现类似效果的。

优点省去Presenter中Model和View之间的手动同步,移除了对View的依赖

代码更简洁,可读性更强

缺点在大型项目上MVVM的性能可能会有问题

下面就让我们假设一个场景来实践一下MVC和MVP 2种模式。假设做一个很简单的界面:

顶部是一个下拉框,用于选择歌手;底部是一个专辑列表,显示当前歌手所有的专辑列表。

在实践上述2种模式之前,我们需要做一些准备工作:示例的语言选用Typescript(本来想用C#,无奈已经忘得差不多了...)

不管MVC、MVP,都涉及到单向的数据流动,实现方式很多种,在这里我就采用了观察者模式

最后开始之前,郑重声明,代码不一定能正常跑起来(<_>

首先贴一下简陋的观察者模式(类似C#事件)实现:

export class UIEvent {

subs: Array>;

constructor() {

this.subs = [];

}

on(cb: UIEventHandler) {

this.subs.push(cb);

}

off(cb: UIEventHandler) {

let index: number = this.subs.findIndex(handler => handler === cb);

if (index > -1) {

this.subs.splice(index, 1);

}

}

emit(args: T) {

this.subs.forEach(handler => {

handler(args);

});

}

}

通过on/off来添加或者移除订阅,通过emit来通知订阅。在这里我把每个UIEvent的回调形式都固定成了如下形式:

export type UIEventHandler = (args: T) => void

接受一个派生于UIEventArg的入参。

同时定义一个下拉选择事件参数的类型:SelectEventArg,后面订阅View的下拉事件会用到

export class SelectEventArg implements UIEventArg {

target: object;

name: string;

}

然后我们的数据格式也定义了,比较简单:

export interface IRecord {

name: string;

date: string;

author: string;

}

export interface IAuthor {

name: string;

age: number;

}

还有需要对2个控件做一个简单的抽象:

export interface IDropdownList extends IControl {

selectedEvent: UIEvent;

data: Array

}

export interface IList extends IControl {

data: Array

}

其中data属性表示控件的数据源,我们可以认为设置了之后控件会自动绑定属性,然后具体的类型由使用者来确定,所以设计成了泛型类。

下拉菜单接口还有另一个属性:selectedEvent。这就是刚才说的观察者模式的应用,事件会在选择项改变的时候触发。

至此,我们的准备工作都已经介绍完了,就要进入正题了。

先实现一下MVC模式:View层的主要功能就是展示,将交互转发到Controller,同时订阅Model层的数据变化,实现如下:

export class View {

private recordModel: ObservableRecords;

private authorModel: ObservableAuthors;

private dropdownList: IDropdownList;

private list: IList;

selectChange: UIEvent

constructor() {

this.recordModel.changeEvent.on(this.bindRecords);

this.dropdownList.selectedEvent.on(this.onSelectChange);

this.dropdownList.data = this.authorModel.getAuthors();

this.list.data = this.recordModel.getRecords();

}

bindRecords(args: RecordChangeEventArg) {

this.list.data = args.data;

}

onSelectChange(args: SelectEventArg) {

this.selectChange.emit(args);

}

}

2. Model相对简单一些,除了数据处理之外,还需要是一个可以被观察的对象,让多个View可以同时订阅。

export class ObservableRecords {

private static data: Array;

changeEvent: UIEvent;

getRecords(): Array {

return ObservableRecords.data;

}

getRecordsByAuthorName(name: string): Array {

return ObservableRecords.data.filter(r => r.name === name);

}

updateRecordsByAuthorName(name: string): void {

this.changeEvent.emit(new RecordChangeEventArg(this.getRecordsByAuthorName(name)));

}

}

export class ObservableAuthors {

private static data: Array;

getAuthors(): Array {

return ObservableAuthors.data;

}

}

3. Controller层依赖了View和Model层,因为需要响应View层的事件,同时又要修改Model层数据:

export class Controller {

private recordModel: ObservableRecords;

private view: View;

constructor() {

this.view.selectChange.on(this.updateRecords);

}

updateRecords(args: SelectEventArg) {

this.recordModel.updateRecordsByAuthorName(args.name);

}

}

实践完后,给人第一的直觉就是是不是太麻烦了?因为现在这个界面太简单了,用不用MVC差别不大,甚至直接不分层,全部写在一起或许更省力。

但是想象一下,如果让你开发一网易云那样的应用,不分离关注点,完全不好分工;同样是歌单,有我的歌单,我喜欢,年度歌单。业务模型完全是一致的,如果分层之后就完全可以重用。还有一些组件,不分层完全不好测试。

还有一个MVP的实现,我们就先留着下篇继续。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值