问题
最近遇到一个问题:有一类产品,需要经过一系列校验方可确定无误。但是这一类产品并非完全一样,即需要校验的项并非完全一样。这样一来就不能在代码中写死全部的校验过程,因为不同的个体校验的项不一样,存在某些不需要校验的项无法通过校验的可能。
思路
今天突发奇想,如果采用责任链模式+建造者模式+枚举类,能否解决这个问题?
这一类产品的最大校验项数是已知的,不同个体校验项数不同,那就通过数据库记录不同个体的校验项,通过枚举类将数据库数据和具体校验项进行映射,通过建造者模式在运行的时候再确定具体校验项。
理论存在,开始实践。
校验类
demo代码如下:
简单的做一下校验,校验项为:年龄,性别,是否是会员。这三项是最大校验项次,也可以只校验其中某一项或者某两项。
package com.hwb.resandstr;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author admin
* @date 2025/5/14
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HandleContext {
private Integer age;
private String gender;
private boolean member;
}
责任链部分
这里只做简单的mock,数据库方面就不写了。用假数据替代。
首先是校验类,这里需要的是责任链模式,责任链模式中需要将各个校验规则串起来,所以需要头尾两个部分,其次还需要next来判断是否还需要继续校验。
demo代码如下:
package com.hwb.resandstr;
/**
* @author admin
* @date 2025/5/14
*/
public abstract class Handler<T> {
protected Handler next;
private void next(Handler next) {
this.next = next;
}
/**
* 责任链传递方法
* @param context 校验内容
*/
protected void fireNext(HandleContext context) {
if (next != null) {
next.doHandler(context);
}else {
System.out.println("校验结束");
}
}
/**
* 校验项覆写方法
* @param handleContext 校验内容
*/
public abstract void doHandler(HandleContext handleContext);
public static class Builder<T> {
private Handler<T> head;
private Handler<T> tail;
public Builder<T> addHandler(Handler handler) {
if (this.head == null) {
this.head = this.tail = handler;
return this;
}
this.tail.next(handler);
this.tail = handler;
return this;
}
public Handler<T> build() {
return this.head;
}
}
}
具体责任链
年龄校验
package com.hwb.resandstr;
/**
* @author admin
* @date 2025/5/14
*/
public class AgeHandler extends Handler{
@Override
public void doHandler(HandleContext handleContext) {
if (handleContext.getAge()<18){
System.out.println("未成年!");
return;
}else {
System.out.println("成年可入!");
}
fireNext(handleContext);
}
}
性别校验
package com.hwb.resandstr;
/**
* @author admin
* @date 2025/5/14
*/
public class GenderHandler extends Handler{
@Override
public void doHandler(HandleContext handleContext) {
if ("boy".equals(handleContext.getGender())){
System.out.println("男性止步!");
return;
}else {
System.out.println("女性可入!");
}
fireNext(handleContext);
}
}
会员校验
package com.hwb.resandstr;
/**
* @author admin
* @date 2025/5/14
*/
public class MemberHandler extends Handler{
@Override
public void doHandler(HandleContext handleContext) {
if (!handleContext.isMember()){
System.out.println("非会员不能进!");
return;
}else {
System.out.println("是会员!");
}
fireNext(handleContext);
}
}
枚举类
通过枚举类来将各个具体的校验类聚合起来,最终实现动态的创建责任链。
这里注意不能使用springutil.getbean来创建实例对象,因为这个会导致getHandlerByType每次获取到的处理器都是同一个,这就会导致在创建责任链的时候,next属性被修改,最终导致结果不如人意,想知道结果的,可以手动试试。
demo代码如下:
package com.hwb.resandstr;
import cn.hutool.extra.spring.SpringUtil;
import lombok.Getter;
/**
* @author admin
* @date 2025/5/14
*/
@Getter
public enum HandleStrategy {
/**
* 校验类枚举
*/
AGE(1, AgeHandler.class),
GENDER(2, GenderHandler.class),
MEMBER(3, MemberHandler.class);
private final Integer type;
private final Class <? extends Handler> handler;
HandleStrategy(Integer type, Class <? extends Handler> handler) {
this.type = type;
this.handler = handler;
}
public static Handler getHandlerByType(Integer type) {
if (null == type) {
return null;
}
for (HandleStrategy strategy : values()) {
if (type.equals(strategy.getType())) {
try {
return strategy.getHandler().newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
return null;
}
}
实际调用
这里通过手动模拟数据库获取数据。根据不同的数据库模拟数据,创建三个责任链处理器,再手动创建三组不同的待测试对象。
具体demo如下:
package com.hwb.resandstr;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* @author admin
* @date 2025/5/14
*/
public class TestHandler {
public static void main(String[] args) {
HandleContext h1 = HandleContext.builder().age(19).gender("girl").member(true).build();
HandleContext h2 = HandleContext.builder().age(19).gender("boy").member(true).build();
HandleContext h3 = HandleContext.builder().age(21).gender("boy").build();
HandleContext h4 = HandleContext.builder().age(17).gender("girl").build();
HandleContext h5 = HandleContext.builder().age(17).build();
HandleContext h6 = HandleContext.builder().age(19).build();
//准备校验规则
Handler.Builder<Object> builder1 = new Handler.Builder<>();
Handler.Builder<Object> builder2 = new Handler.Builder<>();
Handler.Builder<Object> builder3 = new Handler.Builder<>();
//模拟数据库获取数据
List<Integer> list1 = Arrays.asList(1, 2, 3);
List<Integer> list2 = Arrays.asList(1, 2);
List<Integer> list3 = Arrays.asList(1);
//如果size>0即需要校验
for (Integer integer : list3) {
builder3.addHandler(HandleStrategy.getHandlerByType(integer));
}
System.out.println("==============================");
//如果size>0即需要校验
for (Integer integer : list2) {
builder2.addHandler(HandleStrategy.getHandlerByType(integer));
}
System.out.println("==============================");
//如果size>0即需要校验
for (Integer integer : list1) {
builder1.addHandler(HandleStrategy.getHandlerByType(integer));
}
System.out.println("==============第一组==================");
Handler<Object> handler1 = builder1.build();
handler1.doHandler(h1);
System.out.println("---------------------------------");
handler1.doHandler(h2);
System.out.println("=============第二组=======================");
Handler<Object> handler2 = builder2.build();
handler2.doHandler(h3);
System.out.println("---------------------------------");
handler2.doHandler(h4);
System.out.println("==============第三组========================");
Handler<Object> handler3 = builder3.build();
handler3.doHandler(h5);
System.out.println("---------------------------------");
handler3.doHandler(h6);
}
}
通过断点可以看到创建出的责任链对象:
并没有意料之外的责任链对象的创建,即出现处理器错乱的状况。
最终控制台输出如下:
==============第一组==================
成年可入!
女性可入!
是会员!
校验结束
---------------------------------
成年可入!
男性止步!
是会员!
校验结束
=============第二组=======================
成年可入!
男性止步!
校验结束
---------------------------------
未成年!
女性可入!
校验结束
==============第三组========================
未成年!
校验结束
---------------------------------
成年可入!
校验结束
第一组具有所有待检测的数据,因此创建的责任链对象包含三个处理器,第二组则是只有两个处理器,第三组只有一个处理器。校验的结果也是符合预期的。
如果是在实际开发中,可以通过抛出异常来打断校验。
错误范例
上面说过如果采用SpringUtil.getBean(GenderHandler.class)的形式来获取处理器对象的话,会有什么样的后果呢?
枚举类
package com.hwb.resandstr;
import cn.hutool.extra.spring.SpringUtil;
import lombok.Getter;
/**
* @author admin
* @date 2025/5/14
*/
@Getter
public enum HandleStrategy {
/**
* 校验类枚举
*/
AGE(1, SpringUtil.getBean(AgeHandler.class)),
GENDER(2, SpringUtil.getBean(GenderHandler.class)),
MEMBER(3, SpringUtil.getBean(MemberHandler.class));
private final Integer type;
private final Handler handler;
HandleStrategy(Integer type,Handler handler) {
this.type = type;
this.handler = handler;
}
public static Handler getHandlerByType(Integer type) {
if (null == type) {
return null;
}
for (HandleStrategy strategy : values()) {
if (type.equals(strategy.getType())) {
return strategy.getHandler();
}
}
return null;
}
}
可以看到除了builder1,别的都不太正常,这是因为sprinutil.getbean获取到的对象是经由spring容器管理的单例对象,这就会导致当被不同的地方引用时,next属性就会被覆盖,这个例子是list中的age处理器被变成了builder1的形状,而23则是复用的1,所以结果自然是全部校验都走了一遍。
再补充一个点就是关于getbean的,如果不需要每次都是新的对象的话是可以用这个的,比如结合策略模式,谁来都是这一套,也没有什么内容属性修改的。就可以用这个,但是有个点需要注意,getbean的前提是:这个bean被spring容器管理,也就是说需要给getbean的对象加上component注解,并且在主文件下面标记包扫描。
就下面两个注解,一个打在类上,一个打在Application上。
@Component
@ComponentScans({
@ComponentScan(value = "com.xxh.resandstr")
})