维基百科不是技术手册
![enter image description here](https://istack.dev59.com/SHL5a.webp)
维基百科的MVC图像存在许多混淆。问题在于,在UML类图中,A -> B
表示A是活动的并调用B(因此A依赖于B)。但是维基社区不是纯技术性的,并且画出带有未解释和随意箭头含义的图像。拥有一个完整的箭头圆圈看起来很好,很合理,对吗?
不对。最有趣和最明显的荒谬之处是View --sees--> User
:这是什么意思?大兄弟小心翼翼地用网络摄像头跟踪用户?这个含义表明箭头方向被颠倒了,但是我们将没有那个漂亮的生命之环...
但是认真地说:图像最令人震惊的设计缺陷是Model --updates--> View
关系。从技术上讲,它总是需要View --reads--> Model
。如果Model是活动的,那么数据对象和操作将依赖于系统的其他部分,因此它将无法重用。
另一个荒谬之处是User --uses--> Controller
。控制器是不可见的,用户只使用View,其他部分对他们来说是黑盒子。 控制器基本上是事件实现系统。事件的源可以是用户的输入或模型的数据更改,但是它们使用接口,而是控制器实现它们。 (这被称为反向控制,由于它有些人混淆了箭头的方向。)这些操作命令Model和View进行更新,因此箭头指向Controller。没有东西控制Controller。这就是为什么它被称为Controller:所有控制都聚合到其中。主动视图是一个例外:如果需要填充其选择框,则它可以在没有Controller的祝福下读取Model。但是有时View对Model接口的依赖是不希望的,因此并非所有View都设计为主动。
因此,正确的图像是:
--- Controller ----
| | !!! arrows mean dependency, not data flow !!!
V V
View ------------> Model
(if active)
事件调度器
这是维基百科引起混淆的根本原因:MVC架构不能独立运行,需要事件调度器来处理事件并调用控制器:
- 在Web应用程序中,它是HTTP服务器
- 在托管应用程序中,它是IDE自动生成的代码(通常对编码人员隐藏)
- 在本地应用程序中,它位于主循环中
- 在较低级别的应用程序中,它由系统环境提供
User <--> View --> Event Dispatcher
∧ |
| | !!! arrows mean data flow, not dependency !!!
Model <--> Controller <-----
现在我们有交互循环。请注意箭头的含义:从依赖性的角度来看,事件调度器当然是独立于控制器的,而控制器需要一些事件调度器。
要实现MVC,我们需要理解以下活动模型方案中描述的依赖注入技术。
活动模型场景
有时候,模型也可以是事件的来源,例如,如果某个账户信用额度降至某个水平,它可以发出信号通知视图显示警告。但是模型应该独立于系统的其他部分,因此它不能调用视图。观察者设计模式是实现它的方法,请参见下面的简化示例:
模型
模型使用接口让视图钩住其“帐户过低”或“帐户过高”的事件。
interface AccountObserver {
public void accountLow(int value);
public void accountHigh(int value);
}
class Model {
protected int account;
protected AccountObserver observer;
public void setAccountObserver(AccountObserver o) {
observer = o;
}
public void updateAccount(int change) {
account+= change;
if(account<minValue) observer.accountLow(account);
if(account>maxValue) observer.accountHigh(account);
}
...
}
视图
有些人建议聚合观察者而不是实现它。继承更简单,如果模型以一种方式定义了所有观察者,使得它们的方法具有唯一的名称,我们可以继承。
class View : AccountObserver {
public void accountLow(int value) {
warning("Account too low! It has only "+value+" credits!");
}
public void accountHigh(int value) {
warning("Account too high! It has above "+value+" credits!");
}
...
}
控制器
在架构的控制器部分,我们将用户界面(视图)和其他事件源与模型(可能由多个数据源组成)结合在一起。在最简单的情况下:
class Controller {
protected Model model;
protected View view;
public Controller(Model model, View view) {
this.model = model; this.view = view;
model.setAccountObserver(view);
}
void onUpdateAccount(int requestedValue) {
if(requestedValue<0) ...
model.updateAccount(requestedValue);
}
}
注意
model.setAccountObserver(view)
- 模型和视图
对象(作为控制器的属性)是耦合的,但是模型和视图
类是独立的。这种依赖注入模式是理解模型 - 视图关系的关键。
现在回答您的问题
- 哪些关系是正确的?全部和没有。 全部,因为差异来自于箭头不同的含义。 没有,因为它们的箭头含义没有明确描述(或者像维基百科的图片一样错误,请参见Olexander Papchenko的评论)。
- 业务逻辑应该由控制器或模型处理?两者兼顾。纯数据操作肯定属于模型,但模型不能决定一切,例如用户何时以及如何登录。这就属于控制器,并且在下面的代码中显示。
- 如果控制器将一个对象传递给视图,这个对象属于模型吗?是的,请参见下面的代码。
- 视图如何直接从模型检索数据?它直接引用模型还是与来自控制器的模型交互?我认为这两种情况都是可能的:如果视图需要显示一些静态数据,例如国家列表,如果它拥有模型实例(也许通过某个接口),并直接调用其getter方法,则不会出现问题(如果视图需要更改数据,则创建事件并让控制器处理)。这就是上面图片中的箭头
View --> Model
。如果数据是动态的,例如getContacts(int userId)
,则需要控制器验证请求:
class Controller {
protected Model model;
protected View view;
protected User user;
public Controller(Model model, View view) {
this.model = model; this.view = view;
model.setAccountObserver(view);
initBusinessLogic();
}
protected function initBusinessLogic() {
user = view.loginModalDialog();
if(user.isLoggedIn()) view.setContactList(model.getContacts(user.id));
}
}
注意,模型和视图通常是作为具有不同职责的多个类实现的(视图用于对话框、主页面等,模型用于用户操作、订单等)。控制器每个应用程序只有一个;如果我们为每个视图有专门的控制器,那么它被称为Presenter,并且该架构为MVP。