单机下桌面程序应用架构 - 图1

今天我们换一个角度, 站在应用架构的角度, 来聊聊如何设计一个桌面应用程序

从MVC说起

MVC

关于桌面程序, 我想你听得最多的莫过于MVC这个架构范式, MVC全称 模型(Model) - 视图(View) - 控制器(Controller)

单机下桌面程序应用架构 - 图2

怎么理解MVC呢? 一种理解是, Model 是 Input , View 是 Output . Controller 是 Process , 认为MVC与计算机的Input-Process-Output 这个基础模型暗合

更准确的解释是: Model 是数据, View 是数据显示结果, 同时也接收用户交互动作, 也就是事件. 从这个意义来说, 说Model是Input并不严谨, View 接受用户交互, 也是Input的 一部分.

Controller 负责Process(处理) , 它接收 Model + 由View转发的事件 作为Input, 处理的结果(Output) 仍然是Model, 它更新了 Model的数据

View之所以被理解为Output , 是因为 Model的数据更新后, 会发送DataChanged(数据更新) 事件, View 会在监听并受到DataChanged事件后, 更新View. 所以把View 理解为Output 也并不算错, 它从数据角度看 其实是Model的镜像

MVP

对MVC模式做些细微的调整, 就会产生一些变种. 比如, Model的数据更新发出 DataChanged事件后, 由Controller 负责监听并 Update View, 这样就变成了 MVP架构. MVP 全称是 模型(Model)- 视图(View) - 表现(Presenter)

单机下桌面程序应用架构 - 图3如何选择架构范式呢?

