前端设计模式在SpringBoot中的应用

背景

最近研究大佬代码,发现了一个贼特么有意思的前端设计模式实现,在这里记录一下

项目结构

  • 数据存储:可适配各种存储,redis,hbase,hive,mysql,tidb,mongo等,无所谓
  • 数据访问:上述技术栈对应的数据访问
  • 服务:常见的Service
  • 控制器:常见的controller

如何体现前端设计模式

众所周知,前端设计模式指的是用一个统一的接口收纳请求,再根据不同的条件分发给不同的服务实现。
为什么要采用前端模式?因为如果只是普通的注入服务,就容易出现这么一种情况:

  1. 重复开发各种VO用来封装返回值
  2. 重复开发分页相关的功能
  3. 服务数量大量膨胀导致服务管理性下降

而采用前端设计模式,我们可以通过统一封装VO,统一封装分页,统一服务的基本返回类型,来实现可控扩容。更妙的是,如果我们想要隐藏真实的服务提供类或者实现,并且实现分层校验,这种方式也能很好的满足需要。

案例

假如存在服务 BussinesService 接口,而对于不同部门的不同bussines有不同的实现,我们就可以这么玩:

基准服务接口

import com.xu.util.PageUtil;
import java.util.Map;

/**
 * 基准服务
 *
 * @author yanyu
 */
public interface BussinesService {

    /**
     * 所有服务的顶级接口
     *
     * @param params 参数列表
     * @return
     */
    PageUtil generateRes(Map<String, String> params);

    /**
     * 获取其服务唯一ID
     *
     * @return
     */
    String getServiceId();
}

我们用serviceId来唯一标记一个服务,用PageUtil来统一封装返回值,用params来统一封装所有入参。这样所有子服务都实现这个顶层服务。

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * @Desc
 * @Author bafan
 * @Date 2022/10/13 16:16
 **/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PageUtil<T> {
	// 数据总量
    private long total;
    private int currentPage;
    private int currentSize;
    // 限定返回总量(可无)
    private int limit;
    // 数据实体
    private List<T> list;
    // 服务说明
    private String desc;
	// 非分页数据
	private T data;
}

那就有同学要问了,这样服务注入的时候不还是好麻烦么,有啥用啊?

服务统一加载

import com.xu.service.BussinesService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @Desc
 * @Author xxx
 **/
@Configuration
public class BussinesServiceConfig {

    private Map<String, BussinesService> allService;

    @Autowired
    public void setAllService(List<BussinesService> bussinesServices) {
        allService = new HashMap<>(bussinesServices.size());
        bussinesServices.forEach(e -> allService.put(e.getServiceId(), e));
    }

    @Bean
    public Map<String, BussinesService> allService() {
        return this.allService;
    }
}

我们写一个config类,用于项目启动时一次性把可能用到的服务都加载到map里,实际上就是简化的IOC。当我们需要使用某个服务时,只需要解析调用方的参数,从map拿数据即可。
那又有人要问,为什么要用Map把请求参数也统一起来呢?
一是形式上方便检查,二是可以预留动态性。
预留动态性,是说万一以后接口的参数要做成可编辑可配置的动态参数表,那用Map就方便很多,而且可以通过在数据库里保存接口参数的相关信息实现动态校验,岂不美哉?

参数的基本检验

为了检测的方便,我们预先定义一下必须 / 容易出错的字段:

/**
 * @Desc
 * @Author yanyu
 **/
public class KeyPropertiesConfig {

    public static final String SERVICE_ID = "serviceId";
    public static final String PAGE = "page";
    public static final String SIZE = "size";
    public static final String LIMIT = "limit";
}

也不做的太麻烦,这里就是展示一下把参数检验剥离出服务流程的方式:

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @Desc 参数异常枚举
 * @Author yanyu
 **/
@Getter
@AllArgsConstructor
public enum ParamErrorEnum {
    /**
     * 正常
     */
    CORRECT(0, "正常"),
    /**
     * 缺少服务ID
     */
    WITHOUT_SERVICE_ID (-1, "缺少服务ID"),
    /**
     * 页码或每页条数数值有误
     */
    PAGE_SIZE_NUM_ERROR(-2, "页码或每页条数数值有误"),
    /**
     * 页码缺失
     */
    PAGE_LACK_ERROR(-3, "页码缺失"),
    /**
     * 条数缺失
     */
    SIZE_LACK_ERROR(-4, "条数缺失"),
    /**
     * 限定数值错误
     */
    LIMIT_NUM_ERROR(-5, "限定数值错误"),
    OTHERS(-6, "无效检查");

    private final int code;
    private final String desc;


    public static ParamErrorEnum valueOf(Integer code) {
        if (code == null) {
            return ParamErrorEnum.OTHERS;
        }
        for (ParamErrorEnum value : ParamErrorEnum.values()) {
            if (code == value.code) {
                return value;
            }
        }
        return ParamErrorEnum.OTHERS;
    }
}

