模版模式+策略模式重构订单

模版模式+策略模式重构订单

1、SOLID设计原则

S:Single Responsibility Principle(单一职责)

一个类只负责一个功能领域中的相应职责。

O:Open Closed Principle(开闭原则)

对扩展开放,对修改关闭。

L:Liskov Substitution Principle(里氏替换)

所有引用基类的地方必须能透明地使用其子类的对象。

I:Interface Segregation Principle(接口隔离)

类之间的依赖关系应该建立在最小的接口上。

D:Dependency Inversion Principle(依赖反转)

依赖于抽象,不能依赖于具体实现。

还有其他的一些设计原则,例如:DRY,KISS等。设计原则是衡量代码好坏的一把尺子,设计模式只是围绕着设计原则总结出来的一些常用套路。

下面针对单一职责和开闭原则进行单独的说明。

1.1、Single Responsibility Principle

单一职责可以指在一个类,或者一个模块中,只包含单一的职责。

从模块层面上来看,当前我们的微服务包含底层的业务服务和上层的聚合服务。例如:在查询预约挂号订单详情时,需要返回医生的头像,职称等信息。预约挂号订单信息当中,不会冗余医生头像这些信息,所以在进行查询的时候,我们可以有两种方式:

  • appointmentOrderDao.getOrderInfoAndDoctorInfo(),直接在预约挂号订单的Dao层查出订单信息和医生信息。显然这种违背了我们微服务拆分的原则和单一职责原则,预约挂号模块只需要关注自己模块的操作,不需要关注医生信息,只需要知道预约挂号订单当中的医生的doctorId和doctorName即可。
  • 在medical聚合层分别调用transaction和user,查询出预约挂号订单信息和医生信息,并且组合出参返回给前端,这种做法是我们比较推荐的。

当然,有时候单一职责并不是这么好区分的。例如下面的一个类,我们分析下是否符合单一职责原则。

public class UserInfo {
  private long userId;
  private String username;
  private String email;
  private String telephone;
  private long createTime;
  private long lastLoginTime;
  private String avatarUrl;
  private String provinceOfAddress; // 省
  private String cityOfAddress; // 市
  private String regionOfAddress; // 区 
  private String detailedAddress; // 详细地址
  ...
}

对于这个问题,有两种不同的观点。

  1. UserInfo 类包含的都是跟用户相关的信息,所有的属性和方法都隶属于用户这样一个业务模型,满足单一职责原则;
  2. 地址信息在 UserInfo 类中,所占的比重比较高,可以继续拆分成独立的 UserAddress 类,UserInfo 只保留除 Address 之外的其他信息,拆分之后的两个类的职责更加单一。

实际上,要从中做出选择,我们不能脱离具体的应用场景。

如果在这个社交产品中,用户的地址信息跟其他信息一样,只是单纯地用来展示,那 UserInfo 现在的设计就是合理的。但是,如果添加了电商的模块,用户的地址信息还会用在电商物流中,那我们最好将地址信息从 UserInfo 中拆分出来,独立成用户物流信息(或者叫地址信息、收货信息等)。

所以,不同的业务场景,单一职责原则下的类或模块的划分都不一定一样。

1.2、Open Closed Principle

软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。这个描述比较简略,如果我们详细表述一下,添加一个新的功能应该是在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

单看这个理论,确实不太好理解,下面我们通过创建订单的各种校验这个例子来体会下。

1.2.1、不符合“开闭原则”的代码示例
public class OrderService {

    public static void main(String[] args) {
        OrderService orderService = new OrderService();
        orderService.createOrder3(new CreateOrderReq());
    }

    public Integer createOrder1(Long userId, Map<String, Object> orderInfo) {
        // 校验
        OpposeChecker.check(userId, (Long) orderInfo.get("skuId"));
        // 创建订单
       return this.doCreateOrder1(userId, orderInfo);
    }

    public Integer createOrder2(Long userId, Map<String, Object> orderInfo) {
        // 改动1
        // 如果这个方法被多个地方调用,那么多个地方都需要修改
        OpposeChecker.check(userId, (Long) orderInfo.get("skuId"), (Long) orderInfo.get("areaId"));
        // 创建订单
        return this.doCreateOrder1(userId, orderInfo);
    }

    public Integer createOrder3(CreateOrderReq createOrderReq){
        CheckerFactory.getInstance().check(createOrderReq);
        return doCreateOrder2(createOrderReq);
    }

    private Integer doCreateOrder2(CreateOrderReq createOrderReq) {
        System.out.println("创建订单成功");
        return 1;
    }

    private Integer doCreateOrder1(Long userId, Map<String, Object> orderInfo) {
        System.out.println("创建订单成功");
        return 1;
    }
}

public class OpposeChecker {
    public static Boolean check(Long userId, Long skuId) {
        // 校验黑名单
        System.out.println("校验用户黑名单, userId = " + userId);
        // 校验库存
        System.out.println("校验库存, skuId = " + skuId);
        return true;
    }

