iOS架构设计:最初的MVC

767 阅读16分钟

这是对1979年5月12日Trygve Reenskaug提出MVC时的文章的翻译。

原始MVC报告

—— 来自奥斯陆大学信息学系Trygve Reenskaug

1978/79年,我在施乐帕洛阿尔托研究实验室(PARC)做访问科学家时,完成了第一个实现并编写了原始的MVC报告。MVC是作为一个明显的解决方案创建的,它解决了从多个角度给用户控制其信息的一般问题。MVC已经创造了惊人的话题。有些文本甚至使用歪曲的变体,相反的目的是使计算机控制用户。

MVC被认为是用户控制庞大而复杂的数据集问题的一般解决方案。最难的部分是为不同的建筑构件打上好名字。Model-View-Editor是第一套。它们在1979年5月12日的第一个注释中描述:THING-MODEL-VIEW- EDITOR —— 一个系统规划的例子。

经过长时间的讨论,特别是与阿黛尔·戈德伯格的讨论之后,我终于用我在1979年12月10日的第二份说明中描述的术语“模型-视图-控制器”(Model-View-Controller):模型-视图-控制器(Model-VIEWS-CONTROLLERS)。

在我离开施乐PARC之后,Jim Althoff和其他人为Small.-80类库实现了一个MVC版本;我没有参与这项工作。

奥斯陆,2007年2月12日
Trygve Reenskaug

THING-MODEL-VIEW-EDITOR

一个规划系统的例子

日期:1979年5月12日
由:Trygve Reenskaug
写给:LRG
归档于:[IVY]SMALL> TERMIN0LOGY2.DOC

本文的目的是通过一组连贯的例子来探究thing-model-view-editor结构。这些例子都是从我的计划系统中提取出来的,并说明了以上四个概念。所有示例都已实现,尽管与提出的架构有所差异。这个结构与DynaBook在([Ivy]DynaBook.doc)中提出的world-Model-view-Tool相对应。

THING

定义

用户感兴趣的东西。它可以是具体的,比如房子或集成电路。也可以是抽象的,就像一个新的想法或关于一篇论文的观点。它可以是一个整体,像一台计算机,也可以是一个部件,比如一个电路元件。

例子:一个大型项目

假设Thing是一个大项目。它可以是一座大桥、一座发电站或一个离岸石油生产平台的设计和建造。
这样的项目代表复杂的任务,具有非常多的相互依赖的细节。负责项目的人员必须检查所有细节及其依赖,以便对可能出现的各种实际或建议的情况的后果进行推演。
大量不同的抽象被用来帮助控制大型项目。每个抽象都突出了整个项目的特定方面,并且单独使用或与其他抽象一起用于控制这些方面。抽象的例子是对材料要求、成本估算、各种预算和会计理论的思考。
活动图(或者说流程图)或PERT图对抽象的过程有很大的帮助。它们能让我们理清楚应该做什么、谁应该做、什么时候要做。
在抽象网络中,项目中必须执行的每个任务都被映射到一个称为活动的简单元素中。基本上,一个活动包含了它的持续时间和它的前置。持续时间指的是执行相应任务所需的时间,而前置是在启动当前活动之前必须完成的活动。
这种简单的抽象通常以某种方式扩展。基于活动的前置,很容易找到每个活动的后继者。网络最开始活动都是没有前置的活动,而结束活动是没有后继者的活动。其他信息可以被附加在每个活动上。例如活动的资源需求,以及与活动相关的成本和现金流。

MODEL

定义

Model将(上文中的)抽象以数据的形式在计算系统中表述出来。

探讨

如上所述,通常存在许多不同的抽象相同事物的方法,因此对于给定“Thing”的设置几个共存模型通常很有用。或者,人们可以认为该项目有一个大Model,它被细分成若干个子Model。

