Skip to content

通过策略+工厂模式来重构多个版本的下单流程

看到这里相信大家对用户生成订单的流程有了详细的了解了,针对锁的分类、锁的粒度、同步转异步的优化等分别写出了4个生成订单的版本,而且用于分布式锁使用了切面的方式,在Controller和Servic层中添加了Lock层,来实现切面的锁

当写到这里发现Lock层设计的有点冗余,最好是去掉,还有就是生成订单的版本是多个,如果后续再加新版本的话,类与类之间的单一职责原则和开闭原则就不是那么符合了

所以经过上述原因,将生成订单的多个版本流程进行重构,将Lock层去掉,使用策略+工厂模式来优化下单流程。

本次优化是在Controller和Lock层进行,Service层并不受到影响,不影响大家学习之前讲解完的业务。

策略模式

这4个版本的生成订单流程其实就是对应着不同的生成订单策略,所以这种场景使用策略模式再适合不过了,那么既然是策略,我们就要定义一个生成订单策略的接口,来规定生成订单的行为

生成订单策略接口

java
public interface ProgramOrderStrategy {
    
    /**
     * 创建订单
     * @param programOrderCreateDto 订单参数
     * @return 订单编号
     * */
    String createOrder(ProgramOrderCreateDto programOrderCreateDto);
    
    /**
     * 获取版本号
     * @return 版本号
     * */
    String version();
}

当定义好生成订单策略的接口后,就开始实现不同版本生成订单的具体策略了,就是将之前Lock层的生成订单逻辑移动到各自版本的生成订单策略中

已生成订单版本1为例:

java
@Component
public class ProgramOrderV1Strategy implements ProgramOrderStrategy {
    
    @Autowired
    private ProgramOrderService programOrderService;
    
    @Autowired
    private CompositeContainer compositeContainer;
    
    
    @RepeatExecuteLimit(
            name = RepeatExecuteLimitConstants.CREATE_PROGRAM_ORDER,
            keys = {"#programOrderCreateDto.userId","#programOrderCreateDto.programId"})
    @ServiceLock(name = PROGRAM_ORDER_CREATE_V1,keys = {"#programOrderCreateDto.programId"})
    @Override
    public String createOrder(final ProgramOrderCreateDto programOrderCreateDto) {
        compositeContainer.execute(CompositeCheckType.PROGRAM_ORDER_CREATE_CHECK.getValue(),programOrderCreateDto);
        return programOrderService.create(programOrderCreateDto);
    }
}

当把这四个版本的生成订单策略逻辑都设计好了后,就开始思考如何去调用这些实现好的策略,如果此时我们要具体调用的话,就会写成下面这样:

java
@Operation(summary  = "购票V1")
@PostMapping(value = "/create/v1")
public ApiResponse<String> createV1(@Valid @RequestBody ProgramOrderCreateDto programOrderCreateDto) {
    ProgramOrderStrategy programOrderStrategy = SpringUtil.getBean(ProgramOrderV1Strategy.class);
    String orderNumber = programOrderStrategy.createOrder(programOrderCreateDto);
    return ApiResponse.ok(orderNumber);
}

在Controller层的方法中,先获取 ProgramOrderV1Strategy 的bean对象,通过向上转型 ProgramOrderStrategy 类型来接收,然后调用定好的策略方法 createOrder 来生成订单,获取订单编号,最后返回即可。

整个流程是没有问题的,但是总感觉差点意思,不是那么的优化,也就是从Spring获取对象的过程

java
SpringUtil.getBean(ProgramOrderV1Strategy.class);

工厂模式

我们不应该从Spring容器中直接拿取ProgramOrderV1Strategy 具体的实现策略,这样对扩展性并不友好,如果后续改成在ProgramOrderCreateDto入参中加入订单版本参数,前端直接传入版本就能直接调用对应的生成订单版本,目前这种方式就不符合了

所以需要更好的方式来取得具体的策略实现,而什么结构和这种业务符合呢?通过一个参数直接能命中拿到一个具体策略?也就是说通过key能拿到Value?

