属性模式(Property):动态属性、属性列表、类型安全,以及配置文件与动态表单

说实话,属性模式这个名字听起来有点「工具人」的感觉。但你别小看它。我在好几个大型项目中,都靠它解决了「需求变来变去,但核心代码不想动」的难题。

属性模式的核心思想很简单:把对象的属性从硬编码的字段,变成可动态增删的键值对集合。说白了,就是给对象挂一个「属性背包」,想装什么装什么。

为什么需要属性模式?

你想想看,传统的 JavaBean 或者 C++ 类,属性都是编译期定死的。比如一个 User 类,有 nameageemail。如果产品经理突然说:「加一个用户等级字段吧。」你得改类、加字段、改构造器、改序列化……烦不烦?

属性模式就是用来应对这种场景的。它让对象在运行时可以「长出」新的属性。我曾在做电商后台时,商品属性经常变——今天加个「材质」,明天加个「适用季节」。用属性模式,一行代码都不用改核心类。

核心价值: 解耦「属性的定义」与「属性的使用」。属性列表可以来自配置文件、数据库、甚至用户输入。

属性模式的结构

属性模式通常包含这几个角色:

  • PropertyHolder:持有属性列表的对象。比如一个 Entity 基类。
  • Property:单个属性,包含 key 和 value。
  • PropertyList:属性的集合,提供增删改查。
  • PropertySchema(可选):定义属性的元信息,比如类型、默认值、是否必填。用来保证类型安全。

我画了一张图,帮你理清它们的关系:

PropertyHolder PropertyList Property PropertySchema key + value 类型、默认值、约束 属性模式核心结构

Java 实现:一个简单的属性容器

我们先看一个最简版本。我用 Map<String, Object> 来存储属性,但加了一层封装,方便扩展。

public class PropertyHolder {
    private Map<String, Object> properties = new HashMap<>();

    public void setProperty(String key, Object value) {
        properties.put(key, value);
    }

    public Object getProperty(String key) {
        return properties.get(key);
    }

    public <T> T getProperty(String key, Class<T> type) {
        Object value = properties.get(key);
        if (value == null) return null;
        return type.cast(value);
    }

    public boolean hasProperty(String key) {
        return properties.containsKey(key);
    }

    public Map<String, Object> allProperties() {
        return Collections.unmodifiableMap(properties);
    }
}

嗯,这里要注意:直接用 Map 虽然方便,但类型不安全。你取出来的是 Object,还得自己强转。所以我加了泛型版本 getProperty(key, Class),至少能省掉显式强转。

C++ 实现:模板与类型安全

C++ 这边,我习惯用 std::any 或者 std::variant。下面是用 std::any 的版本:

#include <iostream>
#include <unordered_map>
#include <any>
#include <string>
#include <typeinfo>

class PropertyHolder {
private:
    std::unordered_map<std::string, std::any> properties;

public:
    template<typename T>
    void setProperty(const std::string& key, T value) {
        properties[key] = value;
    }

    template<typename T>
    T getProperty(const std::string& key) {
        auto it = properties.find(key);
        if (it == properties.end()) {
            throw std::runtime_error("Property not found: " + key);
        }
        return std::any_cast<T>(it->second);
    }

    template<typename T>
    T getPropertyOrDefault(const std::string& key, T defaultVal) {
        auto it = properties.find(key);
        if (it == properties.end()) return defaultVal;
        try {
            return std::any_cast<T>(it->second);
        } catch (const std::bad_any_cast&) {
            return defaultVal;
        }
    }

    bool hasProperty(const std::string& key) const {
        return properties.find(key) != properties.end();
    }
};
注意: std::any_cast 在类型不匹配时会抛异常。我在项目中吃过这个亏——有一次传了个 int 进去,用 double 取出来,直接崩了。所以建议用 getPropertyOrDefault 这种安全版本。

类型安全:用 Schema 约束属性

纯动态属性有个问题:你没法保证别人塞进去的值是合法的。比如「年龄」字段,理论上应该是整数,结果有人传了个字符串「十八岁」。怎么办?

我的做法是引入 PropertySchema。它定义了每个属性的元信息:

public class PropertySchema {
    private String key;
    private Class<?> type;
    private boolean required;
    private Object defaultValue;
    private String validationRegex; // 可选的正则校验

    // 构造器、getter 省略

    public boolean validate(Object value) {
        if (value == null) return !required;
        if (!type.isInstance(value)) return false;
        if (validationRegex != null && value instanceof String) {
            return ((String) value).matches(validationRegex);
        }
        return true;
    }
}

然后在 PropertyHoldersetProperty 方法里,先查 Schema,校验通过再存。这样既保留了动态性,又有了类型安全。

我的习惯: 在项目启动时,从配置文件或数据库加载 Schema 列表。这样改属性约束只需要改配置,不用重新编译。

实战:配置文件驱动的动态表单

这是属性模式最经典的应用场景。我曾经做过一个「动态表单引擎」,表单的字段完全由 JSON 配置文件驱动。核心思路是这样的:

  1. 配置文件定义每个字段的 key、类型、默认值、校验规则。
  2. 前端根据配置渲染表单控件。
  3. 用户提交后,后端用属性模式存储数据。
  4. 查询时,根据 Schema 反序列化回正确的类型。

配置文件示例(JSON):

{
  "fields": [
    { "key": "username", "type": "string", "required": true, "maxLength": 50 },
    { "key": "age", "type": "int", "required": false, "min": 0, "max": 150 },
    { "key": "email", "type": "string", "required": false, "pattern": "^\\S+@\\S+$" },
    { "key": "subscribe", "type": "boolean", "default": true }
  ]
}

后端加载配置后,生成 PropertySchema 列表。用户提交的数据存到 PropertyHolder 里。整个过程,核心代码一行没改,只是换了配置文件,表单就完全变了样。

属性列表的序列化与持久化

属性模式的数据最终要存到数据库或文件里。我常用的方案是:

存储方式 优点 缺点
JSON 列(如 MySQL JSON 类型) 灵活,支持索引 查询复杂属性效率低
键值表(property_key, property_value) 扩展性强,支持任意属性 查询需要多次 JOIN
序列化到 BLOB 简单粗暴 无法按属性查询

我个人偏好「JSON 列 + 缓存」的组合。属性列表整体读写,JSON 列刚好合适。如果某个属性需要频繁查询,再单独建索引列。

避坑指南

我曾经犯过的错:

  • 属性 key 命名混乱。有人用驼峰,有人用下划线,还有人用中文。后来我强制所有 key 用 snake_case,并在 Schema 里注册。
  • 忘记处理 null。动态属性很容易出现某个 key 不存在的情况。一定要在 getter 里提供默认值或 Optional。
  • 类型转换过于自信。用 any_castClass.cast 时,一定要 try-catch,否则线上会 500。

总结

属性模式不是什么高深的设计模式,但它非常实用。说白了,它就是在「静态类型」和「动态灵活」之间找了个平衡点。你不需要把所有属性都写死在类里,也不需要完全放弃类型安全——用 Schema 兜底就好。

如果你正在做一个配置系统、表单引擎、或者任何「属性经常变化」的模块,不妨试试属性模式。代码会清爽很多,改需求时你也会感谢自己的。


公众号:蓝海资料掘金营,微信deep3321