java 动态属性的实现


前言

在 java 中,如何让一个类具有动态属性。这里将介绍一种技巧,可以使得你的类,具有良好的动态属性的能力。普遍的做法是在类中申明一个 map 属性,把想要扩展的属性放入这个 map 中,这样就可以使得类具有动态属性的能力了。本文介绍的实现上本质也是如此,看到这里你是不是已经没兴趣往下看了,兄弟,先别着急,如果仅是样我也没必要写这个了。这里介绍的是具有良好的动态属性的能力,看完本文,你会获得很大的收益!

这里会介绍三种动态属性的实现方式

  1. 普遍的
  2. 较好的
  3. 良好的

本文会循序渐进的从普遍的、较好的、良好的顺序来讲代码的演化过程。

一、普遍的

普遍的-类定义

类中申明一个 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");
    }
}

用接口的方式来实现动态属性有很多好处:

  1. 类只要实现接口就能拥有动态属性的功能。
  2. 即使每个类型都需要声明一个对应的类型转换方法,比如上面普遍中提到的 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!似乎张三提了一个问题。

在【良好的】与【普遍的和较好的】实现方案相比较,我们增加了两个类 AttrOptionAttrOptions。本来一个 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", "塔姆");    	
    }
}

我们用代码级别来约束团队的成员 (此时就变成你可以不听团队的规范,但工具不允许你这样做)。

对组合的说明

我们增加了两个类 AttrOptionAttrOptions,本来一个 map 可以解决的事为什么要多增加两个类呢?

复用:组合是在Java中实现程序复用(reusibility)的基本手段之一。

单一职责:一个类只做一件事

AttrOption:负责属性名和类型明确,实际上我们还可以扩展一些默认值。

AttrOptions:负责管理 AttrOption

类的复杂性降低,实现什么职责都有明确的定义;
逻辑变得简单,类的可读性提高了,而且,因为逻辑简单,代码的可维护性也提高了;
变更的风险降低,因为只会在单一的类中的修改。
类的每个责任都有改变的潜在区域。超过一个责任,意味着超过一个改变的区域。
这个原则告诉我们,尽量让每一个类保持单一责任。

对于 AttrOptionsAttrOption 我们还可以单独的拿出来使用,就像下面这样。

在测试的角度也更轻了些!

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);
    }
}

通过 “良好的 ” 动态属性实现方式,我们做到了类型的明确

我们增加了两个类 AttrOptionAttrOptions,做到了规范编码、单一职责、复用,真棒!

之后

我们还想让其他类具有动态属性,只需实现接口和声明一个 AttrOptions 变量就可以了,是不是很简单。

@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TigerAttrOptionDynamic implements AttrOptionDynamic {
    /** 动态属性项集 */
    final AttrOptions options = new AttrOptions();
}

源码参考地址:

网络游戏框架-文档

网络游戏框架-源码

最后

本文可以转载,但必须保留所有内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值