我都说了通过Key想拿到Value,那自然是Map结构了啊!那么再思考需要线程安全吗?还是思考流程,我们要什么时候往Map中放入策略?那肯定是策略这个对象构建好了,就可以放进入了,而这些策略对象通过通过Spring来管理,而Spring加载对象是线程安全的,所以我们直接使用HashMap就可以了,不需要考虑线程安全问题了

既然我们确定好选用HashMap作为存放策略对象的容器后,那就要再设计一个上下文,来操作这个HashMap

存放生成订单策略实现的上下文

java
@Component
public class ProgramOrderContext {
    
    private static final Map<String,ProgramOrderStrategy> MAP = new HashMap<>(8);
    
    @Autowired
    private List<ProgramOrderStrategy> programOrderStrategyList;
    
    @PostConstruct
    public void init() {
        for (ProgramOrderStrategy programOrderStrategy : programOrderStrategyList) {
            MAP.put(programOrderStrategy.version(), programOrderStrategy);
        }
    }
    
    public ProgramOrderStrategy get(String version){
        return Optional.ofNullable(MAP.get(version)).orElseThrow(() -> 
                new DaMaiFrameException(BaseCode.PROGRAM_ORDER_STRATEGY_NOT_EXIST));
    }
}

这个上下文就是所谓的工厂,当我们传入版本参数后,此工厂就能直接给我们想要的对应版本策略。

上下文有了后,下面就来具体思考什么时候往上下文里放了,

具体策略的加载

当项目启动时,programOrderStrategyList 这个就是从 Spring 容器中获取所有 ProgramOrderStrategy 类型的集合,也就是所有实现策略的集合了,

接着执行被 @PostConstruct 修饰的 init 方法,这个方法的逻辑很简单,循环 programOrderStrategyList 这个集合,放到 MAP 中,key 就是版本号,value 就是对应的策略实现了。

版本枚举

在从上下文获取和放入的方法中,版本参数是使用字符串,通过ProgramOrderVersion枚举中的version来指定

java
public enum ProgramOrderVersion {
    /**
     * 版本
     * */
    V1_VERSION("v1","v1版本"),
    
    V2_VERSION("v2","v2版本"),
   
    V3_VERSION("v3","v3版本"),
    
    V4_VERSION("v4","v4版本"),
    ;

    private final String version;

    private final String msg;

    ProgramOrderVersion(String version, String msg) {
        this.version = version;
        this.msg = msg;
    }

    public String getVersion() {
        return version;
    }
    

    public String getMsg() {
        return this.msg == null ? "" : this.msg;
    }
    

    public static String getMsg(String version) {
        for (ProgramOrderVersion re : ProgramOrderVersion.values()) {
            if (re.version.equals(version)) {
                return re.msg;
            }
        }
        return "";
    }

    public static ProgramOrderVersion getRc(String version) {
        for (ProgramOrderVersion re : ProgramOrderVersion.values()) {
            if (re.version.equals(version)) {
                return re;
            }
        }
        return null;
    }
}

讲解到这里,我们的具体实现策略才真正的成型,下面就将每个具体的实现策略粘贴出来

生成订单v1策略

java
@Component
public class ProgramOrderV1Strategy implements ProgramOrderStrategy {
    
    @Autowired
    private ProgramOrderService programOrderService;
    
    @Autowired
    private CompositeContainer compositeContainer;
    
    
    @RepeatExecuteLimit(
            name = RepeatExecuteLimitConstants.CREATE_PROGRAM_ORDER,
            keys = {"#programOrderCreateDto.userId","#programOrderCreateDto.programId"})
    @ServiceLock(name = PROGRAM_ORDER_CREATE_V1,keys = {"#programOrderCreateDto.programId"})
    @Override
    public String createOrder(final ProgramOrderCreateDto programOrderCreateDto) {
        compositeContainer.execute(CompositeCheckType.PROGRAM_ORDER_CREATE_CHECK.getValue(),programOrderCreateDto);
        return programOrderService.create(programOrderCreateDto,ProgramOrderVersion.V1_VERSION.getValue());
    }
    
