https://www.huangdf.xyz/categories/study-notes
南风
南风
发布于 2025-05-14 / 5 阅读
0
0

自定义校验--责任链模式

问题

最近遇到一个问题:有一类产品,需要经过一系列校验方可确定无误。但是这一类产品并非完全一样,即需要校验的项并非完全一样。这样一来就不能在代码中写死全部的校验过程,因为不同的个体校验的项不一样,存在某些不需要校验的项无法通过校验的可能。

思路

今天突发奇想,如果采用责任链模式+建造者模式+枚举类,能否解决这个问题?

这一类产品的最大校验项数是已知的,不同个体校验项数不同,那就通过数据库记录不同个体的校验项,通过枚举类将数据库数据和具体校验项进行映射,通过建造者模式在运行的时候再确定具体校验项。

理论存在,开始实践。

校验类

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")
})


评论