设计模式实战

使用设计模式消除if/else

    • 设计模式概述
    • 业务场景
    • 场景拆分
    • 问题产生
    • 如何解决

设计模式概述

设计模式其实是一个比较抽象的东西,它就像武侠小说里的武功秘笈,比如单例模式是一阳指,策略模式是独孤九剑,模板方法模式是无相神功……但即使我们对这些武功秘籍倒背如流,在实战的时候往往还是被一拳擂倒,那为什么在人人如龙的时代(武功秘笈不要钱),有的人降龙十八掌就能打出龙,有的人打出虫呢,下面我们根据一个实例一步步去掌握这些武功秘笈的诀窍

业务场景

手上接到一个项目,需要有动态表功能,什么意思呢,正常比如我们有一个user表,可能有name,age,phoneNum等字段,这些字段都是我们在软件创建初期就设计好的,在编码里面有个User类与表一一映射,一旦我们的表需要新增一个字段sex,那我们需要修改表结构,并修改代码。而我们现在的需求直接在管理界面动态修改表的字段,这样无需修改表和代码就能做到表字段的动态扩展,甚至我们能动态扩展新的表

场景拆分

我们需要实现上述功能,肯定需要定义表的属性,字段,字段的类型,长度,是否唯一,是否是可编辑,是否是展示字段,是否是搜索字段,是否是排序字段,是否允许空,是否是导入字段等等,表定义比较简单,但是表定义的数据怎么存储呢?如果按字段key,value等方式存储,查询时我们要列转行,分页排序等功能就非常麻烦了,由于我们的字段是动态的,所以数据是可变的,这种格式是不是很熟悉,json,没错,对nosql比较熟悉的人,脑子里肯定已经跳出来,Bson->mongodb,所以我们最终决定固定数据存储mysql,动态数据存储mongodb

问题产生

一直到这里其实还没讲到和设计模式有半毛钱关系,客观莫急,听我慢慢道来,mysql数据库字段属性固定的属性,比如是否唯一,是否为空,字段类型等等,他帮我们做了很多的校验,但是我们现在属性是动态的,字段就需要我们去做校验了,比如我现在要新增一条user记录,我需要做哪些内容呢?

{
    "name": "张芷",
    "age": "17",
    "tableName": "user"
}

上述内容为新增一条记录的前端传过来的数据

  1. 当前表是否存在
  2. 数据字段是否为表里的字段
  3. 字段类型是否匹配
  4. 非空字段是否不为空

如果是修改呢?

  1. 当前表是否存在
  2. 数据字段是否为表里的字段
  3. 字段类型是否匹配
  4. 非空字段是否不为空
  5. 字段是否是可编辑字段

如果是导入呢?查询呢?
不同的操作需要有不同的校验,校验内容获取相同或许不同,或者有顺序或者无序,如每个操作前加这样的校验会极其丑陋且大量重复代码,各种if/else满天飞,还没到具体的业务逻辑就已经晕了

如何解决

我们既然已经掌握了大量秘笈,要学会见招拆招
首先我们这么多校验需要在每个操作前去做,它不属于核心代码,我们不能让方法自己去做,但是如果它不做,谁去呢?代理啊,西门庆要去找潘金莲鼓掌,他不能直接冲上去啊,那样是违法的,所以他得先问问金莲的意愿,所以他找了代理->王婆,王婆也是女人,女人了解女人啊,所以她在里面穿针引线,帮西门庆解决了很多前期问题,等王婆搭好桥梁,他就直接过去鼓掌了,所以我们首先得找个代理,由此,我们的第一个设计模式,代理模式呼之欲出——

代理模式

代理又分静态代理和动态代理,明显我们各种校验的是动态的,所以我们这里需要使用到动态代理
spring的aop为我们提供了很好的实践,我们直接使用就好了。
首先我们需要一个注解,去标记哪些类需要做校验

