设计模式其实是一个比较抽象的东西,它就像武侠小说里的武功秘笈,比如单例模式是一阳指,策略模式是独孤九剑,模板方法模式是无相神功……但即使我们对这些武功秘籍倒背如流,在实战的时候往往还是被一拳擂倒,那为什么在人人如龙的时代(武功秘笈不要钱),有的人降龙十八掌就能打出龙,有的人打出虫呢,下面我们根据一个实例一步步去掌握这些武功秘笈的诀窍
手上接到一个项目,需要有动态表功能,什么意思呢,正常比如我们有一个user表,可能有name,age,phoneNum等字段,这些字段都是我们在软件创建初期就设计好的,在编码里面有个User类与表一一映射,一旦我们的表需要新增一个字段sex,那我们需要修改表结构,并修改代码。而我们现在的需求直接在管理界面动态修改表的字段,这样无需修改表和代码就能做到表字段的动态扩展,甚至我们能动态扩展新的表
我们需要实现上述功能,肯定需要定义表的属性,字段,字段的类型,长度,是否唯一,是否是可编辑,是否是展示字段,是否是搜索字段,是否是排序字段,是否允许空,是否是导入字段等等,表定义比较简单,但是表定义的数据怎么存储呢?如果按字段key,value等方式存储,查询时我们要列转行,分页排序等功能就非常麻烦了,由于我们的字段是动态的,所以数据是可变的,这种格式是不是很熟悉,json,没错,对nosql比较熟悉的人,脑子里肯定已经跳出来,Bson->mongodb,所以我们最终决定固定数据存储mysql,动态数据存储mongodb
一直到这里其实还没讲到和设计模式有半毛钱关系,客观莫急,听我慢慢道来,mysql数据库字段属性固定的属性,比如是否唯一,是否为空,字段类型等等,他帮我们做了很多的校验,但是我们现在属性是动态的,字段就需要我们去做校验了,比如我现在要新增一条user记录,我需要做哪些内容呢?
{
"name": "张芷",
"age": "17",
"tableName": "user"
}
上述内容为新增一条记录的前端传过来的数据
如果是修改呢?
如果是导入呢?查询呢?
不同的操作需要有不同的校验,校验内容获取相同或许不同,或者有顺序或者无序,如每个操作前加这样的校验会极其丑陋且大量重复代码,各种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了,其实我们在开发过程中都有意无意地使用到了各种设计模式,只是自己没有去深究,但我还是建议大家在代码开发前先进行构思,怎么更方便地去实现自己的业务,说明白点就是抽象化的能力,代码抽象化越高,扩展性越强,我们需要有一种大局观,要先跳出具体的实现,考虑通用性,抽取通用性,公共部分,将我们的武功秘笈融会贯通,这样招式才有威力