欢迎光临散文网 会员登陆 & 注册

MVC、PMVC框架和模式的理解

2021-11-15 21:21 作者:会飞的蜗牛007  | 我要投稿

MCV,有些地方叫框架,有些地方叫设计模式,连度娘的搜索名词都叫"MVC框架",解释说明叫mvc模式,词条说明"mvc一般指mvc框架",很难让人不混淆。

mvc的词义很好理解:m指业务模型、v指用户界面,c是控制器。

很多资料上说框架和模式还是有区别的,干嘛不解释的更清晰一点呢?框架和模式不是一个东西,这么说不行吗?那么我就是这么理解,框架是框架,模式是模式。框架是实实在在存在的,是可以用代码实现的,很多地方都有框架源码,框架通常是代码重用,比如一套代码框架,很多应用都可以复用,它甚至可以直接执行,这叫框架。而模式,它是一种思想,是对在某种环境中反复出现的问题以及解决该问题的方案的描述,它比框架更抽象,它只有在实例化的时候才可以说:“你看,我这个类用了单例模版,你看我这个类是用观察者模式实现的,用来解决各界面间的联系。”设计模式是比框架更小的元素,一个框架中往往含有一个或多个设计模式,框架总是针对某一特定应用领域,但同一模式却可适用于各种应用。或者说,框架是一本书,设计模式书中的知识使用的思想;或者说《西游记》中使用了排比句,感叹句,等等,《水浒传》也使用了排比句,感叹句,等等,这些句子的类型就是一种设计模式,《西游记》或《水浒传》的大纲就是框架,套路通用,内容不同。

所以说,通常说的mvc模式,你可以先把它理解成一种思维方式,而mvc框架把这种思维方式落到实处,通过代码的方式强制性地使应用程序的输入、处理和输出分开。使用MVC应用程序被分成三个核心部件:模型、视图、控制器;

大致理解了框架和模式的区别。就可以更好的去理解mvc的模式并去实现它。

在mvc的编程思想中:

V即View视图是指用户看到并与之交互的界面。比如由html元素组成的网页界面,或者软件的客户端界面。在视图中其实没有真正的处理发生,它只是作为一种输出数据并允许用户操作的方式。

M即model模型是指模型表示业务规则。在MVC的三个部件中,模型拥有最多的处理任务。被模型返回的数据是中立的,模型与数据格式无关,这样一个模型能为多个视图提供数据,由于应用于模型的代码只需写一次就可以被多个视图重用,所以减少了代码的重复性。

C即controller控制器是指控制器接受用户的输入并调用模型和视图去完成用户的需求,控制器本身不输出任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。

MVC的好处在于它能为应用程序处理很多不同的视图,代码复用,更清晰的视图和逻辑数据分离,视图就是视图,逻辑就是逻辑,数据就是数据。它的优点:

1.耦合性低

视图层和业务层分离,这样就允许更改视图层代码而不用重新编译模型和控制器代码,同样,一个应用的业务流程或者业务规则的改变只需要改动MVC的模型层即可。因为模型与控制器和视图相分离,所以很容易改变应用程序的数据层和业务规则。

2.重用性高

MVC模式允许使用各种不同样式的视图来访问同一个服务器端的代码,因为多个视图能共享一个模型,它包括任何WEB(HTTP)浏览器或者无线浏览器(wap),比如,用户可以通过电脑也可通过手机来订购某样产品,虽然订购的方式不一样,但处理订购产品的方式是一样的。由于模型返回的数据没有进行格式化,所以同样的构件能被不同的界面使用。

3.部署快,生命周期成本低

MVC使开发和维护用户接口的技术含量降低。使用MVC模式使开发时间得到相当大的缩减,它使程序员(Java开发人员)集中精力于业务逻辑,界面程序员(HTML和JSP开发人员)集中精力于表现形式上。

4.可维护性高

分离视图层和业务逻辑层也使得WEB应用更易于维护和修改。

mvc的解释和优点都说完了,可是我为什么要用这种思想,为什么非得把m\v\c分离,我写在一个文件中不行吗?我用它不是更复杂了吗?

参考一个网上的例子:

我要做一个界面,于是我写了一个代码文件,这个代码文件控制界面的所有逻辑,很简单对不对,非常合理。

后来又要开发一个客户端的界面,也很简单,每多一个界面写一个代码文件与之对应即可。非常合理。

但是问题来了,比如游戏登陆的时候,我请求到了玩家的信息,而这个信息要在很多界面使用,我不希望每个界面都要向服务器去请求这个信息,这样消耗太大。但是如果我这个信息存在登陆界面,后面的界面要访问这个信息的时候,登陆界面都关掉了,不太好拿到。
于是我写了一个单独的文件来存储这个信息。非常合理,存储数据的文件出现了。到目前出现了两种类型的文件,为了方便阅读代码结构和查阅代码,就给它们取个名字吧,我把那些全局要使用的信息文件称为model文件,把处理界面逻辑的文件称为view文件。

