校验器模式设计

参数校验

Posted by zhida on January 10, 2019

应用工程中,数据校验是不可或缺的一环,为了避免随处散落和耦合紧密的代码,如何优雅灵活的编写校验代码,提供了一些思路供大家参考。

数据校验大体可以分为两种,参数校验(参数非空,长度等) 和 业务校验(符合业务流转要求)。

参数校验

Preconditions

入参校验是最基础的,一般包括以下几种

  • 非空校验
  • 业务条件校验
  • 下游代码的前置条件校验
  • 数据库条件校验
  • 第三方HSF接口参数校验

使用 Preconditions 可以简洁优雅的实现,避免过多的if语句出现。

Preconditions.checkNotNull(Object);

但是涉及校验项过多,还是会手动编写很多代码,为了避免这种情况,可以对入参使用注解的方式校验

注解式校验 Bean Validation

通过在入参中定义限制条件,可以减少校验代码的编写。

Maven 依赖

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

直接在入参对象中定义限制条件。

public class Car {

    @NotNull(message = "tire不能为空")
    private String tire;     

    public String getTire() {
        return tire;
    }

    public void setTire(String tire) {
        this.tire = tire;
    }

}

使用 Validator 进行统一校验。

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();

@Test
public void testValidate(){
    Car car = new Car();
    Set<ConstraintViolation<Car>> constraintViolations = validator.validate(car);
    assertEquals(2, constraintViolations.size());
}

参考链接:

Validation step by step

Java Code Examples for javax.validation.Validator

校验器

总的思想: 把会变化的部分取出并封装起来,以便以后可以轻易的改动和扩充此部分,而不影响不需要变化的其他部分

在设计一个业务流程代码的时候,会有很多校验规则,有些同学会将规则校验混淆在业务代码中,各种if-else语句,或是抽象的不够干净,在走读代码的时候真心累,后续代码维护的时候,要增加业务校验,破窗思维惯性会继续在业务代码中增加校验代码,使得代码不断的腐化。

最好的办法是将校验逻辑抽取出来一个专门的校验类, 后续需要修改校验逻辑,不用修改原有的业务流程,集中管理,增加代码的内聚性,并且代码可重用,

对于校验类的返回值,一般有三种

  • 简单的规则校验,返回布尔值,简单易懂;
  • 针对多种条件返回,可以定义一个枚举值,定义状态码和状态消息,缺点是接收方需要一个枚举解析类;
  • 针对异常场景返回一个异常类,将异常信息直接封装在消息中。
public class WithdrawValidator {

    static boolean validateAmount(Object param){

        // TODO 规则判断

        return true;
    }

    static void validateRights(){
        //TODO 规则判断
        if( 1 == 1){
            throw new ValidationException("权限不足");
        }
    }
}

可组装校验器

工厂模式 & 门面模式

一个复杂系统中,校验规则是无处不在,业务的的易变性极强,每一个场景的数据相关联的业务规则都不相同,对扩展性和可配置性的要求较高,对每一个数据类都要设置专属的校验类。

定义一个接口类和若干规则类,相应的代码如下:

public interface BaseFilter {
    void  validate(Request request);
}


public class AbstractBaseFilter implements  BaseFilter{

    @Override
    public void validate(Request request) {
        if(request == null){
            throw  new FilterException("cannot be null");
        }
    }
}

public class DefaultFilter extends AbstractBaseFilter {

    DefaultFilter(){}

    public  void validate(Request request) {

        super.validate(request);

        if(request.getId() == null){
            throw new FilterException("id 不能为null");
        }

        //TODO 
        其他业务规则
    }

}

public class AdvanceFilter extends AbstractBaseFilter {

    AdvanceFilter(){}

    public  void validate(Request request) {

        super.validate(request);

        if(request.getId() == null){
            throw new FilterException("id 不能为null");
        }

        //TODO 
        其他业务规则
    }

}

一个系统中有很多校验类,我们需要一个管理机制,针对不同的场景取出需要的检验类使用,简单的说,提供一个工厂类,提供实例化、缓存、和查找服务的功能,通过一个工厂类缓存示例,然后创建一个门面类的,取出相应的校验实例; 本例中的实例都是在工厂中实例化,这里的资源也可以是远程的服务,通过HSF获取校验器并注册。

public class FilterFactory {

    private static Map<String,BaseFilter> maps = Maps.newHashMap();

    public static void register(String name,BaseFilter filter){
        maps.put(name,filter);
    }

    public static BaseFilter getFilter(String name){
        return  maps.get(name);
    }

}

最后创建一个门面类,隐藏工厂类,在业务代码中需要校验的时候,只需要引用门面类,获取需要的规则类即可。

public class FilterFacade {

    public static void validate(String filterName, Request request){
        BaseFilter filter = FilterFactory.getFilter(filterName);
        filter.validate(request);
    }
}

总体类图设计如下: image

动态策略的链式校验

以上的实现还是比较简单,一个场景和规则类绑定的比较紧密,可复用性也比较弱,如果将规则类再细分,自由组合,能够让业务代码更灵活的使用检验类。

参考SpringSecurity的过滤链模式,可以实现这个目标,每一个链条,其实就是一个包含若干校验器的集合,将需要的检验类封装在一起。

相同的入参,针对不同的场景,提供不同的校验链,链条中的校验器可自由组装;

校验器的代码参考方式二,校验链的代码实现如下:

定义校验链,为每一种场景配置一条校验链。

public class FilterChain {

    private String businessName;
    private List<BaseFilter> list = Lists.newArrayList();

    FilterChain(){}

    FilterChain(String bussinessName)
    {
        this.businessName = bussinessName;
    }

    // 组装 Filter
    FilterChain(String bussinessName,List<BaseFilter> list)
    {
        this.businessName = bussinessName;
        this.list = list;
    }

    // 组装 Filter
    public FilterChain append(BaseFilter validator){
        list.add(validator);
        return this;
    }

    public List<BaseFilter> getFilter(){
        return list;
    }
} 

定义工厂类管理链条

public class FilterFactory {

    private static Map<String, FilterChain> maps = Maps.newHashMap();

    public static void register(String name,FilterChain filter){
        maps.put(name,filter);
    }

    public static FilterChain getFilter(String name){
        return  maps.get(name);
    }

    /**
     * 应用启动的时候初始化模板
     */
    public static void init(){

        FilterChain filterChainA = new FilterChain();
        filterChainA.append(new DefaultFilter());
        filterChainA.append(new AdvanceFilter());

        FilterChain filterChainB = new FilterChain();
        filterChainB.append(new DefaultFilter());

        FilterFactory.register("sceneA",filterChainA);
        FilterFactory.register("sceneB",filterChainB);
    }

}

门面类提供调用方法

public class FilterChainFacade {

    public void doFilter(String type , Request request){

        FilterChain filter = FilterFactory.getFilter(type);
        filter.getFilter().iterator().next().validate(request);

    }

    public static void main(String[] args) {

        FilterChainFacade filterChainFacade = new FilterChainFacade();
        String type = "withdraw_usd";
        Request request = new Request();
        filterChainFacade.doFilter(type,request);

    }
}

总体的设计类图如下: image