设计模式业务实践

184 阅读13分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

工作中由于业务的复杂与多变性,接触到了一些设计模式相关的内容,网上已经有很多设计模式的基础知识分享,所以这里不涉及到详细知识点,只是把工作中相关设计模式在业务场景下的应用做一个总结,以及开发过程中对相关模式的思考;

一、状态模式

  1. 状态模式一般基于接口进行设计,应用场景为:针对于同一个对象,不同的操作导致不同的状态,典型的应用场景比如工单审批。
  2. 在处理这类场景之前,要先梳理出一个操作与状态转换流程图;
  3. 至于编码过程中,按照流程图,先抽象出操作与状态两个枚举类,更有利于提升编码效率;
  4. 对于扩展不同的状态非常方便,比如,工单需要三次处理或者需要减少一种状态的需求; 我们的B端系统中有一个业务场景,生成工单,并对工单进行审批,审批操作流程图如下所示:

工单状态处理.PNG

伪代码逻辑为:

//抽象出工单状态操作类
public interface WorkTaskState<T> {
    //同意操作,代表该状态下,可以点击同意
    void agree(T request);

    //驳回操作,代表该状态下,可以点击驳回
    void reject(T request);
    
    //超时操作,代表该状态下,工单可以做超时处理操作,超时处理可以通过定时任务触发
    void timeOutClose(T request);
}

//用一个抽象类去承载默认的处理逻辑
public class WorkTaskAbstractState implements WorkTaskState<RequestDTO> {

    @Resource
    protected WorkTaskManager workTaskManager;

    public WorkTaskStatusEnum getCurStatus() {
        throw new Exception("不被允许的操作");
    }

    @Override
    public void agree(RequestDTO request) {
        throw new Exception("不被允许的操作");
    }

    @Override
    public void reject(RequestDTO request) {
        throw new Exception("不被允许的操作");
    }

    @Override
    public void timeOutClose(RequestDTO request) {
        throw new Exception("不被允许的操作");
    }
    //公共的业务逻辑,可以被子类继承,比如公共的参数转换,日志打印等
    //protected Object convertDTO() {  // do something }
}

//不同状态的子类,实现了不同状态下对应的不同操作,这些子类可以注入到IOC容器中
@Component
public class WorkTaskWaitingState extends WorkTaskAbstractState {

    private static final Logger logger = ...;

    @Override
    public WorkTaskStatusEnum getCurStatus() {
        return WorkTaskStatusEnum.WAITING;
    }

    @Override
    public void agree(RequestDTO request) {
        //待处理状态下,用户点击同意,主要关注点在于将工单状态置为已处理done
        workTaskManager.updateStatus(WorkTaskStatusEnum.DONE.getStatus());
        insertLog();
    }

    @Override
    public void reject(RequestDTO request) {
        //待处理状态下,用户点击驳回,主要关注点在于将工单状态置为待复核checking
        workTaskManager.updateStatus(WorkTaskStatusEnum.CHECKING.getStatus());
        insertLog();
    }

    //由于待处理状态下没有其余操作,因此,其余的方法使用抽象类中的公共方法即可,不需要覆盖
}

@Component
public class WorkTaskCheckingState extends WorkTaskAbstractState {

    private static final Logger logger = ...;

    @Override
    public WorkTaskStatusEnum getCurStatus() {
        return WorkTaskStatusEnum.CHECKING;
    }

    @Override
    public void agree(RequestDTO request) {
        //待复核状态下,用户点击同意,主要关注点在于将工单状态置为已处理done
        workTaskManager.updateStatus(WorkTaskStatusEnum.DONE.getStatus());
        insertLog();
    }

    @Override
    public void reject(RequestDTO request) {
        //待复核状态下,用户点击驳回,主要关注点在于将工单状态置为关闭closed
        workTaskManager.updateStatus(WorkTaskStatusEnum.CLOSED.getStatus());
        insertLog();
    }