但是还有个大问题,我所有的界面上都会有一个后退按钮,它的功能只是退到当前界面的上一级界面。这时候,我似乎要存一个所有打开界面的堆栈,让后退的时候知道它要退到哪一个节目。而且因为每个界面有后退功能,我还想统一去处理后退按钮的逻辑。于是我就写了一个单独的文件,里面存了界面打开的堆栈,并统一实现了后退逻辑。因为我要维护这个堆栈,所以打开界面的时候,我也提供了一个统一的接口,好让我知道有界面打开,以便存到堆栈里面,这个文件似乎叫view和model都不合理。好吧,那我再取一个名字,就叫controller了。是不是和mvc有点像,不是像,它就是mvc,并不是先有了这种思想,才把它套用到各种应用中,而是各种应用在处理各种问题时提供的解决方案有了相通的地方,牛逼的开发者把这些相同的解决方案思想总结了一下,就出现了mvc设计模式,那么mvc框架也就应运而生。那么可能又有疑问了,既然mvc能这么自然的写出来了,我还要框架干什么?

这么说确实没毛病,但框架有点好处,在起步的时候,你的大致方向就给你规划好了,细节的地方,会更加统一,按照框架走,越走越平坦。它有它的好处,也有它的缺点,就像人生路啊,在你出生的时候,父母就给你规划好了未来,步步都是铺好的路,它不会歪不是么,它没有坑不是么?它很顺不是么?但它也就这么着了。。。悲哀啊。

我不是非要依赖它,而是走一条新的路,太难了。。。

PMVC

在理解了mvc的基础上,pmvc会相对更好理解一些了。

PMVC是一个框架,一个实现了mvc模式的框架,你可以把它理解为另一个版本的mvc框架,只是它改了一些实现方式,重新起了个名字,中心思想还是mvc思想。

贴一张官方的图:

大致看了一下这张图,是不是又懵逼了?我理解的mvc没这么复杂啊?不就是分成model、view、controller三大类吗?这上面又是Mediator,又是Facade、又是Proxy的,圈圈框框的是什么玩意儿?密密麻麻的箭头,玩自动追踪呐?!玩呐?!

不着急,先抛开箭头不看。

上图中有3个大圈圈就是刚刚说的Model、View、Controller

自己写MVC的时候,有很多View,也有很多Model,可能也会有很多Controller。但是PMVC这里都只有一个。这还得慢慢解释。

以View、Model、Controller为中心,分开来看。

View边上很多小圈Mediator,这些Mediator就是我们写的一个个控制界面的代码文件,而这些Mediator文件都统一由View管理。

//TODO更多补充

Model边上有很多Proxy,Proxy右边又有很多Obj。

这里Obj其实就是我们写的一个个存全局信息的文件,它们都继承自DataObjects。

你可以认为DataObjects和Obj才是真正的Model,但是在PMVC里面,它将Model的概念稍微放大了一点。Model包括图中的:Model、Proxy、DataObjects、Obj

这里多了一层Proxy,主要是让Obj文件的信息,不直接在各种地方读和写,而让一个Proxy来负责一个或多个Obj文件的读和写。这样可以保证数据读写的逻辑在一块,以后要统一加个啥处理,都比较简单。当然这里的Proxy都由统一的Model管理

//TODO更多补充

Controller边上有很多Command,这些Command就是我们之前说的统一处理打开界面后退界面的真正的Controller文件。而图中Controller作用是,你需要通过Controller才能调用Command,而不能直接去调用Command。所有的Command都通过这个Controller调用,自然Controller就一个了。
但是这里有点要注意的。Command文件里面并不存界面的堆栈信息,界面的堆栈信息一般会再写一个单独的文件来存储。而这个单独的文件在图上并没有。
PMVC的设计里面Command只写处理逻辑,要处理的数据,都是传进去的,所以它本身并不存储任何数据

//TODO更多补充

图上最左边的UI、ViewComponents。
我们之前自己写MVC的时候,说的过于简单了。一般绝大部分客户端都会有一个界面层和界面逻辑层。比如网页,界面层是HTML,界面逻辑层是JS。Unity里面,界面层是Perfabs,逻辑层是c#/js代码。
所以这里面的UI/ViewComponents主要是对应界面上的布局,它有自己的事件系统,比如按钮的点击,界面的拖拽等等。这些事件系统,会被后面的逻辑层:Mediator监听和处理

//TODO更多补充