    // 改动2
    // 新增商品配送地区校验
    public static Boolean check(Long userId, Long skuId, Long areaId) {
        // 校验黑名单
        System.out.println("校验用户黑名单, userId = " + userId);
        // 校验库存
        System.out.println("校验库存, skuId = " + skuId);
        // 改动3
        // 校验配送地址
        System.out.println("校验库存, areaId = " + areaId);
        return true;
    }
}

显然上面的代码就是大家口中的垃圾代码,新增一种校验逻辑时,有三个地方做了改动:

  1. OpposeChecker#check()新增了入参,如果这个方法只是某个业务使用,影响范围还比较小,如果是公共的,那其他业务线的都得进行改动;
  2. OpposeChecker#check()方法内部逻辑进行了改动,需要重新进行单元测试;
  3. OrderService#createOrder2进行了入参修改。

试想,如果我们后面有更加多的业务校验,是否都需要进行上述的三个改动。这样check函数将会越来越庞大,可读性极差。这个只是举例单纯的校验,可能直接在最后添加一个方法就行了,如果是复杂的业务逻辑也这样去修改,最后可能会导致没有人看得懂这个代码,也没有人敢改这个代码。

1.2.2、符合“开闭原则”的代码示例
public class CheckerFactory {
    private static RecommendChecker checker = new RecommendChecker();
    static {
        checker.addCheckStrategy(new StockCheckHandler());
        checker.addCheckStrategy(new BlackListCheckHandler());
        // TODO 改动1
        checker.addCheckStrategy(new AreaCheckHandler());
    }
    private CheckerFactory() {}
    public static RecommendChecker getInstance(){
        return checker;
    }
}

public class CreateOrderReq {
    private Long userId;
    private Long skuId;
    // TODO 改动2,新增属性
    private Long areaId;
		// ...setter,getter
}

public interface CheckCreateOrderStrategy {
    Boolean checkCreateOrder(CreateOrderReq createOrderReq);
}

public class BlackListCheckHandler implements CheckCreateOrderStrategy {
    @Override
    public Boolean checkCreateOrder(CreateOrderReq createOrderReq) {
        System.out.println("黑名单校验");
        return true;
    }
}

public class StockCheckHandler implements CheckCreateOrderStrategy {

    @Override
    public Boolean checkCreateOrder(CreateOrderReq createOrderReq) {
        System.out.println("库存校验");
        return true;
    }
}

// TODO 改动3,新增一个配送地校验类
public class AreaCheckHandler implements CheckCreateOrderStrategy {
    @Override
    public Boolean checkCreateOrder(CreateOrderReq createOrderReq) {
        System.out.println("配送地校验");
        return true;
    }
}

public class RecommendChecker {

    List<CheckCreateOrderStrategy> checkStrategyList = new ArrayList<>();

    public void addCheckStrategy(CheckCreateOrderStrategy checkCreateOrderStrategy) {
        checkStrategyList.add(checkCreateOrderStrategy);
    }

    public Boolean check(CreateOrderReq createOrderReq) {
        for (CheckCreateOrderStrategy strategy : checkStrategyList) {
            if (!strategy.checkCreateOrder(createOrderReq)) {
                return false;
            }
        }
        return true;
    }
}

当前的这一版代码,改动点同样有三个:

  1. CheckerFactory中需要新增一个校验策略类实现;
  2. CreateOrderReq中新增一个配送地属性;
  3. 新增一个配送类校验处理类AreaCheckHandler。

虽然同样是有三个改动点,但是对比上一版代码,扩展性和新增代码的可测试性提升了很多。如果后续需要再添加新的校验规则,我们新增策略类和属性即可,不需要改动原有的check()方法。如果我们直接使用Spring按照类型注入,CheckerFactory都不需要修改,这点在预约挂号订单的重构中可以看到使用。

当然,有的人可能会说CreateOrderReq中新增了属性,改动了代码,那也违法了“开闭原则”。

其实这个问题也需要从不同的维度去看待,如果我们从类的维度去看,当前的修改确实是违法了“开闭原则”,但是从方法的维度看,我们并没有去修改原有属性的方法,所以可以看作是新增。

想要完全不修改代码是不可能的,所以,我们可以尽量通过新增的方式去“扩展”原有的代码逻辑,不修改原有方法和属性的方式去“闭合”改动。这样,我们代码的扩展性和可测试性都会提高很多。

2、模版方法模式

模板方法模式在一个方法中定义算法骨架,并将某些步骤推迟到子类中实现。

模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。

这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。

模版方法的两大作用:复用和扩展。

在这里插入图片描述

2.1、复用

在Java中,常见的代码复用方式有继承、组合、代理。

模板模式把一个算法中不变的流程抽象到父类的模板方法 templateMethod() 中,将可变的部分 abstractMethod1()、abstractMethod2() 留给子类 ContreteClass1 和 ContreteClass2 来实现。

所有的子类都可以复用父类中模板方法定义的流程代码。

2.2、扩展

模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。

例如Spring在初始化Bean的过程中的一些扩展点,HttpServlet等。

  • ApplicationListener
  • InitializingBean
  • BeanPostProcessor
  • BeanFactoryPostProcessor
  • BeanDefinitionRegistryPostProcessor
  • Aware