@Target({
      ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModelCheck {
     

    FiledCheckEnum[] value() default {
     };

}

其中FiledCheckEnum是一个枚举类,列举了我们需要校验的类型

    //检查字段是否存在
    CHECK_FILED_EXIST(0,"存在校验"),

    CHECK_FILED_NULL(1,"非空校验"),

    CHECK_FILED_SEARCH(2,"搜索校验"),

    CHECK_FILED_EDIT(3,"编辑校验"),

    CHECK_FILED_IMPORT(4,"导入校验"),

    CHECK_FILED_SORT(5,"排序校验"),

    CHECK_FILED_TYPE(6,"类型校验"),

我们一个操作需要有多重的校验,所以用一个数组将需要进行的校验操作进行组合

    @ApiOperation("添加ci项")
    @PostMapping("/add")
    @ModelCheck(value = {
     FiledCheckEnum.CHECK_FILED_EXIST,FiledCheckEnum.CHECK_FILED_NULL})
    public ApiResponse add(
            @ApiParam(required = true, name = "params", value = "添加参数")
            @RequestBody Map<String, Object> params) {
     
        ciItemService.addCiItem(params);
        return new ApiResponse<>().success();
    }

然后我们需要定义一个切面去拦截这些方法和进行具体操作

@Aspect
@Component
public class ModelCheckAspect {
     


    @Autowired
    private TablesService tablesService;

    @Autowired
    private CustomizeConfig customizeConfig;

    @Pointcut("@annotation(com.dr.cmdb.application.annotation.ModelCheck)")
    public void modelCheckAspect() {
     
    }

    @Before("modelCheckAspect()")
    public void doBefore(JoinPoint joinPoint) {
     
        //拿到被注解的方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //拿到被注解的方法
        Method method = signature.getMethod();
        Method targetMethod = AopUtils.getMostSpecificMethod(method, joinPoint.getTarget().getClass());
        //拿到被注解类的参数
        Object[] arguments = joinPoint.getArgs();
        if (null == arguments || arguments.length <= 0) {
     
            throw new BizException(BaseCode.PARAM_ERROR);
        }
        //获取当前方法注解里的参数
        ModelCheck modelCheck = AnnotationUtils.findAnnotation(targetMethod, ModelCheck.class);
        //先匹配方法名
        Map<String, Object> argument = (Map<String, Object>) arguments[0];
        List<TableModelsDto> tableCols = tablesService.getTableCols((String) argument.get(customizeConfig.getCiTableName()));
        if (CollectionUtils.isEmpty(tableCols)) {
     
            throw new BizException(BaseCode.PARAM_ERROR);
        }
        //构建校验参数
        FiledCheckDto build = FiledCheckDto.builder()
                .argument(argument)
                .tableCols(tableCols)
                .checkEnums(Arrays.asList(modelCheck.value()))
                .ciTableName(customizeConfig.getCiTableName())
                .build();
         //执行校验责任链
        FiledCheckHandlerFactory.processHandler(build);
    }

具体操作如代码所示,拦截方法,获取方法的参数,获取注解里面的参数
最后调用校验执行

FiledCheckHandlerFactory.processHandler(build);

大家可以先忽略这句,我们继续分析

链接
链接
校验参数表
校验参数字段
校验参数值

看到上面这个图有没有福至心灵,链……链……对,

责任链模式

我们先定义我们的校验责任链。然后传递我们需要校验的类型去做匹配,不同于普通的责任链只匹配一个节点就退出,我们需要遍历完整个责任链,我们的校验类型是动态的,所以我们需要把我们需要做的校验都做完,我们一般称这种为变种责任链,降龙十八掌你会了,是不是可以考虑降龙三十六掌也是可以的啊

先定义责任链的抽象类

@Data
public abstract class FiledCheckHandler {
     

    protected FiledCheckHandler nextHandler = null;

    public abstract FiledCheckEnum getType();

    /**
     * 检测字段是否符合规范
     */
    public void handler(FiledCheckDto filedCheckDto) {
     
        //匹配到该类型
        if (filedCheckDto.getCheckEnums().contains(getType())) {
     
            checkHandler(filedCheckDto);
        }
        if (null == nextHandler) {
     
            return;
        }
        nextHandler.handler(filedCheckDto);
    }


    /**
     * 子类具体校验
     *
     * @param filedCheckDto
     */
    public abstract void checkHandler(FiledCheckDto filedCheckDto);
}

代码里面有两个方法,getType,checkHandler
getType主要让子类告我们它是做哪种校验的
checkHandler是子类具体的校验操作

@Order(3)
@Component
@Slf4j
public class FiledCheckEditHandler extends FiledCheckHandler {
     

    @Override
    public FiledCheckEnum getType() {
     
        return FiledCheckEnum.CHECK_FILED_EDIT;
    }

    /**
     * 检测字段是否符合规范
     *
     * @param filedCheckDto
     */
    @Override
    public void checkHandler(FiledCheckDto filedCheckDto) {
     
        Map<String, Object> argument = filedCheckDto.getArgument();
        List<TableModelsDto> tableCols = filedCheckDto.getTableCols();
        List<String> collect = tableCols.stream().filter(e -> !e.getEditFlag()).map(TableModelsDto::getModelName).collect(Collectors.toList());
        argument.forEach(
                (k, v) -> {
     
                    if (collect.contains(k)) {
     
                        LoggerUtil.error(log,"字段不可编辑:{}", k);
                        throw new BizException(BizCode.FIELD_NOT_EXIST.getCode(), "字段不可编辑:" + k);
                    }
                }
        );
    }

}

这种父类定义方法,子类去实现,然后父类调用的设计模式就是

模板方法模式

大家也注意到这个细节@Order(3),由于我们的参数校验有顺序的,所以我们在构建责任链的时候需要按照我们设定的顺序来
到此为止,我们已经有我们的各种检验方法了,那么我们怎么把我们的检验方法创建出来呢,造,造出来,没错,下一个模式脱口而出

工厂模式

spring容器在初始化过程中给我们预留了很多的钩子,我们可以利用这个特性将我们的校验责任链初始化,具体逻辑如下:

@Component
public class FiledCheckHandlerFactory implements CommandLineRunner, ApplicationContextAware {
     
    private volatile ApplicationContext applicationContext;


    private static FiledCheckHandler handlers;


    /**
     * 拼接变种责任链
     * @param args
     */
    @Override
    public void run(String... args) {
     
        Collection<FiledCheckHandler> checkHandlers = this.applicationContext.getBeansOfType(FiledCheckHandler.class).values();
        List<FiledCheckHandler> list = Lists.newArrayList(checkHandlers);
        Collections.sort(list, AnnotationAwareOrderComparator.INSTANCE);
        for (int i = 0; i < list.size() - 1; i++) {
     
            list.get(i).setNextHandler(list.get(i + 1));
        }
        handlers = list.get(0);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
     
        this.applicationContext = applicationContext;
    }

    /**
     * 执行方法校验
     * @param filedCheckDto
     */
    public static void processHandler(FiledCheckDto filedCheckDto) {
     
        handlers.handler(filedCheckDto);
    }


}

你以为这样就完了么?
看到processHandler方法没,它确实是一个普普通通的静态方法,自己不干事交给别人干,我们可以把它理解为

委托模式

在这些设计模式的架构下,我们就可以很方便的进行校验规则的扩展,如果要新增校验类型,只要继承校验抽象类,实现两个方法就可以了,基本不需要使用if/else了,其实我们在开发过程中都有意无意地使用到了各种设计模式,只是自己没有去深究,但我还是建议大家在代码开发前先进行构思,怎么更方便地去实现自己的业务,说明白点就是抽象化的能力,代码抽象化越高,扩展性越强,我们需要有一种大局观,要先跳出具体的实现,考虑通用性,抽取通用性,公共部分,将我们的武功秘笈融会贯通,这样招式才有威力

你可能感兴趣的:(设计模式,设计模式,java)