基于Spring的枚举类+策略模式设计(以实现多种第三方支付功能为例)

摘要

最近阅读《贯彻设计模式》这本书,里面使用一个更真实的项目来介绍设计模式的使用,相较于其它那些只会以披萨、厨师为例的设计模式书籍是有些进步。但这书有时候为了使用设计模式而强行朝着对应的 UML 图来设计类结构,并且对设计理念缺少讲解,所以也不能说有多优秀,79分的水平。

书中就这部分内容设计,提到使用了:策略模式、门面模式、策略工厂模式、享元模式。但可能真正称得上是设计的内容就两个部分,策略模式和策略工厂模式。但是就书中所写的策略工厂,个人认为有些啰嗦,并且指定全类名,通过反射来获取对象,这种实现不够优雅。个人相信的设计理念就是在实现代码可扩展的前提下,尽可能使用少的类,只开放必要的接口。虽然 Spring 获取 Bean 本质上也是通过反射来创建的,效率并没有提高。但本文设计并不依赖具体框架,基于 Spring 的目的也是和该书一样,为了让案例更接近现实,基于 Spring 既是一种便利,也是一种约束。

本文的设计方案大体总结如下:

  1. 具体的支付策略实现类(支付宝支付、微信支付)和支付策略门面(Facade)共同实现支付接口
  2. 通过枚举类定义支付策略,并实现编号到实现类的映射。由于实现类是交给 Spring 管理,所以只需要实现编号到 beanName 的映射
  3. 在 Facade 中使用 Map 来实现从编号到实现类对象的映射,根据 Bean 的生命周期,在初始化过程中为 Map 赋值。之所以使用 @PostConstruct 注解,是为了让 init 方法作为 private 方法,而 Facade 只需要暴露上层服务真正需要调用的接口方法就行。

基础环境

pom 依赖

xml 复制代码
<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>4.34.0.ALL</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.7.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <version>2.7.4</version>
</dependency>

常量工具类

java 复制代码
/**
 * 根据阿里开放平台给出的文档,字段为为第三方平台要求
 */
public class AliPayConstant {
    public static final String OUT_TRADE_NO = "out_trade_no";
    public static final String TOTAL_AMOUNT = "total_amount";
    public static final String SUBJECT = "subject";
    public static final String PRODUCT_CODE = "product_code";
    public static final String FAST_INSTANT_TRADE_PAY = "FAST_INSTANT_TRADE_PAY";
}

实体类

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@ToString(callSuper = true)
@EqualsAndHashCode
@Builder
public class Order {

    /**
     * 订单编号
     */
    private String orderNo;

    /**
     * 订单金额
     */
    private Double payment;

    /**
     * 订单标题
     */
    private String orderTitle;
}

Spring 配置文件、配置属性类和配置类

yaml 复制代码
payout:
  alibaba:
    # 沙箱环境的支付宝网关接口
    url: https://openapi-sandbox.dl.alipaydev.com/gateway.do
    app-id:
    private-key:
    alipay-public-key:
java 复制代码
@ConfigurationProperties("payout.alibaba")
@Data
public class AlipayProperties {
    private String url;
    private String appId;
    private String privateKey;
    private String alipayPublicKey;

    // 默认值
    private String format = "json";
    private String charset = "UTF-8";
    private String signType = "RSA2";
}
java 复制代码
@Configuration
@EnableConfigurationProperties(AlipayProperties.class)
public class AlipayConfiguration {

    @Bean
    public AlipayClient alipayClient(AlipayProperties alipayProperties) throws AlipayApiException {
        AlipayConfig alipayConfig = new AlipayConfig();
        //设置网关地址
        alipayConfig.setServerUrl(alipayProperties.getUrl());
        //设置应用ID
        alipayConfig.setAppId(alipayProperties.getAppId());
        //设置应用私钥
        alipayConfig.setPrivateKey(alipayProperties.getPrivateKey());
        //设置请求格式,固定值json
        alipayConfig.setFormat(alipayProperties.getFormat());
        //设置字符集
        alipayConfig.setCharset(alipayProperties.getCharset());
        //设置签名类型
        alipayConfig.setSignType(alipayProperties.getSignType());
        //设置支付宝公钥
        alipayConfig.setAlipayPublicKey(alipayProperties.getAlipayPublicKey());
        //实例化客户端
        return new DefaultAlipayClient(alipayConfig);
    }

}

Controller 层

java 复制代码
/**
 * 支付接口,由于要回显html页面,因此直接使用@Controller接口
 */
@Controller
@RequestMapping("/payout")
@Slf4j
public class PayoutController {

    @Autowired
    private PayoutService payoutService;


    @SneakyThrows
    @GetMapping("/{payType}")
    public void payout(HttpServletResponse response,
                       @PathVariable Integer payType) {

        log.info("支付方式:{}", payType);

        Order order = Order.builder()
                .orderNo(UUID.randomUUID().toString())
                .payment(900.0)
                .orderTitle("兰博基尼")
                .build();

        String payPageForm = payoutService.pay(order, payType);

        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write(payPageForm);
        out.flush();
        out.close();
        log.info("显示支付页面");
    }

    @SneakyThrows
    @GetMapping("/callback")
    public void callback(HttpServletResponse response) {
        log.info("回调页面");
        PrintWriter out = response.getWriter();
        out.write("Hello World");
        out.flush();
        out.close();
        log.info("写出消息");
    }
}

