一、需求背景和目标
参考链接:
主要考虑解决以下问题:
1. 实体拖拽和替换问题(来自内部和工行)
- 拖拽一个实体就会添加多个逻辑和数据结构,复用性差、负担很重
- 已拖拽出的实体要替换实体或属性,变更起来很粘滞
- 带有复杂关系的多个实体需要在同一个页面完成增删改查,需要很多修改的步骤,原来拖出来的也不好用
2. 常见的典型问题(下面是资产预算中遇到的)
- 页面表格如何更方便的添加字段,比如额外的一些 format 或计算字段?而不需要考虑多个地方的修改
- 表格分页请求和合计总数的数据怎么一起处理问题
- PageOf 和 ScopeOf 非常难用
- 树型列表等组件构建数据的 Builder 机制
3. 不必要的理解成本
- 前后端多层逻辑调用、多层变量等
4. 页面表格和 Excel 的导入导出汇总点问题
- 相同数据的页面展示、Excel 导入导出的汇总处理
二、思考方向和设计方法
1. 从技术经验进行抽象和封装
- 原来视图和数据的关系是直接把前后端框架硬搬上来处理的,少一些良好的抽象封装设计,低代码本身就是要减轻开发者的负担
- 思考各种组件的接口设计
- 学习领域驱动设计的充血模型
2. 从用户视角寻找路径(更重要)
- OutSystems 的视图和数据的关系并不接近用户认知(可能是有包袱问题),然后我们又只抄了70分(没有落地 Record 类型和拖拽替换等功能、Aggregate组件人家可以放页面下)
- 收集复杂场景,亲自搭建并录屏绘制体验地图,分析用户行为和用户意图之间的 gap
- 我理解的声明式设计就是求解
3. 学习竞品
- Mendix 的绑定模型机制很容易上手,值得参考
4. 需要额外注意的点
- 读写分离问题
- 先定义返回类型(模型)还是直接从查询推断
三、复杂场景分析
0. 场景概况
方案 | 自定义视图模型方案 | 数据流方案 | 数据流交互示意图 |
---|---|---|---|
资产预算的增删改查和实时计算 | 有示例 | 有示例 | 有示例 |
资产预算的新员工数据推送 | 不算是 | 有示例 | |
一对多案例 | 有示例 | 有示例 | |
多对多案例 | 有示例 | 有示例 | |
树型列表展示 | 有示例 | 有示例 |
1. 资产预算的增删改查和实时计算(第三题)
o. 需求介绍
请大家了解场景题目第三题。
其他参考链接:
主要特点
- 读写分离
- 前后端都有求和过程,要求的计算方式一致
为了简化描述,实体去除了 createdTime, updatedTime, createdBy, updatedBy 几个字段。
classDiagram
direction RL
class Version {
id: Long
year: Integer
versionCode: Integer
startDate: Date
endDate: Date
notify: Boolean
remark: String
status: VersionStatus
}
class Department {
id: Long
versionId: Long
deptId: String
deptName: String
parentId: String
deptLevel: Integer
fullPathName: String
costCenterId: String
costCenterDefaultId: String
costId: String
bu: String
buName: String
state: Integer
costName: String
}
class Budget {
id: Long
departmentId: Long
fillingPerson: String
approverSecond: String
financePerson: String
approverFirst: String
open: Boolean
status: BudgetStatus
repurchase: Boolean
}
class Asset {
id: Long
versionId: Long
category: String
name: String
price: Decimal
premium: Decimal
premiumN: Decimal
}
class AssetBudget {
id: Long
budgetId: Long
assetId: Long
purchaseReason: String
month1: Integer
month2: Integer
month3: Integer
month4: Integer
month5: Integer
month6: Integer
month7: Integer
month8: Integer
month9: Integer
month10: Integer
month11: Integer
month12: Integer
}
Department "*" --> "1" Version : versionId
Asset "*" --> "1" Version : versionId
Budget "1" --> "1" Department : departmentId
Department "*" --> "1" Department : parentId
AssetBudget "*" --> "1" Budget : budgetId
AssetBudget "*" --> "1" Asset : assetId
a. 雏形:回顾上一版设计
先从各种组件的接口设计、充血模型开始思考:
问题点
- 数据查询需要的其他筛选、排序的条件如何传递?数据查询的切片、筛选、排序的需求来源共有三种:
- 业务自身的需求处理,一般在后端逻辑能直接处理掉
- 前端组件需求。比如表格的分页大小、选择框的输入过滤等(开发者不太关心具体细节但又是必需的)
- 前端上下文需求,比如筛选框的值、页面 url 的一些条件、弹窗上下文等
- 表格中的数据如何传递给弹窗表单内?
优点
- 组件数据源绑定实体或视图模型接近用户认知,有明确从实体或视图模型推导视图结构的意思。
a. 自定义视图模型方案
在组件中补充筛选和排序功能。
问题点
- 拉平的数据再次 submit 的时候比较困难,相当于读和写要 transform 两次,而且用户自己建模型很容易遗忘一些关键字段
- 数据处理和组件太耦合,简单的筛选有引导用户在组件上配的倾向,复杂的场景又需要用户自己先建个一样的模型并实现逻辑,操作过于繁琐
优点
- Flat 的数据结构和表格字段是对齐的,相当于提前设计了模型
b. 数据流方案
c. 小结
- 针对组件的接口抽象很重要
- 老的框架表单没有做抽象,应该把 submit 等行为封装起来。按钮行为可以根据所在位置做推断。
- 弹窗最好支持类似子页面的局部命名空间
- 支持组件下挂变量,减少中间变量
- 利用组件之间的依赖关系推断同步异步的处理问题
2. 资产预算的新员工数据推送
o. 需求介绍
classDiagram
direction RL
class Version {
id: Long
year: Integer
versionCode: Integer
startDate: Date
endDate: Date
notify: Boolean
remark: String
status: VersionStatus
}
class Department {
id: Long
versionId: Long
deptId: String
deptName: String
parentId: String
deptLevel: Integer
fullPathName: String
costCenterId: String
costCenterDefaultId: String
costId: String
bu: String
buName: String
state: Integer
costName: String
}
class Asset {
id: Long
versionId: Long
category: String
name: String
price: Decimal
premium: Decimal
premiumN: Decimal
}
class HeadCount {
departmentId: Long
type: String
position: String
rank: String
region: String
departure: Double
hcEnd: Integer
hc1: Integer
hc2: Integer
hc3: Integer
hc4: Integer
hc5: Integer
hc6: Integer
hc7: Integer
hc8: Integer
hc9: Integer
hc10: Integer
hc11: Integer
hc12: Integer
}
class StandardEquipmentRaw {
deptId: String
employeeType: String
position: String
rank: String
equipment1: String
equipment2: String
equipment3: String
equipment4: String
equipment5: String
equipment6: String
equipment7: String
region: String
}
class StandardEquipment {
deptId: String
employeeType: String
position: String
rank: String
equipment: String
region: String
}
class BuyBackEquipment {
deptId: String
type: String
position: String
rank: String
equipment: String
price: Decimal
}
Department "*" --> "1" Version : versionId
Asset "*" --> "1" Version : versionId
HeadCount "*" --> "1" Department: departmentId
StandardEquipment ..> StandardEquipmentRaw
a. 过程式 + 函数式 API 方案
这里有一个可 Run 的例子。
b. 数据流方案
下面是一个用 LINQ 模拟 SQL 的方案,另一种带控制流和前面的类似。
c. 小结
- 考虑到要像 OutSystems 和 Mendix 引入(一对一, 一对多,多对多)的关系,才能方便推断多实体的场景。但要注意x对x关系(cardinality/multiplicity)是 association 的子集,不能涵盖全部,是一种辅助手段。另外要注意多外键实体属性的配置
- 该例中有一个控制流相对比函数式好理解的 case
3. 商品订单一对多、多对多等场景
o. 需求介绍
用户在创建了下面多个实体之后,想直接拖出来合适的增删改查页:
classDiagram
direction RL
class Category {
id: Long
parentId: Long
name: String
status: Boolean
priority: Integer
}
class Brand {
id: Long
name: String
logo: String
status: Boolean
detail: String
}
class Product {
id: Long
name: String
categoryId: Long
brandId: Long
status: Boolean
detail: String
}
class ProductSpecItem {
id: Long
productId: Long
specs: String
price: Decimal
barcode: String
image: String
amount: Integer
status: Boolean
detail: String
}
class Order {
id: Long
orderNo: Long
status: String
totalCount: Integer
totalAmount: Decimal
createdTime: DateTime
paymentId: Long
customerId: Long
remark: String
}
class OrderProductItem {
id: Long
orderId: Long
productSpecId: Long
count: Integer
remark: String
}
class Payment {
id: Long
orderId: Long
platform: String
amount: String
status: String
}
Category "*" --> "1" Category : parentId
Product "*" --> "1" Category : categoryId
Product "*" --> "1" Brand : brandId
ProductSpecItem "*" --> "1" Product : productId
OrderProductItem "*" --> "1" Order : orderId
OrderProductItem "*" --> "1" ProductSpecItem : productSpecId
Payment "1" --> "1" Order : orderId
目前我们实体属性配置了外键之后,默认拖出来的就类似一对一和多对一的处理。
- 一种交互是:虽然一对多和多对一在数据库关系上是一样的,但为了表示是否带出子级,引入一对多和多对一做区分,然后自动做推断。参考 TypeORM 和 JHipster 等框架;
- 另一种交互是:每次拖拽出实体的时候,让用户勾选连带的实体。
拖拽意图 | 与周边实体的关系 | 表格 | 表单 | 详情 | 选择框 |
---|---|---|---|---|---|
拖拽 Category | Category多对一Category | 树型表格 或某一级的单层表格 | 表单带选择父级选择框 | 单层详情 可能带出父级文本+子级表格 | 树型选择框 |
拖拽 Brand | 无 | 单层表格 | 单层表单 | 单层详情 | 单层选择框 |
拖拽 Product | Product多对一Category Product多对一Brand Product一对多ProductSpecItem | 带出 Category, Brand 可能带出 ProductSpecItem 的细节 | 带出 Category, Brand 选择框 可能带出同步添加修改 ProductSpecItem 的表格 | 带出 Category, Brand 可能带出 ProductSpecItem 的细节 | 单层选择框 |
拖拽 ProductSpecItem | ProductSpecItem多对一Product | 单层表格 | 单层表单 | 单层详情 | 单层选择框 |
拖拽 Order | Order一对多OrderProductItem Order一对一Payment | 带出支付信息 | 带出商品批量表格 | 带出商品和支付信息 | 单层选择框 |
拖拽 OrderProductItem | OrderProductItem多对一Order | - | - | - | - |
拖拽 Payment | Payment一对一Order | 带出订单信息 | - | 带出订单信息 | 单层选择框 |
a. 自定义视图模型方案(一对多)
创建商品时,希望一并创建商品规格。
问题点
- 表格、表单、详情变成了绑定一致。万一表单和详情想显示子项,表格不想显示子项,强制带出来会做多余的计算
优点
- 针对商品+规格项这类场景,增删改查封装在了一块。
b. 数据流方案(一对多)
a. 自定义视图模型方案(多对多)
创建订单时,希望一并创建订单商品项。
问题点
- 这里缺点比较明显,从实际场景,增删改查的几种结构都不太想约束在一种数据结构上。
b. 数据流方案(多对多)
4. 树型列表展示
树型实体是一种特殊的自关联实体,这边单独展示讲:
a. 自定义视图模型方案
b. 数据流方案
五、基本结论
- 自定义视图模型方案不是很可取,主要原因是:
- 60-70%的场景读写的结构是分离的,约束在一起反而增加了负担
- 不能很方便的表达传递页面上下文的查询需求(原来至少还是个函数,用户可以自定义输入参数)
- 数据流方案更接近用户意图
- 数据库查询和内存查询对用户来说只要学习一套操作 API
- 分页和树 LazyLoad 等需求可以在组件中追加,开发者负担小
- 最重要的一点是查询流和表单流很清晰,前端组件是一种显式依赖。另外私有查询可以下放到组件单元上打开,公共的用户再自建逻辑
1. 数据流方案的主要技术依赖项
语言相关
- 引入 LINQ 支持数据库查询和内存查询的设计
- LINQ 分析数据库计算和内存计算,前端计算和后端计算
- 嵌套结构和 Mendix 固定的一对多、多对多模式都不是很灵活,采用 structural typing 解决这类问题:
{ assetBudget, asset, totalCount, totalAmount }
前端组件相关
- 页面中引入区块或套件的概念(类似子页面),能做局部命名空间管理,同时也能访问上层命名空间
- 组件下支持挂变量,减少中间变量
- 针对组件的接口再做一些优化,特别是原来表单没有做抽象,应该把 submit 等行为封装起来。按钮行为可以根据所在位置做推断
- 前端异步依赖的分析能力
- 组件事件的 pipe 机制(可选)
2. 当前问题的解决策略
a. 实体拖拽和替换问题(来自内部和工行)
- 拖拽一个实体就会添加多个逻辑和数据结构,复用性差、负担很重
- 数据结构通过 Structural typing 解决
- 根据前面数据流的分析:多个逻辑并不一定要直接干掉,建议是由页面需求出发产生一个链路的展示引导用户快速定位具体逻辑。全局逻辑部分私有,不要都挂在一颗树上,类似事件逻辑那种形式
- 已拖拽出的实体要替换实体或属性,变更起来很粘滞
- IDE 增加字段和原实体的联动功能
- 带有复杂关系的多个实体需要在同一个页面完成增删改查,需要很多修改的步骤,原来拖出来的也不好用
b. 常见的典型问题(下面是资产预算中遇到的)
- 页面表格如何更方便的添加字段,比如额外的一些 format 或计算字段?而不需要考虑多个地方的修改
- 目前可以用去 PageOf + transform 和批量赋值的函数来解决
- 后面引入了 Structural typing 可以添加个 with 函数更方便
- 表格分页请求和合计总数的数据怎么一起处理问题
- 目前得按两个逻辑去解决
- PageOf 很难用
- 前端组件直接调用两个后端逻辑
- 引入 Structural typing
{ list: List<T>, count: Integer }
- ScopeOf 非常难用
- 表格中隐藏 scope.item
- 框架帮用户析构出来,用户不感知
- 树型列表等组件构建数据的 Builder 机制
- 提供高阶函数构建树的形式
- 提供函数模板
c. 不必要的理解成本
- 前后端多层逻辑调用、多层变量等
- 多层变量用组件下挂变量的方式解决
d. 页面表格和 Excel 的导入导出汇总点问题
- 相同数据的页面展示、Excel 导入导出的汇总处理