|
1 | 1 | # **责任链模式重构复杂业务场景** |
2 | 2 |
|
| 3 | +## 引言: |
| 4 | + |
| 5 | +在软件设计中,我们经常会遇到复杂的业务场景,这些场景往往涉及多个处理对象和一系列操作。为了提高代码的可维护性和扩展性,我们可以采用设计模式来优化结构。其中,责任链模式(Chain of Responsibility)是一种有效的解决方案,它通过将请求沿着链传递来处理复杂的业务逻辑。 |
| 6 | + |
| 7 | +责任链模式定义了一系列处理器对象,每个处理器都有机会处理请求。请求会沿着链传递,直到被某个处理器处理或达到链的末端。这种模式降低了发送者和接收者之间的耦合度,使得新增或修改处理器更加灵活。 |
| 8 | + |
| 9 | +想要深入了解责任链设计模式的可直接访问<a href ="#jump">#</a> |
| 10 | + |
| 11 | +## 应用场景: |
| 12 | + |
| 13 | +在12306項目中,列车购买车票需要对前端传过来的购票参数进行一系列的校验,包括必填参数不可为空、参数是否满足要求、验证当前车次票数是否充足、乘客是否已购买当前车次等,如果通过if-else处理,虽然也能实现逻辑,但不利于扩展,如果要新增一个校验规则,就得修改当前代码,这样不符合设计模式的开闭原则。 |
| 14 | +因此,为了代码的健壮和扩展性,采用了责任链模式进行代码解耦,减少if-else的模板代码出现,提高代码的阅读性和可扩展性。 |
| 15 | + |
| 16 | +校验规则主要有如下: |
| 17 | +- 验证参数必填。 |
| 18 | +- 检查参数是否有效。 |
| 19 | +- 验证列车站点库存是否充足。 |
| 20 | + |
| 21 | +每个环节都可以视为一个处理器,它们按照一定的顺序组成了处理链。 |
| 22 | + |
| 23 | +## 责任链模式实现: |
| 24 | + |
| 25 | +首先,定义一个全局抽象的处理器接口: |
| 26 | + |
| 27 | +```java |
| 28 | +public interface AbstractChainHandler<T> extends Ordered { |
| 29 | + /** |
| 30 | + * 执行责任链逻辑 |
| 31 | + * |
| 32 | + * @param requestParam 责任链执行入参 |
| 33 | + */ |
| 34 | + void handler(T requestParam); |
| 35 | + |
| 36 | + /** |
| 37 | + * @return 责任链组件标识 |
| 38 | + */ |
| 39 | + String mark(); |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +接着,定义每个处理环节的处理器接口,用于区分不同环节的处理器,如下是列车购买车票的校验过滤器 |
| 44 | + |
| 45 | +```java |
| 46 | +/** |
| 47 | + * 列车购买车票过滤器 |
| 48 | + */ |
| 49 | +public interface TrainPurchaseTicketChainFilter<T extends PurchaseTicketReqDTO> extends AbstractChainHandler<PurchaseTicketReqDTO> { |
| 50 | + |
| 51 | + @Override |
| 52 | + default String mark() { |
| 53 | + return TicketChainMarkEnum.TRAIN_PURCHASE_TICKET_FILTER.name(); |
| 54 | + } |
| 55 | +} |
| 56 | +``` |
| 57 | +具体的列车购票校验器实现类 |
| 58 | + |
| 59 | +```java |
| 60 | +/** |
| 61 | + * 购票流程过滤器之验证参数必填 |
| 62 | + * |
| 63 | + */ |
| 64 | +@Component |
| 65 | +public class TrainPurchaseTicketParamNotNullChainHandler implements TrainPurchaseTicketChainFilter<PurchaseTicketReqDTO> { |
| 66 | + |
| 67 | + @Override |
| 68 | + public void handler(PurchaseTicketReqDTO requestParam) { |
| 69 | + if (StrUtil.isBlank(requestParam.getTrainId())) { |
| 70 | + throw new ClientException("列车标识不能为空"); |
| 71 | + } |
| 72 | + if (StrUtil.isBlank(requestParam.getDeparture())) { |
| 73 | + throw new ClientException("出发站点不能为空"); |
| 74 | + } |
| 75 | + if (StrUtil.isBlank(requestParam.getArrival())) { |
| 76 | + throw new ClientException("到达站点不能为空"); |
| 77 | + } |
| 78 | + if (CollUtil.isEmpty(requestParam.getPassengers())) { |
| 79 | + throw new ClientException("乘车人至少选择一位"); |
| 80 | + } |
| 81 | + for (PurchaseTicketPassengerDetailDTO each : requestParam.getPassengers()) { |
| 82 | + if (StrUtil.isBlank(each.getPassengerId())) { |
| 83 | + throw new ClientException("乘车人不能为空"); |
| 84 | + } |
| 85 | + if (Objects.isNull(each.getSeatType())) { |
| 86 | + throw new ClientException("座位类型不能为空"); |
| 87 | + } |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + @Override |
| 92 | + public int getOrder() { |
| 93 | + return 0; |
| 94 | + } |
| 95 | +} |
| 96 | + |
| 97 | +/** |
| 98 | + * 购票流程过滤器之验证参数是否有效 |
| 99 | + */ |
| 100 | +@Component |
| 101 | +@RequiredArgsConstructor |
| 102 | +public class TrainPurchaseTicketParamVerifyChainHandler implements TrainPurchaseTicketChainFilter<PurchaseTicketReqDTO> { |
| 103 | + |
| 104 | + private final TrainMapper trainMapper; |
| 105 | + private final TrainStationMapper trainStationMapper; |
| 106 | + private final DistributedCache distributedCache; |
| 107 | + |
| 108 | + @Override |
| 109 | + public void handler(PurchaseTicketReqDTO requestParam) { |
| 110 | + // 查询会员购票车次是否存在,通过封装后安全的 Get 方法 |
| 111 | + TrainDO trainDO = distributedCache.safeGet( |
| 112 | + TRAIN_INFO + requestParam.getTrainId(), |
| 113 | + TrainDO.class, |
| 114 | + () -> trainMapper.selectById(requestParam.getTrainId()), |
| 115 | + ADVANCE_TICKET_DAY, |
| 116 | + TimeUnit.DAYS); |
| 117 | + if (Objects.isNull(trainDO)) { |
| 118 | + // 如果按照严谨逻辑,类似异常应该记录当前用户的 userid 并发送到风控中心 |
| 119 | + // 如果一段时间有过几次的异常,直接封号处理。下述异常同理 |
| 120 | + throw new ClientException("请检查车次是否存在"); |
| 121 | + } |
| 122 | + // TODO,当前列车数据并没有通过定时任务每天生成最新的,所以需要隔离这个拦截。后期定时生成数据后删除该判断 |
| 123 | + if (!EnvironmentUtil.isDevEnvironment()) { |
| 124 | + // 查询车次是否已经发售 |
| 125 | + if (new Date().before(trainDO.getSaleTime())) { |
| 126 | + throw new ClientException("列车车次暂未发售"); |
| 127 | + } |
| 128 | + // 查询车次是否在有效期内 |
| 129 | + if (new Date().after(trainDO.getDepartureTime())) { |
| 130 | + throw new ClientException("列车车次已出发禁止购票"); |
| 131 | + } |
| 132 | + } |
| 133 | + // 车站是否存在车次中,以及车站的顺序是否正确 |
| 134 | + String trainStationStopoverDetailStr = distributedCache.safeGet( |
| 135 | + TRAIN_STATION_STOPOVER_DETAIL + requestParam.getTrainId(), |
| 136 | + String.class, |
| 137 | + () -> { |
| 138 | + LambdaQueryWrapper<TrainStationDO> queryWrapper = Wrappers.lambdaQuery(TrainStationDO.class) |
| 139 | + .eq(TrainStationDO::getTrainId, requestParam.getTrainId()) |
| 140 | + .select(TrainStationDO::getDeparture); |
| 141 | + List<TrainStationDO> actualTrainStationList = trainStationMapper.selectList(queryWrapper); |
| 142 | + return CollUtil.isNotEmpty(actualTrainStationList) ? JSON.toJSONString(actualTrainStationList) : null; |
| 143 | + }, |
| 144 | + Index12306Constant.ADVANCE_TICKET_DAY, |
| 145 | + TimeUnit.DAYS |
| 146 | + ); |
| 147 | + List<TrainStationDO> trainDOList = JSON.parseArray(trainStationStopoverDetailStr, TrainStationDO.class); |
| 148 | + boolean validateStation = validateStation( |
| 149 | + trainDOList.stream().map(TrainStationDO::getDeparture).toList(), |
| 150 | + requestParam.getDeparture(), |
| 151 | + requestParam.getArrival() |
| 152 | + ); |
| 153 | + if (!validateStation) { |
| 154 | + throw new ClientException("列车车站数据错误"); |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + @Override |
| 159 | + public int getOrder() { |
| 160 | + return 10; |
| 161 | + } |
| 162 | + |
| 163 | + public boolean validateStation(List<String> stationList, String startStation, String endStation) { |
| 164 | + int index1 = stationList.indexOf(startStation); |
| 165 | + int index2 = stationList.indexOf(endStation); |
| 166 | + if (index1 == -1 || index2 == -1) { |
| 167 | + return false; |
| 168 | + } |
| 169 | + return index2 >= index1; |
| 170 | + } |
| 171 | +} |
| 172 | + |
| 173 | +/** |
| 174 | + * 购票流程过滤器之验证列车站点库存是否充足 |
| 175 | + */ |
| 176 | +@Component |
| 177 | +@RequiredArgsConstructor |
| 178 | +public class TrainPurchaseTicketParamStockChainHandler implements TrainPurchaseTicketChainFilter<PurchaseTicketReqDTO> { |
| 179 | + |
| 180 | + private final SeatMarginCacheLoader seatMarginCacheLoader; |
| 181 | + private final DistributedCache distributedCache; |
| 182 | + |
| 183 | + @Override |
| 184 | + public void handler(PurchaseTicketReqDTO requestParam) { |
| 185 | + // 车次站点是否还有余票。如果用户提交多个乘车人非同一座位类型,拆分验证 |
| 186 | + String keySuffix = StrUtil.join("_", requestParam.getTrainId(), requestParam.getDeparture(), requestParam.getArrival()); |
| 187 | + StringRedisTemplate stringRedisTemplate = (StringRedisTemplate) distributedCache.getInstance(); |
| 188 | + List<PurchaseTicketPassengerDetailDTO> passengerDetails = requestParam.getPassengers(); |
| 189 | + Map<Integer, List<PurchaseTicketPassengerDetailDTO>> seatTypeMap = passengerDetails.stream() |
| 190 | + .collect(Collectors.groupingBy(PurchaseTicketPassengerDetailDTO::getSeatType)); |
| 191 | + seatTypeMap.forEach((seatType, passengerSeatDetails) -> { |
| 192 | + Object stockObj = stringRedisTemplate.opsForHash().get(TRAIN_STATION_REMAINING_TICKET + keySuffix, String.valueOf(seatType)); |
| 193 | + int stock = Optional.ofNullable(stockObj).map(each -> Integer.parseInt(each.toString())).orElseGet(() -> { |
| 194 | + Map<String, String> seatMarginMap = seatMarginCacheLoader.load(String.valueOf(requestParam.getTrainId()), String.valueOf(seatType), requestParam.getDeparture(), requestParam.getArrival()); |
| 195 | + return Optional.ofNullable(seatMarginMap.get(String.valueOf(seatType))).map(Integer::parseInt).orElse(0); |
| 196 | + }); |
| 197 | + if (stock >= passengerSeatDetails.size()) { |
| 198 | + return; |
| 199 | + } |
| 200 | + throw new ClientException("列车站点已无余票"); |
| 201 | + }); |
| 202 | + } |
| 203 | + |
| 204 | + @Override |
| 205 | + public int getOrder() { |
| 206 | + return 20; |
| 207 | + } |
| 208 | +} |
| 209 | +``` |
| 210 | +将上述的列车购票校验处理器组装成一条链: |
| 211 | + |
| 212 | +```java |
| 213 | +/** |
| 214 | + * 抽象责任链上下文 |
| 215 | + */ |
| 216 | +public final class AbstractChainContext<T> implements CommandLineRunner { |
| 217 | + |
| 218 | + private final Map<String, List<AbstractChainHandler>> abstractChainHandlerContainer = new HashMap<>(); |
| 219 | + |
| 220 | + /** |
| 221 | + * 责任链组件执行 |
| 222 | + * |
| 223 | + * @param mark 责任链组件标识 |
| 224 | + * @param requestParam 请求参数 |
| 225 | + */ |
| 226 | + public void handler(String mark, T requestParam) { |
| 227 | + List<AbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(mark); |
| 228 | + if (CollectionUtils.isEmpty(abstractChainHandlers)) { |
| 229 | + throw new RuntimeException(String.format("[%s] Chain of Responsibility ID is undefined.", mark)); |
| 230 | + } |
| 231 | + abstractChainHandlers.forEach(each -> each.handler(requestParam)); |
| 232 | + } |
| 233 | + |
| 234 | + @Override |
| 235 | + public void run(String... args) throws Exception { |
| 236 | + Map<String, AbstractChainHandler> chainFilterMap = ApplicationContextHolder |
| 237 | + .getBeansOfType(AbstractChainHandler.class); |
| 238 | + chainFilterMap.forEach((beanName, bean) -> { |
| 239 | + List<AbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(bean.mark()); |
| 240 | + if (CollectionUtils.isEmpty(abstractChainHandlers)) { |
| 241 | + abstractChainHandlers = new ArrayList(); |
| 242 | + } |
| 243 | + abstractChainHandlers.add(bean); |
| 244 | + List<AbstractChainHandler> actualAbstractChainHandlers = abstractChainHandlers.stream() |
| 245 | + .sorted(Comparator.comparing(Ordered::getOrder)) |
| 246 | + .collect(Collectors.toList()); |
| 247 | + abstractChainHandlerContainer.put(bean.mark(), actualAbstractChainHandlers); |
| 248 | + }); |
| 249 | + } |
| 250 | +} |
| 251 | +``` |
| 252 | +最后,在列车购票服务中使用。 |
| 253 | +```java |
| 254 | + @Override |
| 255 | + public TicketPurchaseRespDTO purchaseTicketsV1(PurchaseTicketReqDTO requestParam) { |
| 256 | + // 责任链模式,验证 1:参数必填 2:参数正确性 3:乘客是否已买当前车次等... |
| 257 | + purchaseTicketAbstractChainContext.handler(TicketChainMarkEnum.TRAIN_PURCHASE_TICKET_FILTER.name(), requestParam); |
| 258 | + // other logic 省略 |
| 259 | +} |
| 260 | +``` |
| 261 | + |
| 262 | +## 总结: |
| 263 | + |
| 264 | +通过使用责任链模式,我们成功地将复杂的列购票处理流程分解成了独立、可管理的处理器链。这种设计提高了代码的模块化程度,降低了各个处理器之间的耦合性,同时也便于后续的功能扩展和维护。 |
0 commit comments