关于Mediator的设计还有点需要注意。
(1)Mediator里面可以引用多个UI。比如我有个页面,上面是一个通用的玩家信息,下面是自己的独有界面,所以我可能会同时引用两个UI文件。
(2)Mediator里面也可以引用Mediator。还是这个界面有个通用的玩家信息,而且这个玩家信息的显示数据以及交互逻辑都是通用的。那么可以给这个玩家信息UI写一个独立的Mediator,然后在需要的界面Mediator里面,再引用这个玩家信息Mediator就可以。
总结而言,总体上,每个界面一个Mediator。但是如果界面中的某一块,比较通用,可以单独抽出来,做成一个Mediator,在需要的Mediator里面引用

最后就是图中的剩余部分,Facade,这个Facade其实就是一个没有任何意义的对外入口。
怎么说呢,PMVC设计者希望你不需要关心Model、View、Controller。所以写了一个Facade。
Facade提供了Model、View、Controller所有的对外接口,然后你只要调用Facade的这个接口就可以了。而Facade只是把你的调用,转给了里面的Model、View、Controller去执行而已。它自己啥都不干

//TODO更多补充

图是说完了,但具体怎么实现,还是没说清楚,纸上谈兵可不行,PMVC官网提供了非常多的语言版本的源码,版本里面的逻辑都是一模一样的,可以选择你熟悉的语言下载

具体怎么通过Controller去调用Command呢?

因为是cocoscreator做的项目,所以这里下载的是Typescript的版本

PMVC没有使用任何第三方的事件系统,因为它希望跨语言,所以自己做了一个消息系统,来代替事件系统的功能。
如果实际的项目中使用了PMVC,那么你就不需要,也不应该在使用其他事件系统了(主要是怕混淆),UI自带的事件系统(点击,拖拽等事件)除外;

源码的PMVC是一个多例类。但是很多时候只需要用单例就可以了,这里也只讨论单例。而代码里面所有关于MultitonKey字段的赋值、使用,都是多例才需要考虑的问题。
所以所有MultitonKey相关的代码,都不需要管

从Observer文件夹开始。Observer文件夹里面有3个文件:Notification、Notifier、Observer


1.Notification,Notification就是刚刚说的消息,消息里面就3个字段,Name Body Type。所有的消息都是通过Name来唯一标识的,所以呢,消息的Name都是全局唯一的,如果不唯一就会有问题。其次就是Body,大部分消息都需要Body,因为处理消息的人,很可能依赖于Body里面的信息。而Type只是一个补充,可有可无。

Notification不继承任何父类,只实现INotification接口。

这里补充一下:

extends与implements的不同

1、在类的声明中,通过关键字extends来创建一个类的子类。

一个类通过关键字implements声明自己使用一个或者多个接口。 

extends 是继承某个类, 继承之后可以使用父类的方法, 也可以重写父类的方法

implements 是实现多个接口, 接口的方法一般为空的, 必须重写才能使用 

接口实现的注意点:  

a.实现一个接口就是要实现该接口的所有的方法(抽象类除外)。 

b.接口中的方法都是抽象的。  

c.多个无关的类可以实现同一个接口,一个类可以实现多个无关的接口。

接着上面说,Notification一般情况也不会有人继承它,就是一个独立的消息名称,和消息数据的封装。

2.Notifier,Nofier没有父类,很多人继承它。只实现接口INotifier,它只有一个方法,就是SendNotification。里面的其他部分都是多例的处理,前面说了,不用管。例如Facade,如果是单例,就是全局唯一的。

那重点要看看,谁继承了它。
可以认为,所以继承它的类,才能发送Notification,不然没有提供这个方法,你想发也发不了。

继承它的是Command、Mediator、Proxy。

//TODO更多补充

3.)Observer,没有父类,没有子类,只实现接口IObserver

。如果你去使用PMVC,你可能永远不会遇到有关Observer的问题。
所以这个类,是PMVC内部使用的类。但是你要理解PMVC,这个Observer却非常关键

Observer有非常简单,两个成员,一个委托(NotifyMethod),一个Object(NofityContext)。委托是用来处理Notification的委托,它只接受一个参数Notification,并且没有返回值。

那Object(NofityContext)是干啥的呢?

首先,我们看一下Observer的使用。Observer的创建只有两个地方。
①Controller里面 new Observer(ExecuteCommand, this) 这里this就是全局唯一的Controller
②View里面 new Observer(mediator.HandleNotification, mediator)

所以Observer里面的Object只可能是2个东西,全局唯一的Controller,或者某一个Mediator

再看一下使用,所有New出来的Observer都是通过View.RegisterObserver来使用
里面是存在了一个observerMap中,让Notification.Name 能和Observer对应起来
值得注意的是,Notification.Name可以对应一个,或者多个Observer。

那么就看一下observerMap的使用。也就用在了3个地方,
View.RegisterObserver

View.NotifyObservers

View.RemoveObserver

NotifyObservers的时候,不需要使用Observer里面的Object(NotifyContext)