模型在计算机中表示为数据的集合,以及处理这些数据所需的方法。
理想情况下,所有的模型应该是完全一致的。这种理想在实践中是无法达到的,这将需要极强的统一性和令人窒息的死板。因此,人们应该接受一些不一致之处,其目标是整个模型集应当是项目的合理准确的表示,而不必花费太多精力在琐事上。
在我们的示例中,给定的网络模型在Smalltalk系统中表示为类NetworkModel的实例。NetworkModel中的每个活动都被表示为这个类的实例。
为类NetworkModel定义的字段之一是activities——活动名称(UniqueString)和活动实例的字典。Activity类定义的字段中有三个是network(网络)、predecessors(前置)和successors(后继者)。network包含指向NetworkModel相关实例的指针,predecessors和successors是Activity实例的指针向量。

ps:这里可以用类似双向链表的概念来理解

我们的模型的整体结构(Smalltalk项目的一个网络模型的表示)如下:

在这个通用框架中,我们现在可以添加关于我们的项目的信息,这些信息可以连接到所有network及它的activities。对于network,我们可以添加字段,例如plannedStartplannedFinish。对于Activity类的定义,我们可以添加字段durationearlyStartlateStartresourccRcquirements。我们还可以为lateStart添加字段lateFinish,但决定前请考虑是否真的需要。(无论是durationearlyStart还是lateStart)。
注意,每个活动的network Model及其子Model不包含关于如何在屏幕上显示信息的信息,这能让项目的抽象Model保持干净。

VIEW

定义

对于任何给定的Model,附有一个或多个View,每个View能够在屏幕上展示Model的一个或多个图形表示。View也能够对与该View合理关联的Model发挥同样的作用(展示数据)。

探讨

如果能够遵循 Form-Path- Image(这里我理解为“来源-转换-展示”) 原则,那么所有View的实现都将被简化。

EXAMPLE 1: network列表

一个View属于一个super-network,即网络集合。因此,它略微超出目前讨论的Model的范围,但包含在完整性中。上图显示了network列表的外观。

network列表是类NetworkList的一个实例,它是ListView的子类。

ListView有一些字段,如frameitemsListselectionItem。它能够在其frame内的屏幕上显示它的itemsList,并对请求滚动的消息作出反应。它能够响应在它frame范围内的点击事件,以及选中某个item的事件。

因此,ListView与当前系统中的ListPane有点相似,但它不是Window的子类,无法调度。因此,它必须依赖于一个Editor(Controller的变种)来告诉它它的frame在哪里,并安排滚动。选择的一个可能的流程是Editor响应输入,并将(点击的)位置传递给ListView。然后要求ListView(或其他View)来选中对应的item。将用户界面和数据处理分离能够为Editor提供很大的灵活性。
类NetworkList基于ListView构建。这个列表是一个network列表,并且它必须知道如何持有这样的列表。此外,它必须能够执行可能与其关联的View相关的命令。我们只考虑两个这样的命令:返回选中的network名称,设置(编辑)选中的network。

EXAMPLE 2: network中的activity列表

network中的activity列表是类ActivityList的实例,它是ListView的子类(参见上面对这个一般类的描述)。
ActivityList的实例至少必须知道它属于哪个network,以及如何获得该network中所有activity的列表。除了ListView提供的一般选择机制之外,没有特殊命令。

EXAMPLE 3: activity属性展示

这是一个activity的所有属性的文本呈现。它是类ActivityText的一个实例,它是TextView的子类。
TextView类中有一些字段,它可以记住它的frame段落文本。它能够在框内通过wrap-around显示文本,并响应滚动文本的消息。它还能够将给定的坐标链接到文本内的位置,并在给定位置之间选择文本。再进一步,可以要求它响应基于选择的各种操作(如替换、剪切、粘贴等)的消息。
因此,类TextView与当前的ParagraphEditor(段落编辑器)类似,但是每个直接用户界面都被转化为一个或多个消息,以便它可以通过Editor和各种不同的Editor与其他View一起工作。
ActivityText类必须知道如何为给定的activity获取文本。这个操作可以有效地由选择消息触发,通常从Editor触发。它还必须能够对用户通过Editor发起的关于activity的消息作出反应。一组这样的消息用于对View中展示的数据进行修改,这由父类TextView处理。可能对ActivityText本身最重要的命令是,要求View检查其当前内容并将数据作为activity的属性的更新传递给network Model。
如果这个View继承于表格View(listView?)而不是基于当前运行的文本,则此View可以大大改进。

