本文说明为什么在材料试验数据接口中引入策略模式,什么是策略模式,以及相对于旧实现(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,
  })
}