关于“参数值合法判定”的设计思考与实践

1. 前言

1.1. 背景

业务开发中,根据不同的参数类型(Param)和键值(ParamObjectKey)查询参数值,继而判定参数值(ParamData)的合法性。一般来说,若参数值符合某特定规则,则原值即是合法值;否则,将计算得出符合该特定规则的可行值作为参数的合法值。

通用参数模块内定义了合法判定接口(ParamValueLegalRule),该接口声明了“参数值是否合法”以及“根据参数值计算得出合法值”两个接口方法,模块内提供了“固定值判定”和“数值范围判定”两个实现类:

  • 固定值判定:可配置多个合法值,通过比较字符串是否相同以确定参数合法性,若参数值不合法,则以合法值列表的首个元素作为默认合法值。

fixValueRule.png

图1.1 固定值判定

  • 数值范围判定:通过判断参数值是否属于给定区间以确认参数合法性,若参数值小于下限,则以下限值作为合法值;若参数值大于上限,则以上限值作为合法值;否则,原值即是合法值。

numberRangeRule.png

图1.2 数值范围判定

类图如下:

class_uml.png

图1.3 参数值合法判定接口及其实现类

1.2. 问题

业务模块中,开发人员在上下文内维护参数合法值判定规则的映射表:

rule_context.png

图1.4 判定规则映射表

然而,参数值作为一个二元查询值,若仅定义”参数类型“到”判定规则“的映射,而忽视”键值“这一重要因素,在映射规则定义上存在很大的局限性,难以满足复杂多元的判定需求,如:

  • 同键多值:若键 A 等于 1 时,参数合法值为 1;若键A等于 2,则合法值为 5。

  • 动态键值依赖:键值条件非固定,需查询获取键值后,再进行参数值查询,但合法判定时依赖键值条件。

除此之外,部分业务场景也提出了新的判定需求:

  • 业务属性依赖:合法判定时依赖其他业务属性。

  • 属性值判定:参数值由多个属性值组成(如 [{key1:value1}, {key2:value2}, ...] ),以某个属性值是否符合预期值进行判定。

  • 元素值判定:参数值由多个元素值组成(如 [value1, value2, value3, ...] ),以某个索引的元素值是否符合预期值进行判定。

  • ......

既有判定规则实现类在部分业务场景中显得力不从心:

  • 固定值判定:不支持对浮点型参数值的合法判定,由于以字符串比较的方法进行判定,当合法值为 1 而参数值为 1.0 时将被视为不合法;存在合法值获取与合法值计算不相关的情况,如:合法值集合为 {a, b, c},但默认合法值应为 d 。

  • 数值范围判定:可能存在参数值无法比较的情况(如参数值为“infinity”,不能转换为数值类型),合法区间的上限可能为无穷大,下限可能为无穷小;在获取合法值时可能取某个区间中间值而非上限或下限值。

为了满足上述复杂多变的判定需求,部分参数值判定需要在业务代码中区别处理以单独判定,代码中往往充斥着大量的 if-else 语句,且代码较为冗长,不够整洁与难以维护;另一方面弱化了判定规则上下文的封装意义,原期望上下文对象能整合所有参数值的合法判定规则,但实际部分参数值又脱离了上下文对象的定义域,代码不够统一。

problem.png

图1.5 业务场景及代码问题

1.3. 目标

  • 重构通用参数模块,定义更贴切业务场景的接口实现类,并要求高度的灵活性和可扩展性。

  • 明确映射规则上下文和业务层的职责,所有参数值的合法判定规则统一在上下文对象内定义,业务层需提供获取判定规则的必要条件,由上下文对象识别并返回的相应的判定规则,再由业务层调用以判断参数值合法性或计算相关合法值。

  • 尽可能地消除业务层的 if-else 语句,避免代码冗长,提高代码的可读性与可维护性。

ideal_process.png

图1.6 理想的参数值合法判定

2. 设计及实现

2.1. 设计思考

上下文对象内使用了 Map 定义判定规则的映射路由时,映射关系的核心在于建立“参数类型+其他依赖条件”与“判定规则”的动态关联。此处存在两种典型的设计范式:

  1. 键侧设计(Key-Driven Mapping)

将参数类型与其他依赖条件的组合作为映射表的键(ParamCompositeKey),构建 Map<ParamCompositeKey, ParamValueLegalRule> 结构:

public calss ParamCompositeKey {
    // 参数类型
    private Param type;
    // 其他依赖条件(键值、其他参数值、业务属性等)...
    // 如:Map<String, KeyCondition> requiredConditions;
    // 或者:Collection<KeyCondition> requiredCondtions;
    
    // 重写 equals 与 hashCode 实现多维匹配
}

这种设计的优势在于每个参数类型与其他依赖条件的唯一组合都对应独立规则,通过组合键的哈希值快速检索判定规则,但局限性也很明显,一对一的映射关系导致了在处理无边界依赖条件时的先天劣势(本质上还是不支持动态条件匹配),条件值必须在 ParamCompositeKey 构造时固化,仅支持完全一致的等值匹配,无法实现开区间、通配符、正则等复杂条件匹配,并且存在组合键膨胀、条件优先级缺失等问题。

  1. 值侧设计(Value-Driven Routing)

将必要条件如参数类型(Param)作为映射表的一级索引键,采用二级路由或多级路由获取判定规则(实际上二级路由即可满足所有业务场景),构建多重索引表结构。需注意的是,在二级路由或多级路由时不再以映射表进行路由,而是采用更为灵活的逻辑表达式解析,将索引键的匹配条件定义为逻辑表达式,这种动态路由的优势在于可支持条件表达式、正则匹配等复杂路由逻辑,采用无状态的类设计方案也利于同一路由策略被多个不同的参数类型共享,但检索判定规则时可能需要遍历匹配规则,时间复杂度上升至 O(n),且接口设计复杂度相对较高,需要建立统一的路由接口规范。

rule_index_context.png

图2.1 判定规则二级索引表

2.2. 代码实现

由于“键侧设计”并不能达到预期目标,因此,以下主要阐述“值侧设计”的核心实现。

回顾参数值(ParamData)和参数值合法判定接口(ParamValueLegalRule)的定义:

public class ParamData {
    /**
     * 键类型及键值
     */ 
    private final ParamObjectKey objectKey;
    /**
     * 参数类型
     */ 
    private final Param param;
    /**
     * 参数查询结果
     */ 
    private final String originalValue;
    
    // other methods ...
}
public interface ParamValueLegalRule {
    
    boolean isLegal(ParamData paramData);
    
    String getLegalValue(ParamData paramData);
    
}

首要考虑的是判定过程中如何获取其他依赖条件(如其他参数值、业务属性等),由于不同的参数值所依赖的判定条件不尽相同,采用 Map 结构以定义必要的依赖条件则更为灵活与贴切。(若在 ParamData 中定义不同的字段以区分条件属性,在设计之初很难罗列完所有的依赖条件,随着业务开发过程中条件字段的不断追加,势必破坏底层设计的封装性及稳定性),完善 ParamData 的定义:

public class ParamData {
    /**
     * 键类型及键值
     */ 
    private ParamObjectKey objectKey;
    /**
     * 参数类型
     */ 
    private Param param;
    /**
     * 参数查询结果
     */ 
    private String originalValue;
    /**
     * 其他依赖条件
     */ 
    private final Map<String, Object> additionalValues;

    public final void putAdditionalValue(String key, Object value) {
        additionalValues.put(key, value);
    }

    public final <T> T getAdditionalValue(String key) {
        return (T) additionalValues.get(key);
    }
    
    // other methods ...
}

additionalValues 属性值的引入使得 ParamData 能够作为识别判定规则的必要条件。其次,为解决同一参数类型下不同判定规则的路由匹配,设计实现规则链结构类 ParamValueLegalRuleChain,定义 Predicate<ParamData> 封装路由条件,Supplier<ParamValueLegalRule> 惰性加载判定规则,采用二元组结构绑定 Predicate<ParamData> 和 Supplier<ParamValueLegalRule>,定义链式调用 api 方法 appendCase(Predicate<ParamData>, Supplier<ParamValueLegalRule>) 构建可控优先级的顺序路由链,otherwise(Supplier<ParamValueLegalRule>) 设置兜底规则,确保逻辑闭环,类定义如:

public class ParamValueLegalRuleChain implements ParamValueLegalRule {

    private final List<Tuple2<Predicate<ParamData>, Supplier<ParamValueLegalRule>>> cases;

    private ParamValueLegalRuleChain(ParamValueLegalRuleChainBuilder builder) {
        this.cases = builder.cases;
    }

    public static ParamValueLegalRuleChainBuilder builder() {
        return new ParamValueLegalRuleChainBuilder();
    }