    @Override
    public void timeOutClose(RequestDTO request) {
        //待复核状态下,超时自动关闭,主要关注点在于将工单状态置为已超时timeOut
        workTaskManager.updateStatus(WorkTaskStatusEnum.TIME_OUT.getStatus());
        insertLog();
    }
}

//其余状态下没有操作,所以,不需要针对于其余的状态进行子类实现
//同时,我们需要一个入口类,来屏蔽底层的各个状态转换,与controller层的交互依赖于该入口类
@Component
public class WorkTaskStateService implements WorkTaskState<RequestDTO> {

    @Resource
    private WorkTaskWaitingState workTaskWaitingState;

    @Resource
    private WorkTaskCheckingState workTaskCheckingState;

    //根据状态获取不同的子类
    private WorkTaskState getService(RequestDTO request) {
        if (request.getStatus().equals(WorkTaskStatusEnum.WAITING.getStatus())) {
            return workTaskWaitingState;
        } else if (request.getStatus().equals(WorkTaskStatusEnum.CHECKING.getStatus())) {
            return workTaskCheckingState;
        }
        throw new Exception("该状态下不允许任何操作");
    }

    @Override
    public void agree(RequestDTO request) {
        this.getService(request).agree(request);
    }

    @Override
    public void reject(RequestDTO request) {
        this.getService(request).reject(request);
    }

    @Override
    public void timeOutClose(RequestDTO request) {
        this.getService(request).timeOutClose(request);
    }
}

二、模板模式

  1. 模板模式一般基于抽象类的继承进行设计,应用场景为:多个场景下有相似的处理步骤,源码中用的比较多,比如AQS;
  2. 在处理这类场景之前,要先梳理出实现该场景的功能所需要的步骤,区分出哪些步骤是公共逻辑,哪些步骤是各个场景的私有逻辑,需要单独实现;
  3. 同样,最好先画出流程图; 我们部门是价格策略分析部,要做市场价格调研,需求目的在于对比公司平台的商品价格同市场上同类商品价格之间的差异,从而实现比价的功能,为运营配置价格提供参考依据,大体逻辑为(前提:爬虫获取第三方平台商品信息,包括价格,名称,品类,包规,地区等):
  4. 根据第三方商品信息,匹配自营商品,获取自营商品的品类,价格,包规,基础信息等;
  5. 取品类下的配置规则,不同平台下有不同的比价规则
  6. 根据比价规则,自营价格,三方价格,进行价格比对,对自营商品打标签(平台最低价,比XX平台优惠,比XX平台高等);
  7. 落库存储; 伪代码逻辑为:
//抽象模板,当然也可以定义一个接口,此处定义了接口但未写出来
public abstract class AbstractCompareService implements ICompareService {

    @Resource
    private SkuService skuService;

    @Resource
    private RuleService ruleService;

    @Resource
    private CompareService compareService;

    @Override
    public List<RivalComparePriceEntity> handle(ThirdPlatformSkuDTO thirdSkuDTO) {
        //1、根据爬虫获取的第三方商品信息,找出最匹配的自营SKU
        OwnerSkuDTO ownerSkuDTO = this.getOwnerMatchSku(thirdSkuDTO);
        //2、获取品类规则
        CompareRule rule = this.getCompareRule(thirdSkuDTO, ownerSkuDTO);
        //3、针对不同的品类规则进行价格比较,拿到价格标签
        Integer compareTag = this.getCompareTag(rule, thirdSkuDTO, ownerPrice);
        //4、入库
        this.insert(ownerSkuDTO, compareTag);
    }

    public OwnerSkuDTO getOwnerMatchSku(ThirdPlatformSkuDTO thirdSkuDTO) {
        return skuService.getOwnerMatchSku(thirdSkuDTO);
    }

    public CompareRule getCompareRule(ThirdPlatformSkuDTO thirdSkuDTO, OwnerSkuDTO ownerSkuDTO) {
        return ruleService.getRule(thirdSkuDTO.getPlatform(), ownerSkuDTO.getClassId());
    }