要判断我们写的程序架构是否优良, 那么我们心中就要有架构优劣的评判标准. 比较知名且重要的一些基本原则如下.

  • 最低耦合原则: 不同子系统(或模块) 之间有最少的交互频率, 最简洁且自然的接口
  • 单一职责原则: 不要让一个子系统(或模块0 干多件事情, 也不要让它不干事情.

如果在我们心中以遵循架构法则为导向, 回来看MVC , 又会有不同理解

理解Model层

我们先看Model. 如果你真正理解Model层的价值, 那么可以认为你的架构水平已经达到了较高层次的水准, 因为Model层太重要了.

上面说的Model层是数据, 这其实还不是太准确. 更准确来说, Model层是承载业务逻辑的DOM. 即文档对象模型(Document Object Mdoel), 直白理解, DOM是面向对象意义上的数据. 它不只是有数据结构, 也有访问接口

为了便于理解, 假设我们基于数据库来实现Model层, 这种情况下会有<font style="color:#F5222D;">两种常见的架构误区</font>

  • 一种是直接让Controller层直接操作数据库, 也就是拿数据库的读写接口作为Model层的接口
  • 一种是 用所谓的 <font style="color:#F5222D;">ORM</font>技术来实现Model层, 让Controller直接操作ORM

为什么我们说这两种做法都有问题呢? 原因在于对 Modle层的价值不明. Model层的使用接口最重要的是要自然体现业务的需求.

只有这样, Model层的边界才是稳定的, 与你基于的技术无关. 是用MySql, 还是用了 NoSql? 还是裸奔Sql, 还是基于ORM都没有关系, 未来还可以改

另外, 从界面编程角度看, Model层越厚越好. 为什么这么说? 因为这是和操作系统的界面程序架构最为无关的部分, 是最容易测试的部分, 也同时是跨平台最容易的部分.

我们把逻辑更多向Model层倾斜, 那么Controller层就简洁很多, 这堆跨平台开发将极其有利

这样来看, 直接让Controller 层直接操作数据库, 或者基于ORM操作数据库, 都是让Model层啥事不干, 这样非常浪费, 同样也违背了单一职责原则

我们需要强调. 单一职责不只是要求不要让一个子系统(模块)干多件事情, 同时也要求不要让他不干事情

如果我们用一句话来描述Model层的职责, 那么应该是 负责业务需求的内核逻辑, 我们以前经常叫他 DataCore

那么Model层为何要发出 DataChanged 事件

这是从Model层的独立性考虑. Model层作为架构的最底层, 它不需要知道其他层的存在, 也不需要知道是 MVC 还是MVP, 或者是其他的架构范式

有了DataChanged事件, 上层就能够感知到 Model层的变化, 从而做出自己的反应.

显然, DataChanged事件就是 Model层面对需求变化点的对策. 大部分Model层的接口会自然体现业务需求, 这是核心价值点, 是稳定的.

但是业务的用户交互可能会变化多端, 与PC还是手机, 与屏幕尺寸, 甚至可能与地区人文都有关系, 是多变的

用事件回调来解决需求的变化点, 这一点上 CPU干过, 操作系统也干过, 今天你做业务架构也可以这么干

理解View层

View层首要的责任, 是负责界面呈现. 界面呈现只有两个选择, 要么直接调用GDI接口自己画, 要么创建子View让别人画.

View层另一个责任是被自然带来的, 那就是: 它是响应用户交互事件的入口, 这是操作系统的界面编程框架决定的. 比较理想的情况下, View应该把自己所有的事件委托出去, 不要自己干.

但是View的设计细节中, 也有很多问题需要考虑

  1. View 层不一定会负责生成所有用户看到的view. 有的View是Controller在做某个逻辑的过程中临时生成的, 那么这样的View就应该是Contorller的一部分, 而不应该是MVC里面的View层的一部分
  2. View层可能需要非常友好的委托(Delegate)机制的支持. 例如支持一组界面元素的交互事件共同做委托(delegate).
  3. 负责界面呈现, 意味着View层和Model层关系非常紧密, 紧密到需要知道数据结构的细节, 这可能会导致Modle层要为View层提供一些专享的只读访问接口. (这合乎情理, 只是要确保这些访问接口不要扩散使用)
  4. 负责界面呈现, 看似只是根据数据绘制界面, 似乎很简单, 但实则不简单. 原因在于: 为了效率, 我们往往需要做局部更新的优化. 如果我们收到onPaint消息, 永远是不管三七二十一, 直接重新绘制, 那么事情就很好办. 但是在大部分情况下, 只要业务稍微复杂一点, 这样的做法都会遇到性能挑战

在局部更新这个优化足够复杂时, 我们往往不得不在Model和View之间, 再额外引入一层 ViewModel层来做这个事情.

ViewModel层 顾名思义, 是为View的界面呈现而设计的Model层, 它的数据组织更接近View的表达, 和View自身的数据呈一一对应关系(Bidi-data-binding).

单机下桌面程序应用架构 - 图4

一个极端但有很典型的例子是 Word. 它是数据流式的文档, 但是界面显示人们用得最多的却是页面视图, 内容是分页显示的.

这种情况下就需要有一个 ViewModel层是按分页显示的结构来组织数据. 其中负责维持Model与ViewModel层的数据一致性的模块, 我们叫排版引擎

从理解上来讲, 我个人会倾向于认为 ViewModel是View层的一部分, 只不过是View层太复杂了进行了再次拆分的结果, 也就是说 我并不倾向于存在所谓的 Model-View-ViewModel这样的模式

理解Controller层

controller层是负责用户交互的, 可以有很多个controller, 分别负责不同的用户交互需求

这和Model层, View层不太一样. 我们会倾向于认为Model层是一个整体. 虽然这一个层会有很多类, 但是他们共同构成了一个完整的逻辑: DOM. 而View层也是如此,, 它是DOM的界面呈现, 是DOM的镜像, 同样是一个整体.

但是负责用户交互的Controller层, 是可以被正交分解的, 而且应该作正交分解, 彼此完全没有耦合关系.

一个Controller模块, 可能包含一些属于自己的辅助View, 也会接受View层委托的一些事件, 由事件驱动自己状态, 并最终通过调用Model层的使用接口来完成一项业务

Controller模块的辅助View可能是持续可见的, 比如菜单和工具条; 也可能是一些临时性的, 比如Office软件中旋转图形的控制点

对于后者, 如果存在ViewModel层的话, 也可能会被归到 ViewModel + View 来解决, 因为ViewModel层可以有Selection这样的东西来表示View里面被选中的对象

Controller层最应该思考的问题是代码的内聚性. 哪些代码是相关的, 是应该放在一起的, 需要一一理清. 这就是上面说的正交分解的含义.

如果我们做得恰到, Controller之间应该是完全无关的. 而且要干掉某一个交互特别容易, 都不需要删除该Controller本身相关的代码, 只需要把创建该Controller的一行代码注释掉就可以了

从分层角度, 我们会倾向认为 Model层在最底层 View层在中间 它持有Model层的DOM指针; Controller层在最上层 它知道Midel和View层, 它通过DOM接口操作Model层, 但它并不操作View去改变数据, 而只是监听自己感兴趣的事件.

如果View层提供了抽象得当的事件绑定接口, 你会发现 , 其实`Controller层大部分的逻辑都与操作系统提供的界面编程框架无关(除了少量辅助View) , 是跨平台的.

串联模块

谁负责把MVC各个模块串联起来? 当然是应用程序了, 在应用开始的时候, 它就把Model层, View层, 我们感兴趣的若干Controller模块都创建好, 建立了彼此的关联, 一切就如我们期望的那样工作起来了

兼顾API与交互

MVC是很好的模型来支持用户交互. 但这不是桌面程序面临的全部. 另一个很重要的需求是提供应用程序的二次开发接口(API)

提供API的应用程序, 意味着它身处一个应用生态之中, 可以与其他应用程序完美写作.

通过哪一层提供API接口? 我个人会倾向于ViewModel层. Model层也很容易提供API, 但是它可能会缺少一些重要的东西, 比如 Selection