本文探讨在 Vue 开发中做分层设计,类似 Angular 拆分出 Model、View、Controller、Service、Component 等层,通过 import
在 .vue
文件中组合成业务应用,使 .vue
变轻变薄。
问题
我们在实践中面临的主要问题有两个,一是需求变化,二是技术整合。
这两种因素直接造成源码物理体积增加和冗余。又长、又多、又有重复,又要拥抱变化,这对开发工作最直接的影响就是难维护。
“多”层面无法解决,任何事物渐进迭代之后,“多”是必然。
“长”和“重复”是由于累积造成,累积原因有两点,一是迫于快速交付成果“高效率”产出,二是缺少相关抽象归纳意识及环境因素驱动。我们都有受这种“累积”的影响,同时,我们或多或少也在制造这些“累积”。
“长”可以切段,“重复”可以提炼。怎么切,怎么提,有许多模式提出,如 MVP、MVC、MVVM、BDD、DDD,名字和理念虽异,但背后思想是一样的:分离、分层。
本文将从采用 Vue 技术体系过程中面临和制造“长”、“重复”的问题展开 (亦适用于其它技术体系)。
Smart UI Anti-Pattern 和 误解
Vue 官网上教程示例和 Cookbook 示例, 是有着 The Smart UI Anti-Pattern 味道的,造成部分开发者停留其上,认为那就是 Vue Style。这是对 Vue 的误解,造成这个误解的一部分原因是 Vue 团队在应用开发实践上对开发者系统化的指导少 (相对于 Angular 和 React 来说),另一部分原因是部分开发者缺少分层架构的思想。
本文将基于以下各体系知识来探讨 Vue 中如何实现代码分层。
模式、意识
数据意识:
在 MVVM 模式框架中,我们要始终在脑子里挂着 Model 的弦。不能老想着“我有 ××× 这个 DOM,我要让它做 ××× 变化”,而应该是先思考我们有或需要什么样的 Model 数据,然后设计我们的交互数据和交互逻辑,最后才去实现视图,并用 ViewModel 去粘合它们。
——《AngularJS 深度剖析与最佳实践》
领域、场景 (上下文)、角色意识:
一个储蓄账户,可以“增/减账户中的钱” (数据特征,是什么),可以“存/取款” (数据意图,做什么),“存/取款”意味着事务语义、用户交互、异常恢复、处理业务规则和错误情况,这些超出了单个数据模型的处理能力。“增/减账户中的钱”仅是账户具备的一个性质,只关系到一个对象的状态,而“存/取款”是属于整个系统的行为,它影响着整个系统的状态,从系统架构、软件工程、变化率的角度看,这是两种差别极大的性质,而面向对象却将这些归到了一起。
将稳定的部分和将会变化的部分区分开,如果说对象反映了代码中的稳定部分,那么在代码中还应该有一种对象以外的机制去表达需求变化。
DCI 架构:
- 数据 (Data),存在领域对象里,领域对象源自领域类;
- 场景 (Context),根据需要将活动对象带到场景中的位置;
- 交互 (Interaction),从角色的角度描绘最终用户对算法的思维 (关于对象做什么的行为的集合)。
将变化率高的用例部分和稳定的领域部分分离开,拥抱变化。
——《DCI 架构:面向对象编程的新构想》
领域意识:
在面向对象的程序中,常常会在业务对象中直接写入用户界面、数据库访问等支持代码。而一些业务逻辑则会被嵌入到用户界面组件和数据库脚本中。这么做是为了以最简单的方式在短期内完成开发工作。
要想创建出能够处理复杂任务的程序,需要做到关注点分离——使设计中的每个部分都得到单独的关注。在分离的同时,也需要维持系统内部复杂的交互关系。
行为意识:
场景的片段——给定、事件和结果——粒度足够细,可以直接用代码表示。
然后将所有这些连接在一起并执行它们。它创建了一个“世界”,它只是在某个地方存储您的对象,并将其依次传递给每个给定对象,以便它们可以用已知状态填充世界。
组合意识 (不是混合,is composition, not mixin):
组件描述了任何可组合的行为,包含渲染、生命周期和 state。
组件之间的组合是 React 的重要特征。
—— React
服务化意识:
服务是一个广义的概念,它包括应用所需的任何值、函数或特性。狭义的服务是一个明确定义了用途的类。
Angular 把组件和服务区分开,以提高模块性和复用性。通过把组件中和视图有关的功能与其它类型的处理分离开,你可以让组件类更加精简、高效。
理想情况下,组件的工作只管用户体验,而不用顾及其它。 它应该提供用于数据绑定的属性和方法,以便作为视图(由模板渲染)和应用逻辑(通常包含一些模型的概念)的中介者。
—— Angular
这些模式、意识,旨在分层。
我们需要先了解、接受上述各体系中的模式、意识,而不是 “不要跟我说什么……,老夫搬砖都是一把梭” 排斥、抗拒和抵制。
接下来,我们思考框架/库和我们业务各自处理的是什么。
框架和库、业务关注点
首先,我们能明确框架/库的功能是将应用拆解成特定目标的对象,如 Module、Model、View、ViewModel、Controller、Service、Component、Directive 等不同特征的对象,然后再组织起来使其相互协作;这种设计遵循了“职责驱动设计”和“契约式设计”原则,使开发者容易接受、理解和应用,像 VueX、Redux、Mobx、Context 这类设计,是为了生成一个专门用于管理数据状态的环境,关注点分离,这个环境是独立的、隔离的,我们需要从另一个维度去思考和实践,如是否需要采用这类解决方案,以及如何使用。
其次,我们能明确要处理的不同层次的对象/用例都是由属性、特性和行为组成的数据,是存在边界的和可预测的 (即使需求发生变化),这些属性、特性和行为,最终是能反应到视觉和体验上的;这部分数据,和框架和库是分离的,不存在依赖关系。
最后,我们要做的是,独立实现用例,用框架或库组织这些实现。
例如在 Vue 中,Vue:
提供了 Component 机制,用于提供场景;
提供了 Template/Render 机制,用于视图;
提供了 Data/Props/Computed/Reactivity 机制,用于将数据带到视图中;
提供了 Method 机制,用于将行为与数据、视图关联;
提供了 Watch 机制,用于将数据与数据、行为关联;
提供了 Mixins 机制,用于解决横切关注点问题;
提供了 Lifecycle 机制,用于场景进入和退出;
提供了 Directive 机制,用于框架和终端交互;
我们在实践时候,尽可能的将框架/库中的特定机制当作一个“连接器”,而不是“实现”、“执行”,具体的细节,尽可能的分层到对应机制的层级结构中。
接下来,我们思考如何将分层模式、意识与一个框架/库结合起来。
分层
Data (Data/Props/Computed/Reactivity)
Vue 中 Data 主要作用不是生成数据,而是将数据响应化反馈到视图中,因此在 Data 层,建议抽出两个层面:
1、一个用于控制视图特性 —— 视觉 (尺寸、可视),
2、一个用于填充视图属性 —— 内容 (表格、表单);
特性数据可以直接定义或声明;
属性数据建议按领域、实体创建模型;囿于 JavaScript 缺少 Interface 机制,数据模型建议用函数返回对象形式 (工厂模式) 替代;
数据模型建议存放在服务中。
行为
行为的形式有 UI Events、XHR/Fetch、Timers、Browser Location、Service Worker、事务等,可将这些行为细节封装到服务中 (非 UI 逻辑和代码),供 Vue 取用。建议:
1、Vue Template 中 `v-on` 事件包装器不做声明,其它 `v-` 指令不做运算;
2、Method、Watch、Computed 等机制尽可能只充当“联络站”角色,相关的事件处理函数抽出放在服务中。
DOM
建议将 UI 相关操作通过 DOM 规范和技术封装在服务中,然后采用 Vue Directive 机制关联,以实现对 DOM 的操作。
VueX
如果有采用集中式的数据状态管理模式,亦然,仅将 VueX 这类数据状态库作为“组织”用,而不是“实现”。
其它框架/库
一个系统会采用许多第三方提供的服务,如 Moment.js 这类特定领域的服务,建议不要直接使用在我们系统中,而是依赖它封装一个业务系统中这类领域的服务,相当于做一个“适配”,即,我们系统只声明需要什么样的接口,而不关心这个接口具体的实现,这样为未来技术变迁提供可能。
最后
最终,这样分层之后,中上层薄,下层厚,各层解耦,代码结构得到优化,测试、维护和交接都较方便,同时也为复用、移植打下了基础 (复用不是目的是结果)。比如:
数据或数据处理出现异常,直接定位在服务层,步骤不对直接查验组织层。同理,协同对接时也能清晰的将各领域结构化的表达出来;另外,查阅代码时翻页量少了,各结构间逻辑也能较好的梳理,不乱。
当然,这里也会面临一些问题,如:层应该分几个层,粒度应该分多细。
我们先看一下 2010 年 AngularJS 框架的设计,数据相关部分 Providers 中有 Constant、Value、Factory、Service 等各种类型的分层,划分的粒度较细,后来 2016 年在 Angular 设计中,认为以前那样的设计概念过多,造成有关联性的领域被隔离,于是将所有数据、数据处理相关的操作都统一到了 “Service” 这一个概念中,更没有 config、constant、util 这类传统规划,我们提供的所有都是服务,供视图及用户交互消费;我是赞同这样处理的,在实践中发现,按领域及其各作用域进行拆分后,并不会造成服务的冗余和臃肿;也就是说,Vue、VueX 这类框架和库,仅起组织、调度作用,其它业务处理,不依赖这类框架和库而是独立在另外一个维度,这样做的好处是:
- 开发者协作时,能明确知道,框架/库和业务各自所属、划分各自边界,能定位、专注某一层的处理,好开发,好阅读,好维护;
- 保持程序结构清晰、简短,不用一大坨杂糅,不用上下、来回寻找;
- 技术栈迭代或更换,只需在对应的层用新的实现替换原有的实现。
最后,对象之间具有复杂的依赖性,管理这些对象充满了挑战,分层中抽取分离的过程不是一步能到位的,而是需要随着渐进明细的实际情况滚动式规划和进行,总之,分层,能做多少就是多少,不能不做。
扩展阅读:
- The Model-View-Controller (MVC) Pattern | Microsoft Docs
- The Model-View-Presenter (MVP) Pattern | Microsoft Docs
- The Model-View-ViewModel (MVVM) Pattern | Microsoft Docs
- DCI 架构:面向对象编程的新构想 (The DCI Architecture: A New Vision of Object-Oriented Programming) / 原文
- DCI:James O. Coplien 和 Trygve Reenskau 提出的新架构方法 / 原文
- 行为驱动开发与领域驱动设计相结合 (Behaviour-Driven Development Combined with Domain-Driven Design) / 原文
- 《领域驱动设计:软件核心复杂性应对之道》P48/4.3 模式:THE SMART UI “ANTI-PATTERN”
- 《JavaScript 高级程序设计(第4版)》P220/8.2.2 工厂模式