    abstract Integer getCompareTag(CompareRule rule, ThirdPlatformSkuDTO thirdSkuDTO, OwnerSkuDTO ownerSkuDTO);

    public void insert(OwnerSkuDTO ownerSkuDTO, Integer compareTag) {
        CompareLogDTO dto = new CompareLogDTO();
        dto.setSkuId(ownerSkuDTO.getId());
        dto.setPlatform(this.getPlatform());
        dto.setCompareTag(compareTag);
        dto.setTime(new Date());
        compareService.insertDTO(dto);
    }
}

//具体的子类实现,示例中模拟了2个子类,一个是jd,一个是tb,可以注入到IOC容器中
//不同的平台由于业务逻辑,包规比例,平台定位等不同,所以价差计算规则也不同
@Service
public class JdCompareService extends AbstractCompareService {

    private static final Logger logger = ...;

    @Override
    public Integer getCompareTag(CompareRule rule, ThirdPlatformSkuDTO thirdSkuDTO, OwnerSkuDTO skuDTO) {
        //jd比价规则,使用的字段可能是minPrice,maxPrice,进行大小比价,具体业务逻辑略
        return CompareTagEnum.LOW_PRICE.getValue();
    }
}

@Service
public class TbCompareService extends AbstractCompareService {

    private static final Logger logger = ...;

    @Override
    public Integer getCompareTag(CompareRule rule, ThirdPlatformSkuDTO thirdSkuDTO, OwnerSkuDTO skuDTO) {
        //tb比价规则,使用的字段可能是percent,需要乘以一个系数,又或者使用中位数比价,比如midPrice,具体业务逻辑略
        return CompareTagEnum.EQUAL.getValue();
    }
}

//模板方法也需要一个代理类来当作门面的角色,调用方不需要知道具体实现类的内部细节
//比如Lock的实现,NonfairSync,FairSync,在指定ReentrantLock的同时必须指定具体实现类,但是调用方使用Lock的时候无需知道内部模板的具体处理细节;
@Service
public class CompareService {

    private static final Logger logger = ...;

    @Resource
    private JdCompareService jdCompareService;

    @Resource
    private TbCompareService tbCompareService;

    public List<RivalComparePriceEntity> addCompareLog(ThirdPlatformSkuDTO thirdSkuDTO) {
        //基础参数验证
        this.validate(thirdSkuDTO);
        //根据不同的platform来获取实现类
        try {
            this.getService().handle(thirdSkuDTO);
        } catch (Exception e) {
            logger.error("error", e);
        }
    }

    private AbstractCompareService getService(ThirdPlatformSkuDTO thirdSkuDTO) {
        if ("jd".equals(thirdSkuDTO.getPlatform())) {
            return jdCompareService;
        } else if ("tb".equals(thirdSkuDTO.getPlatform())) {
            return tbCompareService;
        }
        throw new Exception("该三方平台暂不支持");
    }


    private void validate(ThirdPlatformSkuDTO thirdSkuDTO) {
        if (thirdSkuDTO == null) {
            throw new Exception("竞品对象不能为空");
        }
        if (thirdSkuDTO.getPlatform() == null) {
            throw new Exception("竞品平台不能为空");
        }
        if (thirdSkuDTO.getPrice() == null) {
            throw new Exception("竞品价格不能为空");
        }
    }
}

还有一个工作中的业务场景,作为一个补充场景放在这里吧。我们以商品组合展示为例

  1. 总计有4个场景展示商品信息:首页,搜索页,推荐页,个人清单
  2. 针对于同一个商品,各个场景下展示的内容是不同的,比如,是否展示店铺入口,是否展示优惠信息,是否展示购买频次,是否添加广告品等,每一个信息均对应一个第三方服务; 现有的解决方案是:定一个reqDTO,里面包含了isGetShop,isGetPromotion,isGetFrequency,isGetAd等,每个场景下通过构造不同的参数去底层获取数据;经过思考,可以使用模板对已有的方案进行改动,且增加可扩展性和个性定制,大体方案为:
//获取商品信息公共抽象类
public abstract class AbstractSkuService {
    @Resource
    private SkuManager skuManager;
    @Resource
    private PriceManager priceManager;
    @Resource
    private StockManager stockManager;
    @Resource
    private ShopManager shopManager;
    @Resource
    private FrequencyManager frequencyManager;

    public List<SkuDTO> getSkuDTOList(List<Long> skuIdList) {
        //模板方法,获取所有的基础信息,然后进行组装,生成一个全量的大对象
        //如果不需要price信息,那么price方法返回一个空数组即可,依靠子类重写
        List<SkuDTO> skuList = skuManager.getSkuDTOList(skuIdList);
        List<PriceDTO> priceList = this.getSkuPriceList(skuIdList);
        List<StockDTO> stockList = this.getSkuStockList(skuIdList);
        List<ShopDTO> shopList = this.getSkuShopList(skuIdList);
        List<FrequencyDTO> frequencyList = this.getSkuFrequencyList(skuIdList);
        this.fillAllFields(skuList, priceList, stockList, shopList, frequencyList);
        //不同场景自定义实现,比如过滤,重新加入一些额外属性等,默认返回skuDTOList
        //注:实际编码中,此处可以考虑返回一个泛型
        return this.customizeAssemble(skuDTOList);
    }

    public List<SkuDTO> customizeAssemble(List<SkuDTO> list) {
        return list;
    }

    public abstract List<PriceDTO> getSkuPriceList(List<Long> skuIdList);

    public abstract List<StockDTO> getSkuStockList(List<Long> skuIdList);

    public abstract List<ShopDTO> getSkuShopList(List<Long> skuIdList);

    public abstract List<FrequencyDTO> getSkuFrequencyList(List<Long> skuIdList);
}

//默认的全量实现类
@Service
public class DefaultSkuService extends AbstractSkuService {

    @Override
    public List<PriceDTO> getSkuPriceList(List<Long> skuIdList) {
        return priceManager.getSkuPriceList(skuIdList);
    }

    @Override
    public List<StockDTO> getSkuStockList(List<Long> skuIdList) {
        return stockManager.getSkuStockList(skuIdList);
    }

    @Override
    public List<ShopDTO> getSkuShopList(List<Long> skuIdList) {
        return shopManager.getSkuShopList(skuIdList);
    }

    @Override
    public List<FrequencyDTO> getSkuFrequencyList(List<Long> skuIdList) {
        return frequencyManager.getSkuFrequencyList(skuIdList);
    }
}

//各个场景下不同的实现类,以首页场景为例,比如不需要库存信息和门店信息,并且过滤商品状态
//controller根据不同的场景类型,使用不同的子类,来完成特定场景下的商品信息返回
@Service
public class HomePageSkuService extends DefaultSkuService {

    @Override
    public List<StockDTO> getSkuStockList(List<Long> skuIdList) {
        return new ArrayList<>();
    }

    @Override
    public List<ShopDTO> getSkuShopList(List<Long> skuIdList) {
        return new ArrayList<>();
    }

    @Override
    public List<SkuDTO> customizeAssemble(List<SkuDTO> list) {
        return list.stream().filter(x -> x.getStatus() != 1).collect(Collectors.toList());
    }
}

三、责任链模式

  1. 强调各个节点,各个步骤,可以自由组合顺序,各个节点无互相调用关系,地位平等,关键:可选择性,可排序性
  2. 顺序的实现,可以使用链表,或者数组;
  3. 场景:各种filter用于筛选,classLoader实际上可以认为是模板+责任链,使用的是parent链表; 业务场景:sku基础价格,需要在该价格的基础上计算其余包规的价格,根据加价策略进行处理,加价策略可以根据一级分类,二级分类,sku进行配置,优先级是skuRule>class2Rule>class1Rule,这个由于有顺性和可选择性,所以我们使用责任链来实现,伪代码如下所示:
//业务实体
public static class PriceRule {
    private Integer c1Id;
    private Integer c2Id;
    private Integer skuId;
    private Integer addNum;
}
public static class SkuDTO {
    private Integer c1Id;
    private Integer c2Id;
    private Integer skuId;
    private Integer originalPrice;
}

//每一个职责链的节点,共有的行为
public interface PriceRuleHandler {
    void setNext(PriceRuleHandler next);
    Integer getPrice(List<PriceRule> ruleList, SkuDTO skuDTO);
}

//该类本质上是一个模板,借鉴了classLoader的源码,大家可以去看看
public abstract class AbstractPriceRuleHandler implements PriceRuleHandler {

    private PriceRuleHandler nextRuleHandler;

    @Override
    public void setNext(PriceRuleHandler nextRuleHandler) {
        this.nextRuleHandler = nextRuleHandler;
    }

    @Override
    public Integer getPrice(List<PriceRule> ruleList, SkuDTO skuDTO) {
        Integer targetPrice = null;
        if (nextRuleHandler != null) {
            targetPrice = nextRuleHandler.getPrice(ruleList, skuDTO);
        }
        if (targetPrice == null) {
            targetPrice = this.getPrice0(ruleList, skuDTO);
        }
        return targetPrice;
    }

    abstract Integer getPrice0(List<PriceRule> ruleList, SkuDTO skuDTO);
}


public class Class1PriceRuleHandler extends AbstractPriceRuleHandler {
    @Override
    Integer getPrice0(List<PriceRule> ruleList, SkuDTO skuDTO) {
        PriceRule rule = ruleList.stream().filter(x -> x.getC1Id().equals(skuDTO.getC1Id())).findFirst().orElse(null);
        if (rule != null) {
            return skuDTO.getOriginalPrice() + rule.getAddNum();
        }
        return null;
    }
}
public class Class2PriceRuleHandler extends AbstractPriceRuleHandler {
    @Override
    Integer getPrice0(List<PriceRule> ruleList, SkuDTO skuDTO) {
        PriceRule rule = ruleList.stream().filter(x -> Objects.equals(x.getC2Id(), skuDTO.getC2Id())).findFirst().orElse(null);
        if (rule != null) {
            return skuDTO.getOriginalPrice() + rule.getAddNum();
        }
        return null;
    }
}
public class SkuPriceRuleHandler extends AbstractPriceRuleHandler {
    @Override
    Integer getPrice0(List<PriceRule> ruleList, SkuDTO skuDTO) {
        PriceRule rule = ruleList.stream().filter(x -> Objects.equals(x.getSkuId(), skuDTO.getSkuId())).findFirst().orElse(null);
        if (rule != null) {
            return skuDTO.getOriginalPrice() + rule.getAddNum();
        }
        return null;
    }
}


//对外提供的门面类,供外部访问,屏蔽底层细节
@Service
public class RuleService {
    
    public Integer getPrice(SkuDTO skuDTO) {
        //如果需要自由组合,那么各个handler不可以使用@Service注入的方式,因为是单实例的,可能多个链之间会有影响,在service中创建对象实时组合即可
        PriceRuleHandler class1Handler = new Class1PriceRuleHandler();
        PriceRuleHandler class2Handler = new Class2PriceRuleHandler();
        PriceRuleHandler skuHandler = new SkuPriceRuleHandler();
        class1Handler.setNext(class2Handler);
        class2Handler.setNext(skuHandler);
        //
        Integer price = class1Handler.getPrice(this.getList(), skuDTO);
        System.out.println(price);
        return price;
    }

    //模拟数据
    public List<PriceRule> getList() {
        PriceRule rule1 = new PriceRule();
        rule1.setC1Id(1);
        rule1.setAddNum(1);
        PriceRule rule2 = new PriceRule();
        rule2.setC2Id(2);
        rule2.setAddNum(2);
        PriceRule rule3 = new PriceRule();
        rule3.setSkuId(3);
        rule3.setAddNum(3);
        return Arrays.asList(rule1, rule2, rule3);
    }
}

四、策略模式