RemoveObserver的时候,需要使用Observer里面的Object(NotifyContext)

所以Observer里面的NofityContext只在RemoveObserver的时候才有用了。

而RemoveObserver是在什么时候使用的呢,两个地方
RemoveMediator

RemoveCommand

所以说NotifyContext是记录这个Observer中的委托是谁注册的。
在删除的时候,不能把某一个Notification.Name下的所有委托都删除了,只能谁注册的谁删除

>>>接下来看一下所有的Facade外观提供的接口,也就是pmvc所有提供的接口了。

RegisterCommand

RegisterProxy

RegisterMediator

SendNotification

有Register就有对应的Retrieve(通过Notification.Name取)、Remove

注意:代码中实现时,register和remove是一一对应的。原因:内存泄漏

RegisterCommand是Controller里面提供的接口,绑定消息名称和Command的关系。也就是前面说的,你要执行Command,没有办法直接执行,必须通过这个接口注册绑定关系,然后发送指定的Notification。所以Controller管理Command

RegisterMediator是View里面提供的接口
所以说View管理Mediator。那么RegisterMediator具体是干什么呢。其实就是给Mediator绑定消息和消息的处理函数。
具体的,每个Mediator里面会有个ListNotificationInterests,里面写着它所有关心的消息,以及一个统一的消息处理方法HandleNotification。
注册Mediator就是把这些消息都绑定到这个统一处理的方法上,然后Mediator内部可以在HandleNotification中根据消息Name和Body再具体实现处理。
刚刚说的Observer里面存了一个NotifyContext,也就是对应的Mediator,所以可以在RemoveMediator的时候,删除掉本界面相关的消息绑定

RegisterProxy,Proxy是Model管理的,自然这个接口也是Model提供的。
但是proxy和前面的Mediator、Command不同,因为它和Observer无关。
前面2个注册的目的就是绑定消息名称和消息处理函数的关系。但是Proxy不干这个事情。
那RegisterProxy干啥呢?
对的,它啥都不干。就是存了一个Proxy和Proxy.Name的对应关系,让你在需要指定Proxy的时候可以拿到而已

SendNotification,这个就是View里面的NotifyObservers,也就是使用observerMap,找到Notification对应的所有委托,并执行。
所以SendNotification的过程,就是找到所有委托并执行,它中间没有任何间隔,不会说等到下一帧啥的再执行

说明:

1、RegisterCommand(string notificationName, Func<ICommand> commandFunc)
咋一看,就是绑定消息名称,和Command的关系。但仔细一看,这里有个奇怪的地方
它不直接传一个ICommand,而是传一个委托,这个委托返回一个ICommand。

再看他的使用,在Controller里面,它绑定了Notification.Name和自己固定执行函数ExcuteCommand。
在ExcuteCommand里面,先通过执行Func<ICommand>commandFunc这个函数,获得一个ICommand
然后再调用ICommand里面的固定处理函数Execute(Notification)

这里是不是多此一举呢,我直接传一个ICommand是不是就可以了呢。
当然他这样做的目的是,实现前面说的,Command里面只处理逻辑,不保存数据。
(也就是官方文档说的,是无状态的)
因为Controller是通过这个传入的委托执行生成的,并不是你传的,可以认为它是一个全新的。
(当然理论上你还是可以通过某些特殊的方式来存数据,只是会很麻烦,于是你可能就会遵从框架的标准了)

注:某些语言中,会在这里传一个类型,然后再ExcuteCommand的时候,New出来。都是一样的作用。


2、Mediator、Command、Proxy都可以SendNotification,
或者任意地方你都可以调用全局的Facade.SendNotification。效果也是一样的。
也就是发消息是个全局通用函数。
但是绑定消息,只有2个接口实现,RegisterCommand/RegisterMediator。
也就是没有提供Proxy绑定消息的方法。所有Proxy里面无法处理Notification。


3、Mediator/Command/Proxy并没有一一对应的关系。
(1)Command是处理一些通用逻辑,以及界面之间的切换逻辑(或者说系统逻辑)。
(2)Proxy是数据层的某一块数据的封装。有很多界面的数据只在有限的几个界面内使用,所以看起来Proxy还是会对应着几个界面。但是事实上,Proxy和界面没有任何对应关系,只是别的地方暂时没有使用这块数据而已。
(3)每个界面必须有一个Mediator,而除了界面Mediator还有些某一块子界面的通用Mediator,作为子Mediator加载到需要的界面Mediator中

所以说,上面的线是为了便于理解画的,乱线并不乱~~

此处感谢《博客园》慈方阳的文章《1个小时PMVC从入门到精通》,大部分理解和记录都是参照此文章。仅用于自身学习记录。













MVC、PMVC框架和模式的理解的评论 (共 条)

分享到微博请遵守国家法律