以及实际工具类:

import com.xu.config.KeyPropertiesConfig;
import com.xu.enums.ParamErrorEnum;

import java.util.Map;

/**
 * @Desc
 * @Author yanyu
 **/
public class ParamValidator {

    public static ParamErrorEnum paramCheck(Map<String, String> param) {
        // 缺少必要字段
        if (!param.containsKey(KeyPropertiesConfig.SERVICE_ID)) {
            return ParamErrorEnum.WITHOUT_SERVICE_ID;
        }
        // 页码 / 每页条数数字异常
        if (param.containsKey(KeyPropertiesConfig.PAGE) && param.containsKey(KeyPropertiesConfig.SIZE)) {
            int page = Integer.parseInt(param.get(KeyPropertiesConfig.PAGE));
            int size = Integer.parseInt(param.get(KeyPropertiesConfig.SIZE));
            if (page < 1 || size < 0) {
                return ParamErrorEnum.PAGE_SIZE_NUM_ERROR;
            }
        }
        // 只有页码但未标明每页条数
        if (param.containsKey(KeyPropertiesConfig.PAGE) && !param.containsKey(KeyPropertiesConfig.SIZE)) {
            return ParamErrorEnum.SIZE_LACK_ERROR;
        }
        // 只有每页条数但未标明页码
        if (param.containsKey(KeyPropertiesConfig.SIZE) && !param.containsKey(KeyPropertiesConfig.PAGE)) {
            return ParamErrorEnum.PAGE_LACK_ERROR;
        }
        // 限定值比1小没有实际意义
        if (param.containsKey(KeyPropertiesConfig.LIMIT)) {
            int limit = Integer.parseInt(param.get(KeyPropertiesConfig.LIMIT));
            if (limit < 1) {
                return ParamErrorEnum.LIMIT_NUM_ERROR;
            }
        }
        return ParamErrorEnum.CORRECT;
    }
}

包装具体服务

为了能有一些中间处理过程,我们把具体服务的中间过程也封装起来:

import com.xu.enums.ParamErrorEnum;
import com.xu.util.PageUtil;
import com.xu.util.ParamValidator;
import org.springframework.stereotype.Service;

import java.util.Map;
import javax.annotation.Resource;

/**
 * @Desc
 * @Author yanyu
 **/
@Service
public class ResultService {

    @Resource
    private Map<String, BussinesService> allService;

    public PageUtil getRes(Map<String, String> params) {
        ParamErrorEnum paramCheck = ParamValidator.paramCheck(params);
        if (paramCheck.getCode() < 0) {
            return PageUtil.builder().desc(String.format("参数检定错误,原因为:%s", paramCheck.getDesc())).build();
        }
        return allService.get(params.get(KeyPropertiesConfig.SERVICE_ID)).generateRes(params)
    }
}

最终的实际服务类

例如有两个部门A和B

我们分别定义A和B的所有服务

@Getter
@AllArgsConstructor
public enum DepartmentA {
    /**
     * 所有服务定义
     */
    ACCOUNT_SERVICE("accountService", "账户服务"),
    LOGIN_SERVICE("loginService", "登陆服务"),
    UNKNOWN("unknown", "未知服务")
    ;

    private final String code;
    private final String desc;
}


@Getter
@AllArgsConstructor
public enum DepartmentB {

    /**
     * 所有服务定义
     */
    EMAIL_SERVICE("emailService", "邮箱服务"),
    PAY_SERVICE("payService", "付款服务"),
    UNKNOWN("unknown", "未知服务")
    ;

    private final String code;
    private final String desc;
}

然后开始实现

@Service
public class DepartmentAServiceAccount implements BussinesService {

    @Override
    public PageUtil generateRes(Map<String, String> params) {
    	// 服务过程开始
		
		// 服务过程结束
        return PageUtil.builder().build();
    }

    @Override
    public String getServiceId() {
        return DepartmentA.ACCOUNT_SERVICE.getCode();
    }
}

public class DepartmentBServiceLogin implements BussinesService {

    @Override
    public PageUtil generateRes(Map<String, String> params) {
        return null;
    }

    @Override
    public String getServiceId() {
        return DepartmentA.LOGIN_SERVICE.getCode();
    }
}

// B服务也类似

这样一来,我们搭建起了以顶层接口为模板的统一服务架构

怎么样,是不是有一种很优雅很协调的快感?

控制器

最终我们就会看到,所有服务均统一在一个接口中:

@RestController
@RequestMapping("bussines")
public class BussinesController {

    @Resource
    private ResultService resultService;

    @PostMapping("/realService")
    private ApiReturn<PageUtil> useBussinesService(@RequestBody Map<String, String> params) {
        return new ApiReturn<>(resultService.getRes(params));
    }
}

极致的简单,极致的享受

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值