EXAMPLE 4: Network Diagram

上面的图表是类DiagramView的一个实例。这是View中的数据不能完全从其Model推导出的唯一示例,并且它必须具有包含所有节点的形状和位置的字段,因为该信息不是Model本身的一部分。(存在用于在图表中自动定位节点的程序,但我们假设至少需要一些手动编辑)。箭头的位置可以从活动之间的依赖关系推导出来,该信息可以在需要显示时从NetworkModel获取。然而,这样的过程将非常慢,并且出于效率的原因,我们假设DiagramView保留该信息的副本。
和所有其他View一样,DiagramView将需要提供选择activity所需的两个操作,以及滚动所需的操作(这次是二维的)。此外,它还需要对View本身进行一些操作,它们与View中节点的定位有关。其他操作必须与NetworkModel有关,例如,修改activity的依赖性,并将activity从一个network传递给另一个network。

EXAMPLE 5: Network Diagram的一个变化

此View提供了前面示例的替代方案。节点较小,因此可以在屏幕上呈现更大的网络部分。小的节点使得在节点中显示activity名称变得不切实际,因此用户必须通过其他方式获得该信息。
这个网络图是在网格背景下呈现的。这是当用户在布置图表时使用的,并且点定义了activity节点的允许位置。
这个View可以实现为DiagramView的子类,或者两者都可以是一些常见View的子类。然而,在本实现中,两个View可以交替地由同一个对象生成:DiagramView类包含字段displayType,其值为#large#small,显示方式由这个字段控制,并通过各自的方式显示在图表中。

EXAMPLE 6: 甘特图

这个View包含垂直轴的activity和水平轴的时间。在这个特定的View中,每个activity对应一个时间轴。通过改变阴影,可以较容易控制每个activity的进度,而不会扰乱整个项目进度。
上图是GanttView类的实例,它是ChartView的子类。ChartView包含了图表的背景:带有图例的轴、网格等。它不包含要放入图表的信息,这里指的是是水平条。然而,它通过提供从外部使用的任何坐标系到图的框架坐标的转换方法,帮助其子类呈现该信息。
GanttView类处理用户的一些消息,最常见的是通过Editor给出的消息。例如viewNetwork:,这个消息提供了当前NetworkModel的名称。它还能够返回选中位置所属的activity,以及选中属于给定activity的进度条。它还能够传递与它相关的network和activitie上的操作,典型操作与修改当前进度表有关。应该提供操作来规划network(backload:frontload:)、修改单个activity的进度(plannedStart:planncdFinish:)以及让这种修改的结果正确地执行。
NetworkModel必须能够处理一些操作,比如从GanttPane取出所有activitie的列表。network包含一个整体的进度表,每个activity有单独的进度。

EXAMPLE 7: 资源图

该图显示了activitie的资源需求之和的时间函数。(一般来说,每个资源类型都会有一个这样的图表。)
该图是类ResourceView的一个实例,它是DiagramView的另一个子类。关于甘特图的特性也适用于这一点,但选择的概念需要详细阐述。这个视图需要可以返回所有目标点的x坐标上所有需要资源的activity,还需要响应***选择***的消息:activity可以高亮其所需要的资源需求。对应的,最好将两者结合起来,并在所有NetworkModel View中设置多个选择的开关。

EDITOR(CONTROLLER)

定义

Editor是View和Model之间的接口。它向用户提供合适的命令系统,例如菜单的形式,可以根据当前上下文动态地改变。它为View提供必要的协调和命令消息。

探讨

用户通常可以同时在屏幕上查看的多个View以便更好地执行任务。用户将希望通过点击、菜单选择或其他方式来操作这些View。像选择这样的命令通常同时适用于多个View。Editor的目的是在屏幕上建立和定位一组给定的View,协调它们,并为用户提供合适的命令接口。

EXAMPLE 8: UserView workspace