    @Override
    public String version() {
        return ProgramOrderVersion.V1_VERSION.getVersion();
    }
}

生成订单v2策略

java
@Slf4j
@Component
public class ProgramOrderV2Strategy implements ProgramOrderStrategy {
    
    @Autowired
    private ProgramOrderService programOrderService;
    
    @Autowired
    private ServiceLockTool serviceLockTool;
    
    @Autowired
    private CompositeContainer compositeContainer;
    
    @Autowired
    private LocalLockCache localLockCache;
    
    
    @RepeatExecuteLimit(
            name = RepeatExecuteLimitConstants.CREATE_PROGRAM_ORDER,
            keys = {"#programOrderCreateDto.userId","#programOrderCreateDto.programId"})
    @Override
    public String createOrder(ProgramOrderCreateDto programOrderCreateDto) {
        compositeContainer.execute(CompositeCheckType.PROGRAM_ORDER_CREATE_CHECK.getValue(),programOrderCreateDto);
        List<SeatDto> seatDtoList = programOrderCreateDto.getSeatDtoList();
        List<Long> ticketCategoryIdList = new ArrayList<>();
        if (CollectionUtil.isNotEmpty(seatDtoList)) {
            ticketCategoryIdList =
                    seatDtoList.stream().map(SeatDto::getTicketCategoryId).distinct().sorted().collect(Collectors.toList());
        }else {
            ticketCategoryIdList.add(programOrderCreateDto.getTicketCategoryId());
        }
        List<ReentrantLock> localLockList = new ArrayList<>(ticketCategoryIdList.size());
        List<RLock> serviceLockList = new ArrayList<>(ticketCategoryIdList.size());
        List<ReentrantLock> localLockSuccessList = new ArrayList<>(ticketCategoryIdList.size());
        List<RLock> serviceLockSuccessList = new ArrayList<>(ticketCategoryIdList.size());
        for (Long ticketCategoryId : ticketCategoryIdList) {
            String lockKey = StrUtil.join("-",PROGRAM_ORDER_CREATE_V2,
                    programOrderCreateDto.getProgramId(),ticketCategoryId);
            ReentrantLock localLock = localLockCache.getLock(lockKey,false);
            RLock serviceLock = serviceLockTool.getLock(LockType.Reentrant, lockKey);
            localLockList.add(localLock);
            serviceLockList.add(serviceLock);
        }
        for (ReentrantLock reentrantLock : localLockList) {
            try {
                reentrantLock.lock();
            }catch (Throwable t) {
                break;
            }
            localLockSuccessList.add(reentrantLock);
        }
        boolean serviceLockFail = false;
        for (RLock rLock : serviceLockList) {
            try {
                rLock.lock();
            }catch (Throwable t) {
                serviceLockFail = true;
                break;
            }
            serviceLockSuccessList.add(rLock);
        }
        try {
            if (serviceLockFail) {
                throw new DaMaiFrameException(BaseCode.SERVICE_LOCK_FAIL);
            }
            return programOrderService.create(programOrderCreateDto,ProgramOrderVersion.V2_VERSION.getValue());
        }finally {
            for (int i = serviceLockSuccessList.size() - 1; i >= 0; i--) {
                RLock rLock = serviceLockSuccessList.get(i);
                try {
                    rLock.unlock();
                }catch (Throwable t) {
                    log.error("service lock unlock error",t);
                }
            }
            for (int i = localLockSuccessList.size() - 1; i >= 0; i--) {
                ReentrantLock reentrantLock = localLockSuccessList.get(i);
                try {
                    reentrantLock.unlock();
                }catch (Throwable t) {
                    log.error("local lock unlock error",t);
                }
            }
        }
    }
    
    @Override
    public String version() {
        return ProgramOrderVersion.V2_VERSION.getVersion();
    }
}

生成订单v3策略

java
@Slf4j
@Component
public class ProgramOrderV3Strategy implements ProgramOrderStrategy {
    
    @Autowired
    private ProgramOrderService programOrderService;
    
    @Autowired
    private BaseProgramOrder baseProgramOrder;
    
    @Autowired
    private CompositeContainer compositeContainer;
    
