本文说明为什么在材料试验数据接口中引入策略模式,什么是策略模式,以及相对于旧实现(testData.js 单一接口)的对比与优势,方便后续在新增试验类型或重构接口时复用同一思路。
背景:从单一接口到按试验类型拆分
历史实现中,所有材料试验数据都通过同一套接口 testData.js 访问,例如:
列表:
/snerdi/material_test_data详情:
/snerdi/material_test_data/{id}拟合图表:
/snerdi/material_test_data/fitting/{type}
这带来几个问题:
不同试验类型混在同一张“逻辑表”里:蠕变、拉伸等试验字段差异较大,前端和后端都需要靠额外字段区分,容易产生耦合和特判。
调用方需要自己区分类型和接口:调用代码中很难用统一的方式处理不同试验类型,经常出现
if (testType === 'CREEP') { ... } else if (...) { ... }之类的分支。扩展困难:新增一种试验类型时,需要在多处插入分支逻辑,难以做到“只在一个地方注册”。
现在需要每种材料都是对应单独一套接口(客户要求的),后端代码也是如图每种材料对应一个controller:

前端部分代码也根据后端改动为每种试验类型有独立的 API 模块,例如:
蠕变:
creepTestData.js(/api/material/creepTestData.js)拉伸:
tensileTestData.js(/api/material/tensileTestData.js)
在此基础上,通过 testDataRegistry.js 把这些具体实现统一“挂接”到一个注册表里,对外暴露统一的访问方式。
什么是策略模式(Strategy Pattern)
通俗理解:
策略模式就是把一类可替换的行为(策略)抽象出来,用统一的接口约束,然后在运行时按条件选择具体的策略来执行。
关键点:
一组算法 / 行为:它们完成“同一类任务”,但实现细节不同(例如:不同试验类型的 CRUD + 拟合接口)。
统一接口定义:调用方只依赖统一的接口(如
list / get / add / update / del / getPlotDataByType),而不关心具体实现。可配置 / 可扩展:新增策略只需要“注册”进去,而不必修改大量调用方代码。
在 JavaScript / Vue 项目里,最常见的写法就是:
用一个对象(或
Map)做注册表:key是策略标识,value是具体实现对象。提供一个小函数,如
getStrategy(type),根据标识返回对应策略。
本项目中的策略模式实现
统一接口形态
在 testDataRegistry.js 中,对每种试验类型定义了统一的接口形态:
list:列表查询get:详情查询add:新增update:更新del:删除getPlotDataByType:绘图 / 拟合数据
例如(简化示意):
const creep = {
list,
get,
add,
update,
del,
getPlotDataByType,
}
const tensile = {
list,
get,
add,
update,
del,
getPlotDataByType,
}
调用方只要拿到某个“策略对象”,就可以用同一套方法名调用,无须关心内部走的是 /snerdi/creep 还是 /snerdi/tensile。
策略注册表:试验类型 id → 策略对象
testDataApiRegistry 把试验类型 id(与 MaterialTestTypeId 枚举一致)映射到对应的策略对象:
export const testDataApiRegistry = {
[MaterialTestTypeId.CREEP]: creep,
[MaterialTestTypeId.TENSILE]: tensile,
}
这样,前端其它模块只需要知道“试验类型 id”,就能通过统一入口拿到对应的 API 策略。
统一访问入口:getTestDataApi
getTestDataApi(testTypeId) 是策略模式对外暴露的“选择器”:
export function getTestDataApi(testTypeId) {
return testDataApiRegistry[testTypeId]
}
调用方示例(伪代码):
import { getTestDataApi } from '@/api/material/testDataRegistry'
const api = getTestDataApi(testTypeId)
// 列表
const { rows } = await api.list(queryParams)
// 详情
const detail = await api.get(id)
// 绘图
const figData = await api.getPlotDataByType(formulaType, filterParams)
这里调用方完全不需要知道“蠕变用 creepTestData.js、拉伸用 tensileTestData.js”,都交给注册表和策略对象来处理。
总结
旧的
testData.js实现把所有试验数据混在一个接口里,扩展和维护成本高。现在通过按试验类型拆分 API 模块 + 策略注册表
testDataRegistry.js的方式,引入了策略模式:对外暴露统一的接口形态;
根据
MaterialTestTypeId动态选择具体策略;新增试验类型时,只需在注册表中配置一次即可。
这种设计提升了扩展性、可读性、解耦程度和测试友好性,适合在类似“多类型同一业务抽象”的场景中复用。
附录:相关代码
1. 试验类型枚举与类型列表(src/utils/constant.js)
import imgWuLiXingNeng from '@/assets/images/material/WuLiXingNeng.png'
import imgWeiGuanJieGou from '@/assets/images/material/WeiGuanJieGou.png'
import imgLaShen from '@/assets/images/material/LaShen.png'
import imgChongJi from '@/assets/images/material/ChongJi.png'
import imgLuoChui from '@/assets/images/material/LuoChui.png'
import imgYingDu from '@/assets/images/material/YingDu.png'
import imgMoSun from '@/assets/images/material/MoSun.png'
import imgLaoHuaGuanLi from '@/assets/images/material/LaoHuaGuanLi.png'
import imgRuBian from '@/assets/images/material/RuBian.png'
import imgYingLiSongChi from '@/assets/images/material/YingLiSongChi.png'
import imgPiLao from '@/assets/images/material/PiLao.png'
import imgLieWenKuoZhan from '@/assets/images/material/LieWenKuoZhan.png'
import imgRuBianPiLao from '@/assets/images/material/RuBian-PiLao.png'
import imgFuShi from '@/assets/images/material/FuShi.png'
import imgFuZhao from '@/assets/images/material/FuZhao.png'
import imgWeiZhuYaSuo from '@/assets/images/material/WeiZhuYaSuo.png'
/** 材料试验类型 id 枚举 */
export const MaterialTestTypeId = Object.freeze({
PHYSICAL: 'PHYSICAL',
MICROSTRUCTURE: 'MICROSTRUCTURE',
TENSILE: 'TENSILE',
IMPACT: 'IMPACT',
DROP_WEIGHT: 'DROP_WEIGHT',
HARDNESS: 'HARDNESS',
WEAR: 'WEAR',
AGEING: 'AGEING',
CREEP: 'CREEP',
STRESS: 'STRESS',
FATIGUE: 'FATIGUE',
CRACK: 'CRACK',
CREEP_FATIGUE: 'CREEP_FATIGUE',
CORROSION: 'CORROSION',
IRRADIATION: 'IRRADIATION',
COMPRESSION: 'COMPRESSION',
})
export const materialTestTypes = [
{ id: MaterialTestTypeId.PHYSICAL, name: '物理性能', subtitle: 'Physical Property', icon: imgWuLiXingNeng, type: 'physical' },
{ id: MaterialTestTypeId.MICROSTRUCTURE, name: '微观结构', subtitle: 'Microstructure', icon: imgWeiGuanJieGou, type: 'microstructure' },
{ id: MaterialTestTypeId.TENSILE, name: '拉伸', subtitle: 'Tensile Test', icon: imgLaShen, type: 'stretch' },
{ id: MaterialTestTypeId.IMPACT, name: '冲击', subtitle: 'Impact Test', icon: imgChongJi, type: 'impact' },
{ id: MaterialTestTypeId.DROP_WEIGHT, name: '落锤', subtitle: 'Drop Weight Test', icon: imgLuoChui, type: 'drop-weight' },
{ id: MaterialTestTypeId.HARDNESS, name: '硬度', subtitle: 'Hardness Test', icon: imgYingDu, type: 'hardness' },
{ id: MaterialTestTypeId.WEAR, name: '磨损', subtitle: 'Wear And Tear', icon: imgMoSun, type: 'wear' },
{ id: MaterialTestTypeId.AGEING, name: '老化管理', subtitle: 'Ageing Management', icon: imgLaoHuaGuanLi, type: 'ageing' },
{ id: MaterialTestTypeId.CREEP, name: '蠕变', subtitle: 'Creep Test', icon: imgRuBian, type: 'creep' },
{ id: MaterialTestTypeId.STRESS, name: '应力松弛', subtitle: 'Stress Relaxation', icon: imgYingLiSongChi, type: 'stress' },
{ id: MaterialTestTypeId.FATIGUE, name: '疲劳', subtitle: 'Fatigue Test', icon: imgPiLao, type: 'fatigue' },
{ id: MaterialTestTypeId.CRACK, name: '裂纹扩展', subtitle: 'Crack Propagation', icon: imgLieWenKuoZhan, type: 'crack' },
{ id: MaterialTestTypeId.CREEP_FATIGUE, name: '蠕变-疲劳', subtitle: 'Creep-Fatigue Test', icon: imgRuBianPiLao, type: 'creep-fatigue' },
{ id: MaterialTestTypeId.CORROSION, name: '腐蚀', subtitle: 'Corrosion Test', icon: imgFuShi, type: 'corrosion' },
{ id: MaterialTestTypeId.IRRADIATION, name: '辐照', subtitle: 'Irradiation Test', icon: imgFuZhao, type: 'irradiation' },
{ id: MaterialTestTypeId.COMPRESSION, name: '微柱压缩', subtitle: 'Micropillar Compression', icon: imgWeiZhuYaSuo, type: 'compression' },
]
2. 策略注册表:src/api/material/testDataRegistry.js
/**
* 材料试验数据 API 策略注册表
* 按试验类型 id(与 constant.js MaterialTestTypeId 一致)映射到对应接口,后续新增类型只需在此配置即可
*/
import { MaterialTestTypeId } from '@/utils/constant'
import * as creepApi from './creepTestData'
import * as tensileApi from './tensileTestData'
/** 统一接口形态:list / get / add / update / del / getPlotDataByType */
const creep = {
list: creepApi.listCreepMaterialTestData,
get: creepApi.getCreepMaterialTestData,
add: creepApi.addCreepMaterialTestData,
update: creepApi.updateCreepMaterialTestData,
del: creepApi.delCreepMaterialTestData,
getPlotDataByType: creepApi.getCreepPlotDataByType,
}
const tensile = {
list: tensileApi.listTensileMaterialTestData,
get: tensileApi.getTensileMaterialTestData,
add: tensileApi.addTensileMaterialTestData,
update: tensileApi.updateTensileMaterialTestData,
del: tensileApi.delTensileMaterialTestData,
getPlotDataByType: tensileApi.getTensilePlotDataByType,
}
/** 试验类型 id -> API 策略(与 constant.js MaterialTestTypeId 一致) */
export const testDataApiRegistry = {
[MaterialTestTypeId.CREEP]: creep,
[MaterialTestTypeId.TENSILE]: tensile,
}
/**
* 根据试验类型获取对应 API 策略
*/
export function getTestDataApi(testTypeId) {
return testDataApiRegistry[testTypeId]
}
3. 蠕变试验接口:src/api/material/creepTestData.js
import request from '@/utils/request'
// 查询蠕变材料测试数据列表
export function listCreepMaterialTestData(query, showUnverified = false) {
if (!showUnverified) {
query.status = 'VERIFIED'
} else {
delete query.status
}
return request({
url: '/snerdi/creep',
method: 'get',
params: query,
})
}
// 查询蠕变材料测试数据详细
export function getCreepMaterialTestData(id) {
return request({
url: '/snerdi/creep/' + id,
method: 'get',
})
}
// 新增蠕变材料测试数据
export function addCreepMaterialTestData(data) {
return request({
url: '/snerdi/creep',
method: 'post',
data: data,
})
}
// 修改蠕变材料测试数据
export function updateCreepMaterialTestData(data) {
return request({
url: '/snerdi/creep/' + data.id,
method: 'put',
data: data,
})
}
// 删除蠕变材料测试数据
export function delCreepMaterialTestData(id) {
return request({
url: '/snerdi/creep',
method: 'delete',
data: id,
})
}
// 绘制图表
export function getCreepPlotDataByType(type, data) {
return request({
url: '/snerdi/creep/fitting/' + type,
method: 'post',
data: data,
})
}
4. 拉伸试验接口:src/api/material/tensileTestData.js
import request from '@/utils/request'
// 查询拉伸材料测试数据列表
export function listTensileMaterialTestData(query, showUnverified = false) {
if (!showUnverified) {
query.status = 'VERIFIED'
} else {
delete query.status
}
return request({
url: '/snerdi/tensile',
method: 'get',
params: query,
})
}
// 查询拉伸材料测试数据详细
export function getTensileMaterialTestData(id) {
return request({
url: '/snerdi/tensile/' + id,
method: 'get',
})
}
// 新增拉伸材料测试数据
export function addTensileMaterialTestData(data) {
return request({
url: '/snerdi/tensile',
method: 'post',
data: data,
})
}
// 修改拉伸材料测试数据
export function updateTensileMaterialTestData(data) {
return request({
url: '/snerdi/tensile/' + data.id,
method: 'put',
data: data,
})
}
// 删除拉伸材料测试数据
export function delTensileMaterialTestData(id) {
return request({
url: '/snerdi/tensile',
method: 'delete',
data: id,
})
}
// 绘制图表
export function getTensilePlotDataByType(type, data) {
return request({
url: '/snerdi/tensile/fitting/' + type,
method: 'post',
data: data,
})
}
5. 旧版统一接口:src/api/material/testData.js
import request from '@/utils/request'
// 查询材料测试数据列表
export function listMaterialTestData(query, showUnverified = false) {
if (!showUnverified) {
query.status = 'VERIFIED'
} else {
delete query.status
}
return request({
url: '/snerdi/material_test_data',
method: 'get',
params: query,
})
}
// 查询材料测试数据详细
export function getMaterialTestData(id) {
return request({
url: '/snerdi/material_test_data/' + id,
method: 'get',
})
}
// 新增材料测试数据
export function addMaterialTestData(data) {
return request({
url: '/snerdi/material_test_data',
method: 'post',
data: data,
})
}
// 修改材料测试数据
export function updateMaterialTestData(data) {
return request({
url: '/snerdi/material_test_data/' + data.id,
method: 'put',
data: data,
})
}
// 删除材料测试数据
export function delMaterialTestData(id) {
return request({
url: '/snerdi/material_test_data',
method: 'delete',
data: id,
})
}
// 绘制图表
export function getPlotDataByType(type, data) {
return request({
url: '/snerdi/material_test_data/fitting/' + type,
method: 'post',
data: data,
})
}