这是一个非常通用的Editor,它为运行中的Smal1talk系统的任何部分提供用户界面,这些部分可以从全局变量中访问。因此,它可以用作规划系统的视图的接口,但它将大部分工作留给用户。

由于此Editor始终可用,因此它用于启动更专用的Editor,并执行不经常使用的命令,以保证这种Editor。后者的一个示例是对完整数据库的注销的命令。

EXAMPLE 9: 一个虚构的规划Editor

这个Editor非常类似于之前的Editor,但是它是在network中创建的。因此,该network及其所有activity的消息可以通过方法直接键入和执行。因此,通过该Editor可以获得network和activity的完整内容。
这个Editor没有在这里实现,而是作为下一个示例中展示的较大Editor的一部分。

EXAMPLE 10: Editor的嵌套

这个Editor是较大的Editor(见下一个示例)的一部分,并被设计为在其上下文中工作。(与示例9实际上是同一个Editor,滚动到它的顶部)。
Editor是DemoEditor类的一个实例,它是TextEditor的子类。因此,一般文本可以在该Editor中键入、存储和编辑。此外,由于DemoEditor包含了network及其所有activitie,因此这些对象支持的任何消息都可以通过方法输入和执行。
由于此Editor是更大的Editor(参见下一个示例)的一部分,所以我们可以执行更一般的操作,比如activity持续时间,其中activity被解释为当前选择的activity。
这个子Editor的一个变体可以与ListView子类的一个实例通讯,通过禁止对显示的文本进行任何编辑,限制用户执行预定义命令的可能性。

EXAMPLE 11: 一个示例Editor

上图显示的Editor演示了一个给定的Model可以通过不同的View来显示。
这个Editor是DemoEditor类的实例,它是PanedWindow的子类。它的每个组成部分都是一个与特定View通信的子Editor,并向父Editor发送和接收命令。

来自:Trygve Reenskaug
日期:1979年12月10日

MODELS - VIEWS - CONTROLLERS

MODELS

Model代表认知,Model可以是单个对象(单一的),也可以是对象的某种结构。
一方面,Model及其部分之间应该存在一对一的对应关系,另一方面,Model与现实世界应该存在一对一的对应关系。因此,Model的节点应该代表问题的可识别部分。
Model的节点都应该处于相同的问题级别,将面向问题的节点(例如日程)与实现细节(例如段落)对应起来是令人困惑的,并且被认为是不好的形式。

VIEWS

View是其Model的(视觉)表示。它通常会突出Model的某些属性,并隐藏其他属性。因此,它充当显示过滤器。
View被附加到它的Model(或Model部分),并从Model中获取显示所需的数据。它还可以通过发送适当的消息来更新Model。所有这些获取方法和消息都必须在Model的定义中,因此View必须知道它所表示的Model的属性的定义。(这很有可能,例如,它可以获取Model的标识符或者获取Text的实例,它可能不会假设Model是Text的子类。)

CONTROLLERS

Controller是用户和系统之间的链接。它为用户提供输入,安排相关View在屏幕上的适当位置显示。它通过向用户展示菜单或提供命令和数据的其他手段来提供用户输出的手段。Controller接收这样的用户输出,将其转换为适当的消息,并将这些消息传递给一个或多个View。
Controller不应该对View进行补充,例如不应该通过绘制节点之间的箭头来连接节点的View(PS:前面例子中各个节点之间的连接线不应该由Controller绘制)。
相反,View不应该知道用户输入,例如鼠标操作和击键。应该在Controller中编写一个方法,该方法向View发送消息,View准确地再现用户命令的任何顺序。

EDITORS

Controller连接到它的所有View,它们(View)被称为Controller的部件。一些View提供了一个特殊的Controller:一个Editor,它允许用户修改View提供的信息。这样的Editor可以被拼接成Controller和它的View之间的路径,并且将作为Controller的扩展。一旦编辑过程完成,Editor将从路径中删除,并被丢弃。
注意,Editor通过连接的View的实例与用户通信,因此Editor与View紧密关联。一个Controller会通过请求编辑来获得它的View——没有其他合适的来源。