    @RepeatExecuteLimit(
            name = RepeatExecuteLimitConstants.CREATE_PROGRAM_ORDER,
            keys = {"#programOrderCreateDto.userId","#programOrderCreateDto.programId"})
    @Override
    public String createOrder(ProgramOrderCreateDto programOrderCreateDto) {
        compositeContainer.execute(CompositeCheckType.PROGRAM_ORDER_CREATE_CHECK.getValue(),programOrderCreateDto);
        return baseProgramOrder.localLockCreateOrder(PROGRAM_ORDER_CREATE_V3,programOrderCreateDto,
                () -> programOrderService.createNew(programOrderCreateDto,ProgramOrderVersion.V3_VERSION.getValue()));
    }
    
    @Override
    public String version() {
        return ProgramOrderVersion.V3_VERSION.getVersion();
    }
}

生成订单v4策略

java
@Slf4j
@Component
public class ProgramOrderV4Strategy implements ProgramOrderStrategy {
    
    @Autowired
    private ProgramOrderService programOrderService;
    
    @Autowired
    private BaseProgramOrder baseProgramOrder;
    
    @Autowired
    private CompositeContainer compositeContainer;
    
    @RepeatExecuteLimit(
            name = RepeatExecuteLimitConstants.CREATE_PROGRAM_ORDER,
            keys = {"#programOrderCreateDto.userId","#programOrderCreateDto.programId"})
    @Override
    public String createOrder(ProgramOrderCreateDto programOrderCreateDto) {
        compositeContainer.execute(CompositeCheckType.PROGRAM_ORDER_CREATE_CHECK.getValue(),programOrderCreateDto);
        return baseProgramOrder.localLockCreateOrder(PROGRAM_ORDER_CREATE_V4,programOrderCreateDto,
                () -> programOrderService.createNewAsync(programOrderCreateDto,ProgramOrderVersion.V4_VERSION.getValue()));
    }
    
    @Override
    public String version() {
        return ProgramOrderVersion.V4_VERSION.getVersion();
    }
}

BaseProgramOrder

由于v3和v4版本还都需要本地锁的逻辑,所以把本地锁的逻辑放到了BaseProgramOrder中,这样v3和v4的实现策略可以实现共用

java
@Slf4j
@Component
public class BaseProgramOrder {
    
    @Autowired
    private LocalLockCache localLockCache;
    
    public String localLockCreateOrder(String lockKeyPrefix, ProgramOrderCreateDto programOrderCreateDto, 
                                          LockTask<String> lockTask){
        List<SeatDto> seatDtoList = programOrderCreateDto.getSeatDtoList();
        List<Long> ticketCategoryIdList = new ArrayList<>();
        if (CollectionUtil.isNotEmpty(seatDtoList)) {
            ticketCategoryIdList =
                    seatDtoList.stream().map(SeatDto::getTicketCategoryId).distinct().sorted().collect(Collectors.toList());
        }else {
            ticketCategoryIdList.add(programOrderCreateDto.getTicketCategoryId());
        }
        List<ReentrantLock> localLockList = new ArrayList<>(ticketCategoryIdList.size());
        List<ReentrantLock> localLockSuccessList = new ArrayList<>(ticketCategoryIdList.size());
        for (Long ticketCategoryId : ticketCategoryIdList) {
            String lockKey = StrUtil.join("-",lockKeyPrefix,
                    programOrderCreateDto.getProgramId(),ticketCategoryId);
            ReentrantLock localLock = localLockCache.getLock(lockKey,false);
            localLockList.add(localLock);
        }
        for (ReentrantLock reentrantLock : localLockList) {
            try {
                reentrantLock.lock();
            }catch (Throwable t) {
                break;
            }
            localLockSuccessList.add(reentrantLock);
        }
        try {
            return lockTask.execute();
        }finally {
            for (int i = localLockSuccessList.size() - 1; i >= 0; i--) {
                ReentrantLock reentrantLock = localLockSuccessList.get(i);
                try {
                    reentrantLock.unlock();
                }catch (Throwable t) {
                    log.error("local lock unlock error",t);
                }
            }
        }
    }
}

