文章目录
前言
在 java 中,如何让一个类具有动态属性。这里将介绍一种技巧,可以使得你的类,具有良好的动态属性的能力。普遍的做法是在类中申明一个 map 属性,把想要扩展的属性放入这个 map 中,这样就可以使得类具有动态属性的能力了。本文介绍的实现上本质也是如此,看到这里你是不是已经没兴趣往下看了,兄弟,先别着急,如果仅是样我也没必要写这个了。这里介绍的是具有良好的动态属性的能力,看完本文,你会获得很大的收益!
这里会介绍三种动态属性的实现方式
- 普遍的
- 较好的
- 良好的
本文会循序渐进的从普遍的、较好的、良好的顺序来讲代码的演化过程。
一、普遍的
普遍的-类定义
类中申明一个 map 属性,把想要扩展的属性放入这个 map 中,这样就可以使得类具有动态属性的能力了。
@FieldDefaults(level = AccessLevel.PRIVATE)
public class BirdAttr {
/** 动态属性 */
final Map<String, Object> attr = new HashMap<>();
public void setAttr(String attrName, Object value) {
this.attr.put(attrName, value);
}
public Object getAttr(String attrName) {
return this.attr.get(attrName);
}
}
使用示例
class BirdAttrTest {
public void test1() {
BirdAttr bird = new BirdAttr();
// 设置属性
bird.setAttr("name","塔姆");
bird.setAttr("age", 18);
// 获取属性
String name = (String) bird.getAttr("name");
int age = (int) bird.getAttr("age");
}
}
通过使用示例,我们可以看到,每次使用属性时都需要进行一次强转。
为了避免强转,我们有必要对这个类进行一次改造
普遍的-类改造1
我们加了一些方法,这些方法的目的在于,当我们使用动态属性时可以省去强转的一个步骤。
@FieldDefaults(level = AccessLevel.PRIVATE)
public class BirdAttr {
/** 动态属性 */
final Map<String, Object> attr = new HashMap<>();
public void setAttr(String attrName, Object value) {
this.attr.put(attrName, value);
}
public Object getAttr(String attrName) {
return this.attr.get(attrName);
}
public String getAttrString(String attrName) {
return (String) this.attr.get(attrName);
}
public Integer getAttrInt(String attrName) {
return (Integer) this.attr.get(attrName);
}
}
使用示例
class BirdAttrTest {
public void test2() {
BirdAttr bird = new BirdAttr();
// 设置属性
bird.setAttr("name","塔姆");
bird.setAttr("age", 18);
// 获取属性
String name = bird.getAttrString("name");
int age = bird.getAttrInt("age");
}
}
这样看起来舒服多了(至少在使用者的角度),毕竟舒服是相对的,相对于上面的示例。
但细心的朋友会发现,每个类型都需要声明一个对应的类型转换方法。比如 int、bool、long、String、byte、short、char、double…等。这样做确实太麻烦,当然我们还可以使用泛型来确定类型,修改如下
普遍的-类改造2
@FieldDefaults(level = AccessLevel.PRIVATE)
public class BirdAttr {
/** 动态属性 */
final Map<String, Object> attr = new HashMap<>();
public void setAttr(String attrName, Object value) {
this.attr.put(attrName, value);
}
/** 泛型来确定类型 */
public <T> T getAttr(String attrName) {
return (T) this.attr.get(attrName);
}
}
使用示例
class BirdAttrTest {
public void test3() {
BirdAttr bird = new BirdAttr();
// 设置属性
bird.setAttr("name","塔姆");
bird.setAttr("age", 18);
// 获取属性
String name = bird.getAttr("name");
int age = bird.getAttr("age");
}
}
看起来似乎很完美,省去了类型的转换的同时使用起来也简洁了些。好了,到这里动态属性介绍完了 (开玩笑的)!
你会发现这个动态属性只属于这一个类,如果还有一个类也想拥有动态属性的功能呢?copy 在来一次是不可能的,但我们可以用接口的方式,也就是接下来要说的 较好的。
二、较好的
动态属性接口
用接口的方式来实现动态属性,可以使得实现接口的类都具有现动态属性的功能。
public interface AttrDynamic {
/**
* 获取动态成员属性map
*
* @return 动态成员属性map
*/
Map<String, Object> getAttr();
/**
* 获取动态成员属性
*
* @param attrName 属性名
* @param <T> t
* @return val
*/
default <T> T getAttr(String attrName) {
Map<String, Object> map = getAttr();
return (T) map.get(attrName);;
}
}
类定义
只需要实现接口,就能拥有动态属性的功能,非常的方便。但你会发现接口中是需要子类实现一个 getAttr() 方法的,类中没有重写 getAttr() 方法,确能够运行是因为巧妙的运用的 lombok 的 Getter 注解,由于 lombok 的这个注解会为属性提供的 getter 方法,正好能对得上接口的方法,所以就不需要显式的重写接口的 getAttr( ) 方法了。
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class BirdAttrDynamic implements AttrDynamic {
/** 动态属性 */
final Map<String, Object> attr = new HashMap<>();
}
使用示例
class BirdAttrDynamicTest {
public void test1() {
BirdAttrDynamic bird = new BirdAttrDynamic();
// 设置属性
bird.setAttr("name", "塔姆");
bird.setAttr("age", 18);
// 获取属性
String name = bird.getAttr("name");
int age = bird.getAttr("age");
}
}
用接口的方式来实现动态属性有很多好处:
- 类只要实现接口就能拥有动态属性的功能。
- 即使每个类型都需要声明一个对应的类型转换方法,比如上面普遍中提到的 int、bool、long、String、byte、short、char、double…等转换方法,我们只需要在动态属性接口中增加default 的方法就可以了(只维护接口)。如果使用【普遍的】方式中改造,假设有10个类需要动态属性,那么你需要修改10个类。
从这里可以看出,【普遍的和较好的】在动态属性的实现方式中,都有一个很大的问题,我们先称为下一任维护者问题、华丽的简洁。
华丽的简洁这里指的是初次看上去的示例使用很简洁且,仿佛没什么毛病。但真正在实际中运用后才会发现缺陷,直至下一任维护者的运来问题暴露得更加疯狂。
class BirdQquestionTest {
public void test1() {
BirdAttrDynamic bird = new BirdAttrDynamic();
// 假设你是下一任的维护者,你知道这个属性名的类型吗
bird.getAttr("fuck");
}
}
从上面的示例中,你无法知道属性 “fuck” 的具体类型,这就是华丽的简洁。如果你想知道,只能阅读该项目其他地方的源码来寻找。实际中你的上一任哥们用这种方式编码可嗨了,你的上一任嗨一分,给你带来的痛苦大概是 2.25分(根据前景理论得出,该理论是行为经济学的重大成果之一)。
无论是普遍的还是较好的在动态属性的实现方式中,我们的下一任维护者无法很快的知道这个属性的确切类型。因为属性太动态的原因,为了可维护性我们需要尽量不要这么做。
当然,到这里你也可以说我们可以先定义一个类或者接口,把动态属性的属性名放到这个文件中。类似下面这样
public interface BirdAttrName {
/** name 的类型是 string */
String name = "name";
/** age 的类型是 int */
String age = "age";
/** fuck 的类型是 Fuck.java 类 */
String fuck = "fuck";
}
这个属性文件只是理想化的产物,你虽然可以在项目团队中定这些规范,让团队成员遵守。但你很难让每个成员都严格遵守(特别是后进的新成员),比如忘记写类型注释了或者说写错了类型注释呢,所以更别说你的上一任了(类似你接手的二手项目)。
那么还有较好的方式来避免这些吗?当然是有的,就是下面介绍的这种 良好的 方式
三、良好的
示例
为了避免一些枯燥,这次我们先看 良好的 方式的使用示例
class BirdAttrOptionDynamicTest {
public static void main(String[] args) {
BirdAttrOptionDynamic bird = new BirdAttrOptionDynamic();
// 设置属性
bird.option(BirdAttrOption.name, "塔姆");
bird.option(BirdAttrOption.age, 19);
// 获取属性
String name = bird.option(BirdAttrOption.name);
int age = bird.option(BirdAttrOption.age);
}
}
从示例中,我们可以动态的增加属性,动态的获取属性的值,并且没有强制转换。属性名由 BirdAttrOption.java 统一来管理。
类的扩展属性文件 BirdAttrOption
interface BirdAttrOption {
AttrOption<String> name = AttrOption.valueOf("name");
AttrOption<Integer> age = AttrOption.valueOf("age");
}
可以看见,在 BirdAttrOption.java 中,我们提前添加了一些需要扩展的属性名,并且类型的明确的。属性的信息由 AttrOption.java 来明确。(就算那些家伙忘记写类型注释或者说写错了类型注释,也没关系)
业务类
BirdAttrOptionDynamic 这个是我们自定义的一个业务类,只需要实现 AttrOptionDynamic 接口就能具备动态属性的功能。
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class BirdAttrOptionDynamic implements AttrOptionDynamic {
/** 动态属性项集 */
final AttrOptions options = new AttrOptions();
}
动态属性项集 - AttrOptions
用于存放动态属性的地方,对多个属性的管理
class AttrOptions {
/** 动态成员属性 */
final Map<AttrOption<?>, Object> options = new HashMap<>();
/** 获取值 */
@SuppressWarnings("unchecked")
public <T> T option(AttrOption<T> option) {
return (T) options.get(option);
}
/** 设置值 */
public <T> void option(AttrOption<T> option, T value) {
options.put(option, value);
}
}
属性项 - AttrOption
属性项, 用于确定属性的名和类型。
record 是值类型,好像是 java14 的出的(具体忘记的),转为java类的大概意思就是类中声明了一个属性 name,并自动提供 getter 方法。
record AttrOption<T>(String name) {
/** 构建属性项 */
public static <T> AttrOption<T> valueOf(String name) {
return new AttrOption<T>(name);
}
}
动态属性接口 - AttrOptionDynamic
interface AttrOptionDynamic {
/** 动态成员属性 */
AttrOptions getOptions();
/** 获取值 */
default <T> T option(AttrOption<T> option) {
return this.getOptions().option(option);
}
/** 设置值 */
default <T> void option(AttrOption<T> option, T value) {
this.getOptions().option(option, value);
}
}
总结
好的到这里就讲完了,良好的实现方式就是这样。(开玩笑的)
张三:凭什么你这个就是良好的动态属性实现方式。
OK!似乎张三提了一个问题。
在【良好的】与【普遍的和较好的】实现方案相比较,我们增加了两个类 AttrOption 和 AttrOptions。本来一个 map 可以解决的事为什么要多增加两个类呢。答案是组合与类型明确,类型明确可能会好理解一些,但组合是什么鬼。
对类型明确的说明
在类的扩展属性文件 BirdAttrOption.java 中,属性名由 BirdAttrOption.java 统一来管理。即使忘记写类型注释了也没关系,因为已经明确类型了(我们在声明属性时就能明确类型了)。意味着你的团队成员 ”不能很轻松的“ 使用动态属性了,毕竟直接的敲字符总是轻松的。如
class BirdAttrTest {
public void test1() {
BirdAttr bird = new BirdAttr();
// 设置属性
bird.setAttr("name","塔姆");
bird.setAttr("age", 18);
// 获取属性
String name = (String) bird.getAttr("name");
int age = (int) bird.getAttr("age");
// test option -- error、error、error
BirdAttrOptionDynamic bird = new BirdAttrOptionDynamic();
// ======== 不能轻松的使用字符来当属性了, 下面的使用方式会在工具中报错 ========
// 因为不支持这样做
bird.option("name", "塔姆");
}
}
我们用代码级别来约束团队的成员 (此时就变成你可以不听团队的规范,但工具不允许你这样做)。
对组合的说明
我们增加了两个类 AttrOption 和 AttrOptions,本来一个 map 可以解决的事为什么要多增加两个类呢?
复用:组合是在Java中实现程序复用(reusibility)的基本手段之一。
单一职责:一个类只做一件事
AttrOption:负责属性名和类型明确,实际上我们还可以扩展一些默认值。
AttrOptions:负责管理 AttrOption
类的复杂性降低,实现什么职责都有明确的定义;
逻辑变得简单,类的可读性提高了,而且,因为逻辑简单,代码的可维护性也提高了;
变更的风险降低,因为只会在单一的类中的修改。
类的每个责任都有改变的潜在区域。超过一个责任,意味着超过一个改变的区域。
这个原则告诉我们,尽量让每一个类保持单一责任。
对于 AttrOptions 和 AttrOption 我们还可以单独的拿出来使用,就像下面这样。
在测试的角度也更轻了些!
class BirdAttrOptionDynamicTest {
public void testAttrOptions() {
AttrOption<Long> love = AttrOption.valueOf("love");
final AttrOptions options = new AttrOptions();
options.option(love, 777L);
Long loveValue = options.option(love);
System.out.println(loveValue);
}
}
通过 “良好的 ” 动态属性实现方式,我们做到了类型的明确
我们增加了两个类 AttrOption 和 AttrOptions,做到了规范编码、单一职责、复用,真棒!
之后
我们还想让其他类具有动态属性,只需实现接口和声明一个 AttrOptions 变量就可以了,是不是很简单。
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TigerAttrOptionDynamic implements AttrOptionDynamic {
/** 动态属性项集 */
final AttrOptions options = new AttrOptions();
}
源码参考地址:
最后
本文可以转载,但必须保留所有内容。