1个小时PMVC从0到精通

问题:pmvc是什么?

答:pmvc是一个框架,是一个实现了mvc设计模式的框架。

稍微解释下,框架和设计模式。
设计模式是一种编程思路,框架是实现这个思路的实实在在的代码。所以PMVC是有源码的。

先说下mvc的设计模式。
相信绝大多数人都了解,它是model-view-controller的缩写。
那为什么要有这个设计模式呢?
有说,为了让代码解耦,也有说通用。等等
我还是想说下我的理解。

大部分mvc是用在客户端的开发。
那么我们抛开设计模式,先来想一个最简单的客户端开发。

我要做一个界面,于是我写了一个代码文件,这个代码文件控制界面的所有逻辑。
例如:界面上有按钮点击后弹窗,有按钮点击后请求服务器数据然后显示在界面上。总之就是用户操作界面,程序通过用户操作来改变界面,中间有需要的话可能会向服务器发送请求。
于是我的客户端开发完了,就一个文件,非常合理。

后来客户端的界面不止一个了,那也很简单,我每个界面写一个代码文件与之对应即可。非常合理。

但是又有新问题了,比如登陆的时候,我请求到了玩家的信息,而这个信息要在很多界面使用,我不希望每个界面都要向服务器去请求这个信息。但是如果我这个信息存在登陆界面,后面的界面要访问这个信息的时候,登陆界面都关掉了,不太好拿到。
于是我写了一个单独的文件来存储这个信息。非常合理。

由此绝大部分问题都解决了。我们总结了一下,既然有两种不同作用的文件,那么我就给它们取个名字吧,我把那些全局要使用的信息文件称为model文件,把处理界面逻辑的文件称为view文件。

但是现在还有个大问题,我所有的界面上都会有一个后退按钮,退到上一级的界面。这时候,我似乎要存一个所有打开界面的堆栈,让后退的时候知道怎么退。而且因为每个界面都需要后退,我还想统一去处理后退按钮的逻辑。于是我就写了一个单独的文件,里面存了界面打开的堆栈,并统一实现了后退逻辑。因为我要维护这个堆栈,所以打开界面的时候,我也提供了一个统一的接口,好让我知道有界面打开,以便存到堆栈里面。

我又总结了一下,这个文件似乎叫view和model都不合理。好吧,那我再取一个名字,就叫controller了。

于是MVC就这样自然而然的出来了,而mvc这个设计模式,只是把它总结了一下,说明了一下,但是这个设计思路,并没有任何特别的东西在里面。


解释完MVC,接着说PMVC框架。
根据前面说的,MVC既然是这么自然而然的就写出来了,还要框架干什么呢?

是的,确实没有必要,但是也有一点好处,用了框架以后,细节的地方,可能更加统一,因为按照框架走,就不好随便创新了。

理解pmvc有两大利器。一个是官方提供的图,一个是官方下载的源码。
先说图:

 

途中有3个大圈圈就是我们刚刚说的Model、View、Controller。
值得注意的是,刚刚我们自己写MVC的时候,有很多View,也有很多Model,可能也会有很多Controller。但是PMVC这里都只有一个。其实也没什么特别的。下面我来说下这个图。


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

2、Model边上有很多Proxy,Proxy右边又有很多Obj。
这里Obj其实就是我们写的一个个存全局信息的文件,它们都继承自DataObjects。
你可以认为DataObjects和Obj才是真正的Model,但是在PMVC里面,它将Model的概念稍微放大了一点。Model包括图中的:Model、Proxy、DataObjects、Obj。

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

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

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

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

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


于是,图就说完了,再来说代码吧。
你可以去PMVC官网下载它的源码。它提供了非常多的语言版本。于是我下载了c#版~~
所有版本里面的逻辑都是一模一样的,你可以选择你熟悉的语言下载即可。

上面说图,说了很多,但是毕竟笼统,没有讲到具体的实现。比如说你需要通过Controller去调用Command,那咋调用呢。

代码的阅读也非常简单。
1、先说下,PMVC没有使用任何事件系统,因为它希望跨语言,所以自己做了一个消息系统,来代替事件系统的功能。
如果实际的项目中使用了PMVC,那么你就不需要,也不应该在使用其他事件系统了,当然UI自带的事件系统(点击,拖拽等事件)除外。

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

3、Interface先不看。
从Observer文件夹开始。Observer文件夹里面有3个文件:Notification、Notifier、Observer
(1)Notification就是刚刚说的消息,消息里面就3个字段,Name Body Type。所有的消息都是通过Name来唯一标识的,所以呢,消息的Name都是全局唯一的,如果不唯一就会有问题。其次就是Body,大部分消息都需要Body,因为处理消息的人,很可能依赖于Body里面的信息。而Type只是一个补充,可有可无。
Notification,是没有父类,一般情况也不会有人继承它的。就是一个独立的消息名称,和消息数据的封装。

(2)Nofifier,没有父类,很多人继承它。它只有一个方法,就是SendNotification。里面的其他部分都是多例的处理,前面说了,不用管。例如Facade,如果是单例,就是全局唯一的。
那重点要看看,谁继承了它。
可以认为,所以继承它的类,才能发送Notification,不然没有提供这个方法,你想发也发不了。
继承它的是Command、Mediator、Proxy。那谁不能发消息呢,好像所有人都可以~~
而不可以的Obj和UI里面是没有逻辑处理的。甚至你在框架里面并没有看到他们。

(3)Observer,没有父类,没有子类。如果你去使用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下的所有委托都删除了,只能谁注册的谁删除。


Observer看完后,大部分pmvc源码都看完了~~
接下来我们看一下所有的Facade提供的接口,也就是pmvc所有提供的接口了。

RegisterCommand
RegisterProxy
RegisterMediator
SendNotification

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

RegisterCommand、RegisterMediator 相关的刚刚已经在Observer里面看到了。这里再接着说一下。

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

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

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

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


看起来pmvc的源码就看完了,这里再说点需要注意的细节。

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中

猜你喜欢

转载自www.cnblogs.com/woasishen/p/11138700.html