调用流程

在Controller层中,我们就可以直接通过传入版本来获取对应的策略,然后调用生成订单方法即可。

java
@RestController
@RequestMapping("/program/order")
@Tag(name = "program-order", description = "节目订单")
public class ProgramOrderController {
    
    @Autowired
    private ProgramOrderContext programOrderContext;
    
    @Operation(summary  = "购票V1")
    @PostMapping(value = "/create/v1")
    public ApiResponse<String> createV1(@Valid @RequestBody ProgramOrderCreateDto programOrderCreateDto) {
        return ApiResponse.ok(programOrderContext.get(ProgramOrderVersion.V1_VERSION.getVersion())
                .createOrder(programOrderCreateDto));
    }
    
    @Operation(summary  = "购票V2")
    @PostMapping(value = "/create/v2")
    public ApiResponse<String> createV2(@Valid @RequestBody ProgramOrderCreateDto programOrderCreateDto) {
        return ApiResponse.ok(programOrderContext.get(ProgramOrderVersion.V2_VERSION.getVersion())
                .createOrder(programOrderCreateDto));
    }
    
    @Operation(summary  = "购票V3")
    @PostMapping(value = "/create/v3")
    public ApiResponse<String> createV3(@Valid @RequestBody ProgramOrderCreateDto programOrderCreateDto) {
        return ApiResponse.ok(programOrderContext.get(ProgramOrderVersion.V3_VERSION.getVersion())
                .createOrder(programOrderCreateDto));
    }
    
    @Operation(summary  = "购票V4")
    @PostMapping(value = "/create/v4")
    public ApiResponse<String> createV4(@Valid @RequestBody ProgramOrderCreateDto programOrderCreateDto) {
        return ApiResponse.ok(programOrderContext.get(ProgramOrderVersion.V4_VERSION.getVersion())
                .createOrder(programOrderCreateDto));
    }
}

以生成订单v1版本为例,在生成订单方法中,通过ProgramOrderVersion.V1_VERSION.getVersion()来获取版本,然后将此版本传入策略上下文中,就可以获取具体的策略了。

这种方式比最开始将的从Spring容器中直接获取具体实现要好多了,不过还是没有真正的让前端改成通过传递版本参数来自动匹配,原因是大家到这里已经将整个流程都学习的差不多了,动前端逻辑修改地方比较多,重点还是想让小伙伴掌握这个核心技巧。

本人将后端自动匹配版本的逻辑贴在下面,其实特别简单,大家直接看就可以了

入参

java
@Data
@Schema(title="ProgramOrderCreateDto", description ="节目订单创建")
public class ProgramOrderCreateDto {
    
    @Schema(name ="programId", type ="Long", description ="节目id",requiredMode= RequiredMode.REQUIRED)
    @NotNull
    private Long programId;
    
    @Schema(name ="userId", type ="Long", description ="用户id",requiredMode= RequiredMode.REQUIRED)
    @NotNull
    private Long userId;
    
    @Schema(name ="ticketUserIdList", type ="List<Long>", description ="购票人id集合",requiredMode= RequiredMode.REQUIRED)
    @NotNull
    private List<Long> ticketUserIdList;
    
    @Schema(name ="seatDtoList", type ="List<SeatDto>", description = "座位")
    private List<SeatDto> seatDtoList;
    
    @Schema(name ="ticketCategoryId", type ="Long", description = "节目票档id(如果不选座位,那么票档id必填)")
    private Long ticketCategoryId;
    
    @Schema(name ="ticketCount", type ="Integer", description = "购买票数量(如果不选座位,那么购买票数量必填)")
    private Integer ticketCount;

    /**
    * 订单版本参数
    */
    @Schema(name ="createOrderVersion", type ="String", description ="生成订单的版本",requiredMode= RequiredMode.REQUIRED)
    @NotBlank
    private String createOrderVersion;
}

更新: 2026-01-22 09:32:21
原文: https://www.yuque.com/u22210564/ykdrdh/sc4unob6or8tv4wx

Java 后端面试知识库