    @Override
    public boolean isLegal(ParamData paramData) {
        return cases.stream().filter(tuple -> tuple.getT1().test(paramData))
                .findFirst()
                .map(tuple -> tuple.getT2().get().isLegal(paramData))
                .get();
    }

    @Override
    public String getLegalValue(ParamData paramData) {
        return cases.stream().filter(tuple -> tuple.getT1().test(paramData))
                .findFirst()
                .map(tuple -> tuple.getT2().get().getLegalValue(paramData))
                .orElse(null);
    }

    public static class ParamValueLegalRuleChainBuilder {

        private final List<Tuple2<Predicate<ParamData>, Supplier<ParamValueLegalRule>>> cases;

        private ParamValueLegalRuleChainBuilder() {
            this.cases = Lists.newArrayList();
        }

        public ParamValueLegalRuleChainBuilder appendCase(Predicate<ParamData> condition, Supplier<ParamValueLegalRule> action) {
            this.cases.add(Tuples.of(condition, action));
            return this;
        }

        public ParamValueLegalRuleChain otherwise(Supplier<ParamValueLegalRule> action) {
            this.cases.add(Tuples.of(paramData -> true, action));
            return new ParamValueLegalRuleChain(this);
        }

    }

}

这类规则链结构类似于“条件分派器”或“规则引擎”的原型,支持任意复杂的条件表达式定义,并可灵活组合现有判定规则实现类。

使用示例:

ParamValueLegalRule chain = ParamValueLegalRuleChain.builder()
    .appendCase(
        paramData -> "1".equals(paramData.getObjectKey().getSubKey()),
        () -> ParamValueLegalRuleFixValue.builder().appendLegalValue("A", "B", "C").build()
    )
    .appendCase(
        paramData -> "2".equals(paramData.getAdditionalValue("QCI")),
        () -> ParamValueLegalRuleFixValue.builder().appendLegalValue("D", "E", "F").build()
    )
    // 任意值合法
    .otherwise(AlwaysValidParamValueLegalRule::getInstance);

最后,为解决现有规则中合法值获取与合法值计算不相关以及数值比较时可能出现非数值类型无法转换等问题,定义规则增强器 ParamValueLegalRuleInterceptor(装饰器),它通过一系列可选的回调函数(如 overrideBeforeIsLegal、beforeGetLegalValue、afterGetLegalValue 等)增强原始规则对象。该类通过委托原始规则的实现,在必要时执行重写、前置、后置等逻辑,实现解耦增强逻辑,类定义如:

public class ParamValueLegalRuleInterceptor extends ParamValueLegalRuleDecorator {

    private final BiPredicate<ParamData, ParamValueLegalRule> overrideBeforeIsLegal;
    private final BiFunction<ParamData, ParamValueLegalRule, String> beforeGetLegalValue;
    private final BiFunction<ParamData, String, String> afterGetLegalValue;

    private ParamValueLegalRuleInterceptor(ParamValueLegalRuleInterceptorBuilder builder) {
        super(builder.paramValueLegalRule);
        overrideBeforeIsLegal = builder.overrideBeforeIsLegal;
        beforeGetLegalValue = builder.beforeGetLegalValue;
        afterGetLegalValue = builder.afterGetLegalValue;
    }

    public static ParamValueLegalRuleInterceptorBuilder builder() {
        return new ParamValueLegalRuleInterceptorBuilder();
    }

    @Override
    public boolean isLegal(ParamData paramData) {
        if (Objects.nonNull(overrideBeforeIsLegal)) {
            return overrideBeforeIsLegal.test(paramData, paramValueLegalRule);
        }
        return paramValueLegalRule.isLegal(paramData);
    }

    @Override
    public String getLegalValue(ParamData paramData) {
        String legalValue;
        if (Objects.nonNull(beforeGetLegalValue)) {
            legalValue = beforeGetLegalValue.apply(paramData, paramValueLegalRule);
        } else {
            legalValue = paramValueLegalRule.getLegalValue(paramData);
        }
        if (Objects.nonNull(afterGetLegalValue)) {
            legalValue = afterGetLegalValue.apply(paramData, legalValue);
        }
        return legalValue;
    }

    public static class ParamValueLegalRuleInterceptorBuilder {

        private ParamValueLegalRule paramValueLegalRule;
        private BiPredicate<ParamData, ParamValueLegalRule> overrideBeforeIsLegal;
        private BiFunction<ParamData, ParamValueLegalRule, String> beforeGetLegalValue;
        private BiFunction<ParamData, String, String> afterGetLegalValue;

