属性模式(Property):动态属性、属性列表、类型安全,以及配置文件与动态表单
说实话,属性模式这个名字听起来有点「工具人」的感觉。但你别小看它。我在好几个大型项目中,都靠它解决了「需求变来变去,但核心代码不想动」的难题。
属性模式的核心思想很简单:把对象的属性从硬编码的字段,变成可动态增删的键值对集合。说白了,就是给对象挂一个「属性背包」,想装什么装什么。
为什么需要属性模式?
你想想看,传统的 JavaBean 或者 C++ 类,属性都是编译期定死的。比如一个 User 类,有 name、age、email。如果产品经理突然说:「加一个用户等级字段吧。」你得改类、加字段、改构造器、改序列化……烦不烦?
属性模式就是用来应对这种场景的。它让对象在运行时可以「长出」新的属性。我曾在做电商后台时,商品属性经常变——今天加个「材质」,明天加个「适用季节」。用属性模式,一行代码都不用改核心类。
属性模式的结构
属性模式通常包含这几个角色:
- PropertyHolder:持有属性列表的对象。比如一个
Entity基类。 - Property:单个属性,包含 key 和 value。
- PropertyList:属性的集合,提供增删改查。
- PropertySchema(可选):定义属性的元信息,比如类型、默认值、是否必填。用来保证类型安全。
我画了一张图,帮你理清它们的关系:
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;
}
}
然后在 PropertyHolder 的 setProperty 方法里,先查 Schema,校验通过再存。这样既保留了动态性,又有了类型安全。
实战:配置文件驱动的动态表单
这是属性模式最经典的应用场景。我曾经做过一个「动态表单引擎」,表单的字段完全由 JSON 配置文件驱动。核心思路是这样的:
- 配置文件定义每个字段的 key、类型、默认值、校验规则。
- 前端根据配置渲染表单控件。
- 用户提交后,后端用属性模式存储数据。
- 查询时,根据 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_cast或Class.cast时,一定要 try-catch,否则线上会 500。
总结
属性模式不是什么高深的设计模式,但它非常实用。说白了,它就是在「静态类型」和「动态灵活」之间找了个平衡点。你不需要把所有属性都写死在类里,也不需要完全放弃类型安全——用 Schema 兜底就好。
如果你正在做一个配置系统、表单引擎、或者任何「属性经常变化」的模块,不妨试试属性模式。代码会清爽很多,改需求时你也会感谢自己的。
公众号:蓝海资料掘金营,微信deep3321