Service 层

java 复制代码
@Service
public class PayoutService {
    @Autowired
    private PayStrategyFacade payStrategyFacade;

    public String pay(Order order, Integer payType) {
        return payStrategyFacade.pay(order, payType);
    }
}

设计模式部分

策略接口

java 复制代码
public interface PayStrategy {

    /**
     * 调用第三方支付接口
     *
     * @param order 订单封装
     * @return 调用成功返回页面信息,即response.getBody();调用失败返回null
     */
    String pay(Order order);
}

策略实现类(支付宝支付、微信支付、银行支付等)

复制支付宝、微信等开放平台的代码内容即可

java 复制代码
@Component("aliPay")
public class AliPayStrategyImpl implements PayStrategy {

    @Autowired
    private AlipayClient alipayClient;

    @Override
    public String pay(Order order) {
        // 支付金额
        double payAmount = order.getPayment();
        // 订单标题
        String orderTitle = order.getOrderTitle();
        // 商户订单号
        String orderNo = order.getOrderNo();

        // 不同的请求类型构造不同的Request对象
        AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
        request.setNotifyUrl("");
        request.setReturnUrl("http://localhost:8080/payout/callback");

        JSONObject bizContent = new JSONObject();
        // 必传参数

        // 商户订单号,商家自定义,保持唯一性
        bizContent.put(AliPayConstant.OUT_TRADE_NO, orderNo);
        // 支付金额,最小值0 .01 元
        bizContent.put(AliPayConstant.TOTAL_AMOUNT, payAmount);
        // 订单标题,不可使用特殊符号
        bizContent.put(AliPayConstant.SUBJECT, orderTitle);
        // 电脑网站支付场景固定传值FAST_INSTANT_TRADE_PAY
        bizContent.put(AliPayConstant.PRODUCT_CODE, AliPayConstant.FAST_INSTANT_TRADE_PAY);

        request.setBizContent(bizContent.toString());
        AlipayTradePagePayResponse response;
        try {
            response = alipayClient.pageExecute(request);
        } catch (AlipayApiException e) {
            throw new RuntimeException(e);
        }

        if (response.isSuccess()) {
            // 在网站上显示支付宝支付页面,让用户扫描支付
            return response.getBody();
        }

        return null;
    }
}

策略枚举类

枚举所有的策略,由于枚举类对象是静态对象,因此不能够将其直接作为 Spring 容器的 Bean。

最开始枚举类中设计的两个字段分别是 int 类型的 payType 和 PayStrategy 类型的对象,希望通过 ALIBABA(0, new AliPayStrategyImpl()) 的方式来初始化枚举类对象。但是由于 AliPayStrategyImpl 中的 AliClient 是交给了 Spring 容器进行管理,而使用 new 方式得到的对象没有经过 Spring,所以其中的 AliClient 为 null。

同时即使将枚举类交给 Spring 管理,其依赖注入也十分麻烦,通过初始化方法去覆盖类对象中的属性,也需要依赖 beanName,且设计丑陋,没有枚举类的优雅。因此直接在枚举类中负责管理 beanName**(在 Spring 框架中,管理了 beanName,就是管理了 BeanDefinition)**

java 复制代码
@Getter
public enum PayStrategyEnum {
    // TODO: 由于实现类依赖了Spring的自动注入来获取AliClient, 因此直接new AlipayStrategyImpl()不会自动注入AliClient
    ALIBABA(0, "aliPay"),
    WECHAT(1, "wechatPay"),
    ;

    private final int payType;

    /**
     * 枚举类结合Spring的中介产物,根据迪米特法则,不需要对外开放
     */
    private final String beanName;


    PayStrategyEnum(Integer payType, String beanName) {
        this.payType = payType;
        this.beanName = beanName;
    }
}

策略门面?策略上下文?策略工厂?

java 复制代码
@Component
public class PayStrategyFacade {
    @Autowired
    private ApplicationContext applicationContext;

    // 由于只在初始化的时候进行设置,并发读不存在线程安全问题,因此不需要使用ConcurrentHashMap
    private static final Map<Integer, PayStrategy> PAY_STRATEGIES = new HashMap<>(PayStrategyEnum.values().length);


    @PostConstruct
    private void init() {
        // 初始化策略
        for (PayStrategyEnum payStrategyEnum : PayStrategyEnum.values()) {
            PAY_STRATEGIES.put(payStrategyEnum.getPayType(),
                    applicationContext.getBean(payStrategyEnum.getBeanName(), PayStrategy.class));
        }
    }


    public String pay(Order order, Integer payType) {
        PayStrategy payStrategy = PAY_STRATEGIES.getOrDefault(payType, null);
        if (payStrategy == null) {
            throw new RuntimeException("不支持的支付类型");
        }
        return payStrategy.pay(order);
    }
}
相关推荐
武子康7 分钟前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
豪宇刘1 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意1 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
FF在路上2 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言
真的很上进2 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
众拾达人3 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
皓木.3 小时前
Mybatis-Plus
java·开发语言
不良人天码星3 小时前
lombok插件不生效
java·开发语言·intellij-idea
守护者1703 小时前
JAVA学习-练习试用Java实现“使用Arrays.toString方法将数组转换为字符串并打印出来”
java·学习
源码哥_博纳软云3 小时前
JAVA同城服务场馆门店预约系统支持H5小程序APP源码
java·开发语言·微信小程序·小程序·微信公众平台