        private ParamValueLegalRuleInterceptorBuilder() {}

        public ParamValueLegalRuleInterceptorBuilder paramValueLegalRule(ParamValueLegalRule paramValueLegalRule) {
            this.paramValueLegalRule = paramValueLegalRule;
            return this;
        }

        public ParamValueLegalRuleInterceptorBuilder overrideBeforeIsLegal(BiPredicate<ParamData, ParamValueLegalRule> overrideBeforeIsLegal) {
            this.overrideBeforeIsLegal = overrideBeforeIsLegal;
            return this;
        }


        public ParamValueLegalRuleInterceptorBuilder beforeGetLegalValue(BiFunction<ParamData, ParamValueLegalRule, String> beforeGetLegalValue) {
            this.beforeGetLegalValue = beforeGetLegalValue;
            return this;
        }

        public ParamValueLegalRuleInterceptorBuilder afterGetLegalValue(BiFunction<ParamData, String, String> afterGetLegalValue) {
            this.afterGetLegalValue = afterGetLegalValue;
            return this;
        }

        public ParamValueLegalRuleInterceptor build() {
            return new ParamValueLegalRuleInterceptor(this);
        }

    }

}

使用示例:

ParamValueLegalRule interceptor = ParamValueLegalRuleInterceptor.builder()
                .overrideBeforeIsLegal((paramData, rule) -> {
                    if (paramData.getOriginalValue().equalsIgnoreCase("ADAPTIVE")) {
                        return true;
                    }
                    return rule.isLegal(paramData);
                })
                // 原始规则:[5, 16]
                .paramValueLegalRule(new ParamValueLegalRuleNumberRange(5, 16))
                // 合法值附加前缀TX
                .afterGetLegalValue((paramData, legalValue) -> "TX" + legalValue)
                .build();

通过该装饰器的引入,开发人员可以在不侵入既定逻辑的前提下,对合法性规则实现灵活的功能增强。

对于其他规则判定实现类,如字符型固定值判定 ParamValueLegalRuleFixValue,数值范围判定 ParamValueLegalRuleNumberRange,针对固定数值匹配的 ParamValueLegalRuleNumber,属性值判定 ParamValueLegalRuleProperty 等等,感兴趣的同学可以拉取cm包的1.4.0版本(已配置源码插件)自行阅读,此处不再赘述。

3. 小结

本次通用参数模块的重构,通过灵活的设计和组件化改造,解决了原有架构在复杂业务场景下的局限性,实现了参数合法性判定的统一管理和业务解耦。核心成果如下:

  1. 解决核心痛点

  • 消除映射局限 打破原有一维映射(仅 Param 类型)的束缚,支持多条件动态路由(如键值、业务属性、动态依赖等)。

  • 覆盖复杂场景 支持同键多值、动态键值依赖、业务属性绑定、结构化参数(属性值/元素值判定)等多元判定需求。

  • 修复既有缺陷

    • 固定值判定:支持默认值自定义、实现固定数值判定类以兼容数值匹配(如 1.0 与 1 等价)

    • 数值范围判定:支持无穷值(infinity)、非数值回退、区间中间值计算

  1. 核心设计突破

  • 扩展式参数值定义 强化 ParamData 的上下文承载能力

  • 值侧动态路由 采用二级规则链结构

  • 装饰器增强 无侵入扩展规则能力

  1. 业务价值

  • 统一治理 所有判定规则收敛至上下文对象,业务层仅需提供 ParamData 并调用统一接口。

  • 消灭冗余代码 提升可读性与可维护性。

  • 开闭原则

    • 新规则:通过实现 ParamValueLegalRule 快速扩展

    • 增强逻辑:通过装饰器动态叠加,无需修改既有实现

  • 灵活适配 内置组件支持常见场景(如数值范围、属性值、元素值等结构化参数),复杂逻辑可通过规则链自由编排。

尽管重构后的参数模块在灵活性和扩展性上取得了显著进步,但仍存在以下可优化空间:

  1. 规则构建缺乏统一接口

现有设计中各类规则(固定值、数值范围等)的构建逻辑分散在各实现类中,应设计统一的 Builder 接口,提供标准化构建方法。

  1. 条件注入未完全解耦

虽然通过 additionalValues 解决了动态依赖问题,但部分场景仍需业务层预判条件以区分不同的参数从而设置对应的依赖条件,业务层需了解具体规则的条件需求等(通常判定规则上下文的参数配置和业务参数值合法判定实现都是同一开发人员)。

Comment