抽象类(策略模式 + 模板方法模式)代码实现
社保公积金上传:社保、公积金、雇员、自有员工:一共四个excel模板,都需要上传的文件服务器,解析方法不一样
1、抽象父类定义
java
@Slf4j
public abstract class PaySipfUploadService {
// ✅ 共享字段
@Resource
private FileAttachmentApi fileAttachmentApi;
// ✅ 抽象方法(子类实现)
abstract SipfFeeItemEnum getSipfFeeItem();
abstract PersTypeEnum getPersType();
abstract String getAttachType();
// ✅ 模板方法(固定流程)
public List<PaySipfDtlTmpDO> uploadHandle(MultipartFile file, PaySipfDtlTmpUploadVO tmpUploadResult) {
// 步骤1:调用子类解析
List<PaySipfDtlTmpDO> sipfDtlTmps = upload(file, tmpUploadResult);
// 步骤2:判断是否成功
if (YesOrNo.YES.getCode().equals(tmpUploadResult.getFlag())) {
// 步骤3:上传文件到 XSKY(共用逻辑)
AttachmentRespDTO upload = fileAttachmentApi.upload(
AttachmentReqDTO.builder()
.file(file)
.attachType(getAttachType()) // 子类提供
.isSave(YesOrNo.YES.getCode())
.isDelete(YesOrNo.YES.getCode())
.build()
);
if (null == upload || null == upload.getFileId()) {
log.error("上传XSKY失败");
throw exception(PayErrorCodeConstants.PAY_UPLOAD_ERROR);
}
tmpUploadResult.setFileId(upload.getFileId());
}
return sipfDtlTmps;
}
// ✅ 抽象方法(子类实现具体解析)
abstract List<PaySipfDtlTmpDO> upload(MultipartFile file, PaySipfDtlTmpUploadVO tmpUploadResult);
}
2、策略注册与选择
java
@Service
public class PaySipfApplServiceImpl {
@Resource
private List<PaySipfUploadService> sipfUploadServices; // Spring 自动注入所有子类
private Map<String, PaySipfUploadService> sipfUploadServiceMap;
@PostConstruct
private void init() {
// 手动构建策略映射表
sipfUploadServiceMap = sipfUploadServices.stream()
.collect(Collectors.toMap(
s -> s.getSipfFeeItem().getKey() + s.getPersType().getKey(), // 业务组合 Key
Function.identity()
));
}
public PaySipfDtlTmpUploadVO uploadDetail(PaySipfDtlTmpUploadVO reqVO) {
String key = reqVO.getSipfType() + reqVO.getPersType(); // "SOCIALEMP"
// 根据业务类型选择策略
PaySipfUploadService uploadService = sipfUploadServiceMap.get(key);
Assert.notNull(uploadService, "服务不能为空!");
// 执行模板方法
return uploadService.uploadHandle(reqVO.getFile(), tmpUploadResult);
}
}
3、具体策略实现
java
@Service
public class SocialEmpUploadService extends PaySipfUploadService {
@Override
SipfFeeItemEnum getSipfFeeItem() {
return SipfFeeItemEnum.SOCIAL;
}
@Override
PersTypeEnum getPersType() {
return PersTypeEnum.EMP;
}
@Override
String getAttachType() {
return "SOCIAL_EMP";
}
@Override
List<PaySipfDtlTmpDO> upload(MultipartFile file, PaySipfDtlTmpUploadVO result) {
// 社保雇员 Excel 的特殊解析逻辑
// ...
}
}
接口(纯策略模式)代码实现
服务费计算有9种公式类型需要解析:如固定值、阶梯、成本比例等
1、接口定义
java
public interface CalcFormulaService {
/**
* 计算公式
* @param reqVO 需要计算的数据
* @return 结果
*/
FormulaDTO calcFormula(NeedCalculateData reqVO);
}
2、策略注册与选择
java
@Service
public class SerIncomeDetailServiceImpl {
// ✅ Spring 自动注入为 Map,Key 就是 @Service 的值
@Resource
private Map<String, CalcFormulaService> calcserviceMap;
public void calculate(NeedCalculateData needCalculateData) {
String formulaType = formulaInfo.getFormulaCalc().getCode(); // "A1", "A2", "BX" 等
// ✅ 直接根据公式类型获取策略并执行
FormulaDTO formulaDTO = calcserviceMap.get(formulaType).calcFormula(needCalculateData);
// ...
}
}
3、具体策略实现
java
// 策略1:固定值计算
@Slf4j
@Service("A1") // ← Bean 名称就是策略 Key
public class FormulaFixedValueServiceImpl implements CalcFormulaService {
@Override
@Transactional(rollbackFor = Exception.class)
public FormulaDTO calcFormula(NeedCalculateData reqVO) {
log.info("A1 - 固定值计算");
List<FormulaDetailDTO> formulaDetailList = reqVO.getFormulaDetail();
FormulaDetailDTO formulaDetail = formulaDetailList.get(0);
List<FormulaDetailFactorDTO> factors = formulaDetail.getFactors();
FormulaDetailFactorDTO factor = factors.get(0);
BigDecimal fixValue = factor.getFactorValue();
FormulaDTO formulaDTO = new FormulaDTO();
formulaDTO.setFixValue(fixValue);
return formulaDTO;
}
}
// 策略2:阶梯计算
@Slf4j
@Service("A2")
public class FormulaStepServiceImpl implements CalcFormulaService {
@Resource
private EmployeeApi employeeApi;
@Override
@Transactional(rollbackFor = Exception.class)
public FormulaDTO calcFormula(NeedCalculateData reqVO) {
log.info("A2 - 阶梯计算");
// 查询在岗雇员数量
EmpDutyNumberReqDTO reqDTO = new EmpDutyNumberReqDTO();
reqDTO.setProjectId(reqVO.getProjectId());
reqDTO.setQueryDate(reqVO.getBusinessDay());
Long empDutyNumber = employeeApi.getEmpDutyNumber(reqDTO);
// 根据人数匹配阶梯区间
BigDecimal num = new BigDecimal(empDutyNumber != null ? empDutyNumber : 0);
FormulaDetailDTO formulaDetail = reqVO.getFormulaDetail().stream()
.filter(dto -> num.compareTo(dto.getMinValue()) >= 0
&& (dto.getMaxValue() == null || num.compareTo(dto.getMaxValue()) <= 0))
.findFirst()
.orElse(null);
Assert.notNull(formulaDetail, "公式配置不正确!");
BigDecimal fixValue = formulaDetail.getFactors().get(0).getFactorValue();
FormulaDTO formulaDTO = new FormulaDTO();
formulaDTO.setFixValue(fixValue);
return formulaDTO;
}
}
// 策略3:成本比例计算
@Slf4j
@Service("BX")
public class FormulaCostRatioServiceImpl implements CalcFormulaService {
@Resource
private Map<String, CalcAmountService> calcAmountServiceMap; // ← 嵌套策略模式
@Override
@Transactional(rollbackFor = Exception.class)
public FormulaDTO calcFormula(NeedCalculateData reqVO) {
log.info("BX - 成本比例计算");
List<FormulaDetailDTO> formulaDetailList = reqVO.getFormulaDetail();
List<BigDecimal> costBaseValueList = new ArrayList<>();
BigDecimal costRatio = null;
BigDecimal btmLineValue = null;
FormulaDetailDTO formulaDetail = formulaDetailList.get(0);
List<FormulaDetailFactorDTO> factors = formulaDetail.getFactors();
for (FormulaDetailFactorDTO factor : factors) {
String factorType = factor.getFactorType();
if (FactorEnum.COST.getCode().equals(factorType)) {
// 根据不同的成本基数类型计算金额(再次使用策略模式)
List<CostBasesDTO> costBasesDTOList = factor.getCostBases();
Set<String> typeList = new HashSet<>();
// ... 分组逻辑
for (String type : typeList) {
BigDecimal costBaseValue = calcAmountServiceMap.get(type).calcAmount(reqVO);
if (costBaseValue != null) {
costBaseValueList.add(costBaseValue);
}
}
} else if (FactorEnum.RATIO.getCode().equals(factorType)) {
costRatio = factor.getFactorValue();
} else if (FactorEnum.BTM_LINE.getCode().equals(factorType)) {
btmLineValue = factor.getFactorValue();
}
}
FormulaDTO formulaDTO = new FormulaDTO();
formulaDTO.setCostBaseValue(costBaseValueList.toArray(new BigDecimal[0]));
formulaDTO.setCostRatio(costRatio);
formulaDTO.setBtmLineValue(btmLineValue);
return formulaDTO;
}
}
详细对比表
| 对比维度 | 方式一:抽象类(策略+模板方法) | 方式二:接口(纯策略) |
|---|---|---|
| 核心类型 | abstract class | interface |
| 设计模式 | 策略模式 + 模板方法模式 | 纯策略模式 |
| 依赖注入 | List< PaySipfUploadService> | Map<String, CalcFormulaService> |
| Key 生成 | 手动构建(@PostConstruct) | Spring 自动(@Service 名称) |
| Key 来源 | 业务组合:sipfFeeItem + persType | Bean 名称:"A1", "A2", "BX" |
| 代码复用 | ✅ 高(共享字段和方法) | ❌ 无(各实现完全独立) |
| 流程控制 | ✅ 父类控制(模板方法) | ❌ 各自控制 |
| 共享状态 | ✅ 可以(如 fileAttachmentApi) | ❌ 不可以 |
| 继承关系 | 单继承(extends) | 多实现(implements) |
| 扩展性 | ✅ 优秀(开闭原则) | ✅ 优秀(开闭原则) |
| 适用场景 | 有共同流程和逻辑 | 只有行为契约,无共同逻辑 |
| 复杂度 | 中等(有继承层次) | 简单(扁平结构) |
| 维护成本 | 中等(修改父类影响所有子类) | 低(各实现互不影响) |
PaySipfUploadService(抽象类) ✅ 正确选择
原因:所有上传服务都有共同流程(解析 → 校验 → 上传文件)
CalcFormulaService(接口) ✅ 正确选择
原因:各公式计算逻辑完全不同,没有可复用代码
优缺点对比
| 类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 接口(纯策略模式) | 代码复用率高、流程可控、共享资源方便、符合 DRY 原则 | 耦合度高、继承层次复杂、测试复杂、灵活性差、需要手动初始化 Map | 有共同流程和逻辑、需要共享资源、想控制执行流程、业务类型相对稳定 |
| 抽象类(策略模式 + 模板方法模式) | 解耦彻底、灵活性高、易于测试、Spring 自动管理、符合单一职责原则、支持嵌套策略 | 代码可能重复、无法共享状态、缺乏流程控制、可能出现 Key 冲突 | 各实现完全独立、策略类型很多且经常新增、需要多重实现、执行流程差异大 |
