尽管 MVC 模式很简单,但很多时候都没有被理解。首先,您需要了解所有这些分离的组件中应该包含哪些内容以及它们如何协同工作。
视图:任何 UI 元素。一个好的可重用视图元素应该在任何地方重用。因此视图不知道它的上下文,也不知道他将与之交互的特定控制器。视图知道的是它自己的状态和一个(或多个)通用侦听器,一旦发生某些操作,它将调用。视图元素知道它的状态,广播变化但不应该自己改变它的状态。例子:一个按钮会广播一个“点击”的动作,你会通过一个控制器通过aButton.setLabel("click me!");这样的方法来设置按钮的名字
模型:模型能够处理数据集的状态,它通常实现一些功能,以便将其状态保存到文件中或从文件中加载。模型是一个盒子,它实际上不应该改变它的状态,除非有人(控制器)要求它。同样,一个好的模型不知道视图甚至控制器。它就是这样。一个非常简单的模型例如是一个字符串。您可以从文件中加载字符串,将其保存到文件中,将其更改为大写、子字符串等。字符串确实有状态,不知道它的上下文,只是等待有人使用它。如果模型知道你被愚弄的控制器,那么除非你实现相同的控制器,否则模型将无法重用(即使使用接口也是一种不好的做法)。
控制器:控制器实际上是您的程序,这是做出决策的部分。在哪儿 ?好吧……您的main function 已经是控制器了。同样,所有决定都由控制器做出。当有人单击按钮时,会调用控制器中的方法(而不是视图方法),当您想要更新标签时,您可以从控制器中执行此操作。此外,所有线程都应处理到控制器中。您的模型正在加载一个大文件?在控制器中创建线程,让模型加载图片,完成后通知主线程(题外话,搜索事件循环)
多个窗口,我们来了!
视图/控制器如何相互交互?
视图的职责只是向控制器提供事件(文本字段更改、按钮被单击等)并能够以我们想要的方式显示。例如 window.setTitle("myWindow");我们可能会想把应该放在控制器中的东西放在视图中,反之亦然。
例如,如果您正在构建一个警报面板,您将有 2 个按钮(确定、取消)和一个用于文本的标签。一旦单击“取消”的“确定”,视图是否知道该怎么做?当然不是......但是一旦点击面板将不得不自行消失。相反,如果您确实创建了一个用于输入密码的面板,则视图将无法保持验证过程。这将是控制器同意或不同意提供的密码的工作。
好吧,我猜你已经理解了所有的 MVC,现在是时候讨论让所有这些组件一起交流的正确方法了:
视图应该如何通知控制器?实际上,这取决于您使用的语言。首先想到以下几点:
- 避免视图知道控制器。
- 视图不应该从控制器获取信息,也不应该调用它的方法。它将如何告诉控制器发生了什么事?通过观察者(或听众)
小括号:一个视图可以调用另一个视图的方法,如果它只是保持视觉正确的问题。例如,调整窗口大小将确保该窗口的所有组件(子视图)仍然正确显示,但可能不会通知控制器窗口改变了大小。
MVC之间的通信是关键!那么,它是如何工作的呢?
谁来创建和显示视图?控制器!
谁应该注册查看事件?控制器也是……创建后
如何?视图的工作是在事件发生时广播事件。通常通过 java 中的ActionListener、objective-c 中的观察者、lambda 函数 c++、javascript 中的函数回调。列出的控制器在 java 中实现正确的“回调”函数actionPerformed、选择器,或者如果您向视图提供了函数回调,甚至直接实现回调函数。
因此,当从控制器创建的视图触发事件时,控制器会收到通知并且必须处理此事件。如果控制器想要将某些内容更改为视图,这很容易,因为控制器始终拥有并知道视图。
对于每个窗口一个控制器:听起来不错,在创建登录控制器对象实例时,您不必只有一个控制器(既不是一个模型也不是视图……),可以创建并显示一个登录窗口也是。
每个服务一个模型实例。这里出现了单例设计模式,您可能只需要一个“身份验证模型”,因为您在同一个应用程序中登录或注销。因此,与其根据登录状态为每个窗口创建多个登录模型实例,不如只创建一个。您可以通过登录模型对象的静态实例来做到这一点。这称为单例。
现在如何确保将我们更改为模型的内容复制到所有控制器?简单的案例是一个控制器和一个模型。控制器正在更改模型并等待模型被更改然后使用更改(例如将其显示到视图中)。如果模型通过多个控制器共享会发生什么?例如,模型是一个登录服务,你有两种状态:登录和注销。
2 个控制器正在使用这个模型,每个控制器都有自己的视图(窗口),一个只是显示密码字段,第二个是一个带有图像的窗口,只有在用户登录时才会显示。有这种情况的解决方案不是一种,而是几种。一种是使用聆听/观察模式。几个控制器可以监听模型的变化。一旦其中一个正在更改它,所有控制器都将收到更改通知,因此将更新视图。另一种方法是保留一个通知中心(这是另一种模式),让控制器或模型广播发生的事情,然后所有对此事件感兴趣的控制器都会收到更改通知并处理该事件。对于模型可以随时更改(当然是通过隐藏在某处的控制器)的情况,最后一种解决方案特别有趣。通知中心正在使用事件循环,并且大部分时间都在尝试通知到主事件循环(应该是你的 UI 控制器的线程)
因此,对于我们的示例,如果登录过程处于脱机状态,我将使用一个简单的侦听器。 2 个控制器正在监听模型更改。一个控制器输入密码后会调用方法的模型login.loginWithPassword(String some password),如果密码正确,模型会广播一个事件“loginStateChanged”。然后所有的监听器(包括我们的 2 个控制器)都会收到这个事件并告诉视图更新我们想要的任何东西(例如显示只能在登录时显示的图像)。
在上面的例子中,我们的控制器询问模型并且模型直接触发事件,这在控制器的线程中,这很好,因为没有并发访问的风险(相同的线程=没问题)但是:
如果登录是远程登录,例如在线服务认证,我们将敲响服务器的门并等待它的回答。由于此过程可能需要几秒钟,因此控制器会一直等待服务器的回答。为了避免这种情况,我们应该将请求发送到另一个线程,并用一条消息(等待服务器)修改视图,直到我们从服务器得到答案。一旦我们得到答案(或者如果没有答案,则在超时之后),我们之前创建的线程将得到模型的答案是真还是假。一个非常糟糕的主意是直接用结果更新视图。问题是得到答案的线程,没有运行到主控制器的线程中,因此如果我们更新视图,我们确实面临并发访问(如果视图对象恰好在同一时间被 2 个线程更改遇到崩溃)。确保我们不会弄乱主线程的一种方法是简单地将模型的答案通过将在主事件循环中分派的通知发送到主线程。在这种情况下,尤其是我将使用通知中心而不是模型中的简单广播。有趣的是,一些框架在主事件循环中提供广播。使用块或 lambda 函数有更好的解决方案,但它超出了这个问题的范围。
总结一下:
- 视图和模型都不应该知道控制器
- 只有控制器知道它们两个
- 注意他们相互交谈的方式,如果同一模型有多个控制器,请使用侦听器或通知。
- 过程始终如下:
- 控制器创建视图并监听它
- 视图告诉控制器一个动作发生了
- 控制器处理操作可能会要求模型更改某些内容
- 模型给出结果(通过监听器或回调返回函数)
- 控制器处理结果
- 控制器使用结果更新视图。