对于我来说,策略模式更像是if-else的增强版,选择一个实体类来执行即可,各个子类是平级的,没有任何关联。什么情况下用策略模式,一句话总结为:意识到一个问题,可以有不同的解法,那么这个场景适用于策略模式,比如:

  1. 旅行出游方式的选择,骑自行车、自驾、高铁、飞机等,每一种旅行方式都是一个策略;
  2. 支付方式的选择,支付宝,微信,银行卡等,底层肯定是不同的第三方服务;
  3. 抽奖策略的选择,抽中了一个自行车,后台如何处理,抽中了一个谢谢惠顾,后台如何处理等;
  4. 版本控制也可以使用策略模式,对于APP端来说,高版本要兼容一段时间内的低版本,如果两个版本差距过大,可以使用策略模式,请求到后端不同的service,特别是首页布局等场景下; 业务场景以首页布局为例,如果一个APP根据版本号的不同,首页分为了3中布局方式,且出参格式均不相同,那么伪代码如何处理呢?
public interface HomePageStrategy {
    Object getContent(ReqDTO reqDTO);
}

@Service
public class HomePageV1Strategy implements HomePageStrategy {
    @Override
    public Object getContent(ReqDTO reqDTO) {
        return new HashMap<>();
    }
}

@Service
public class HomePageV2Strategy implements HomePageStrategy {
    @Override
    public Object getContent(ReqDTO reqDTO) {
        return new ArrayList<>();
    }
}

@Service
public class HomePageV3Strategy implements HomePageStrategy {
    @Override
    public Object getContent(ReqDTO reqDTO) {
        return "v3版本";
    }
}

//注入容器,供controller层调用,并且屏蔽底层的策略细节
@Service
public static class HomePageService implements HomePageStrategy {

    @Resource
    private HomePageV1Strategy homePageV1Strategy;

    @Resource
    private HomePageV2Strategy homePageV2Strategy;

    @Resource
    private HomePageV3Strategy homePageV3Strategy;

    private HomePageStrategy getStrategy(ReqDTO reqDTO) {
        //根据前端参数,返回一个合适的策略
        return null;
    }

    @Override
    public Object getContent(ReqDTO reqDTO) {
        return this.getStrategy(reqDTO).getContent(reqDTO);
    }
}

五、总结

工作中,我用到的设计模式也就4种,再次进行一次啰嗦的总结:

模式相关内容与要点
模板基于继承,允许通过扩展子类中的部分内容来改变部分算法,通常是相同的步骤+顺序(顺序固定不可调整,也是区别于责任链的关键),但子类实现略有不同,更偏重于执行步骤的划分;
状态区分行为与状态,某一个行为导致某一个状态,关键在于画出状态流转图;
责任链①节点个数的可选择性,以及可排序性;②各个节点地位是平等的,内容是固化的(区别于模板模式),强调节点的自由组合,任意调整顺序;③分为有状态的和无状态的,关键在于是否有返回值;④模板的子类和子类是同等地位,互相没有任何关联,这点和责任链类似
策略①不同方式完成特定的任务,各个策略子类是平级的,无调用关系;首先要意识到一个问题,这个问题可以有不同的解法,那么适用于策略模式;②模板要求的是要有公共部分,但是策略模式不要求有公共部分,也就是,可以没有抽象类
  • 相同点:客户端本质上都是找到一个对象的实现类并进行调用,一般都是通过一个参数来进行判断,比如状态,版本,或者某一个可以起特殊意义的字段,类似于创建一个门面类来隐藏底层的实现细节;
  • 设计模式,本质上都是用来解决if-else等大量的命令式编程的一种方式,使得代码更加对象化,更加解耦,易于维护;
  • 我认为设计模式应该用来解决场景化的问题,比如这个业务场景,使用模板好一些,易于理解且易于维护,针对的是一个方向性或者说一个架构层面;但是设计模式不应该用来实现一些简单且很细节的逻辑,因为这些逻辑使用if-else可以更加清晰,而且团队整体水平更容易把控,不应该为了模式而模式。