2.3、源码中的模版方法模式

  • HttpServlet#service(ServletRequest req, ServletResponse res)
  • AbstractApplicationContext#refresh()

2.4、使用回调替代模版方法模式

回调同样可以实现模版方法模式的功能,并且在某些情况下,比模版方法模式更加简洁。

相对于普通的函数调用来说,回调是一种双向调用关系。A 类事先注册某个函数 F 到 B 类,A 类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是“回调函数”。A 调用 B,B 反过来又调用 A,这种调用机制就叫作“回调”。

A B B.P(F()) A.F() A B

回调不仅可以应用在代码设计上,在架构设计上也经常使用。比如,通过三方支付系统来实现支付功能,用户在发起支付请求,三方支付系统执行完成之后,将结果通过回调接口返回给用户,这种属于异步回调。

代码实例:AbstractBeanFactory#doGetBean()

2.5、回调 VS 模版方法模式

应用场景:同步回调跟模板模式几乎一致。它们都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。而异步回调跟模板模式有较大差别,更像是观察者模式。

代码实现:回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。

组合优于继承。在代码实现上,回调相对于模板模式会更加灵活,主要体现在下面几点。

  1. Java 只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。
  2. 回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。
  3. 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。

2.6、应用场景

  • 先实现算法的不变部分,然后再将可变的操作留给子类实现,动静分离。
  • 提取一些公共的操作到父类以避免重复。

3、策略模式

定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端。

策略模式包含如下角色:

  • Context: 环境类
  • Strategy: 抽象策略类
  • ConcreteStrategy: 具体策略类

在这里插入图片描述

3.1、策略的定义

策略往往被定义为一个接口,这样客户端可以针对接口编程,而非针对实现编程。

3.2、策略的创建

策略模式会包含一组策略,在使用它们的时候,一般会通过类型(type)来判断创建哪个策略来使用。

3.3、策略的使用

  • 运行时动态指定:根据用户配置,计算结果,或者业务类型进行动态选择。
  • 静态指定:在使用时直接指定策略。
// 策略接口:EvictionStrategy
// 策略类:LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy...
// 策略工厂:EvictionStrategyFactory

public class UserCache {
  private Map<String, User> cacheData = new HashMap<>();
  private EvictionStrategy eviction;

  public UserCache(EvictionStrategy eviction) {
    this.eviction = eviction;
  }

  //...
}

// 运行时动态确定,根据配置文件的配置决定使用哪种策略
public class Application {
  public static void main(String[] args) throws Exception {
    EvictionStrategy evictionStrategy = null;
    Properties props = new Properties();
    props.load(new FileInputStream("./config.properties"));
    String type = props.getProperty("eviction_type");
    evictionStrategy = EvictionStrategyFactory.getEvictionStrategy(type);
    UserCache userCache = new UserCache(evictionStrategy);
    //...
  }
}

// 非运行时动态确定,在代码中指定使用哪种策略
public class Application {
  public static void main(String[] args) {
    //...
    EvictionStrategy evictionStrategy = new LruEvictionStrategy();
    UserCache userCache = new UserCache(evictionStrategy);
    //...
  }
}

3.4、应用场景

  • 对于同一项任务,有多种实现方式,需要针对不同的场景使用不同的策略。
  • 避免代码中冗长的if else。

ps:以上内容参照极客时间《设计模式之美》。

4、预约挂号订单重构

预约挂号订单当前包含策略类型:自建渠道号源策略、160对接号源策略。

同时两种策略包含许多相同操作,例如:查询就诊人信息、就诊卡信息、公共参数的组装等。

基于上述的一些业务特征,可以考虑使用模版方法模式+策略模式实现预约挂号下单逻辑。

4.1、UML

  • AppointmentStrategyContext:预约挂号策略上下文
  • AppointmentStrategy:预约挂号策略接口
  • AbstractAppointmentMedicalService:预约挂号抽象父类
  • ChannelAppointmentMedicalService:第三方渠道预约挂号实现
  • SelfAppointmentMedicalService:自建渠道预约挂号实现
    在这里插入图片描述

4.2、code review

参照类图或联系博主,掌握思想即可。

4.3、扩展

当前系统中,包含预约挂号、咨询问诊、团队订单、春雨医生等多种业务订单,后续可考虑使用相同方法进行重构。

4.3.1、创建订单

各个业务创建订单时,都会操作基础订单表,订单状态改变记录表分别插入基础订单表和订单状态变更记录,需要支付的订单还需要发送超时支付取消订单MQ消息。这种公共的操作,可以提取到抽象父类中,创建各个业务子订单的操作,可以定义为抽象方法,让各个子业务分别实现。这样就可以做到代码的复用,同时将变化的业务订单操作隔离,修改子业务订单代码时,不会影响基础订单的代码。

4.3.2、取消订单

取消订单时,同样会操作基础订单表,订单状态改变记录表这两张表,如果有支付的订单,还需要进行退款操作,同时在退款单表中生成订单退单记录。在这过程中,还会有订单状态的合法性及幂等性校验。这些公共的操作,抽取到抽象父类,各个子业务的操作在各自的实现中进行操作。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值