自定义数据操纵器

自定义数据的核心就是 DataManipulator 类。在实现它之前,你首先需要决定一下你的自定义数据是否需要一个单独的 API。一般说来,最好的方式是把 API 和其实现分离(SpongeAPI 就是这么做的),不过如果其他的开发者不会接触到它,那么对于这两者而言,使用同一个类也没什么问题。

你可能需要为你的数据的每一个“单元”定义一些 API 方法,如 StringintItemStack 、或者其他的自定义类型如 Home 。这些单元会被包装到相应的 Value 中去,这样你就可以通过 Key 来访问它们。有若干取决于包含的对象的 Value 的实现,如用于基础的键值对操作的 MapValue ,还有可用于根据提供的 Comparable 对象如整数设置上界和下界的 BoundedComparableValue

现在我们考虑你需要继承实现的 AbstractData 类型。虽然你可以从零开始,但这些类型已经为你减少了 很多 诸如实现需要的方法的工作。你可以在 org.spongepowered.api.data.manipulator.mutable.common 里找到一个完整的列表。请参阅 单一数据类型 或者 复合数据类型 以了解关于每一种类型的实现细节。

你需要创建两个不同的类——一个用于实现可变的数据操纵器,也就是实现 DataManipulator 和你的抽象类型,一个用于不可变数据操纵器,也就是实现 ImmutableDataManipulator 和你的 不可变 抽象类型。

注解

数据 API 要求 所有 的数据都必须实现可变的和不可变的类型,所以请乖乖地把它们俩都实现了。

对于所有类型,你都需要定义 DataManipulator#asImmutable()asMutable() 两个方法——你只需要把当前的对象传入另一种类型的类的构造方法就行了。

数据值

你的相应 Getter 需要返回一个数据值,不过如下所示,我们为你提供了 ValueFactory 帮你代劳一部分工作。因为 Sponge 已经实现了一些 Value 对象,所以说这可以大大减少不必要的数据值类型。产生不同类型的数据值取决于使用不同的方法,如 createMapValuecreateBoundedComparableValue 等。

代码示例:实现一个数据值的相关 Getter

import org.spongepowered.api.Sponge;
import org.spongepowered.api.data.value.ValueFactory;
import org.spongepowered.api.data.value.mutable.Value;

import org.spongepowered.cookbook.myhomes.data.home.Home;
import org.spongepowered.cookbook.myhomes.data.Keys;

@Override
protected Value<Home> defaultHome() {
    return Sponge.getRegistry().getValueFactory()
            .createValue(Keys.DEFAULT_HOME, getValue(), null);
}

请注意 ImmutableDataManipulator 需要返回 ImmutableValue 类的实例,不过你可以调用返回的 ValueasImmutable() 方法解决这一问题。我们强烈建议你对不可变数据值实施缓存(如存储入一个类的字段)。

每种类型的数据值也都需要一个数据键,也就是 Key 类用于唯一标识符,你可以看看下面的 Keys.DEFAULT_HOME 的示例。和数据值类似,你可以通过使用 KeyFactory 类的诸如 makeXKey() 的方法以创建你自己的数据键。

你需要传入一个 TypeToken 用于代表你的数据值包装的 原生 类型,以及另一个 TypeToken 代表数据值类型本身。你同时需要提供一个 DataQuery ——这通常用于序列化你的数据值。还要为你的数据类型提供一个唯一的标识 ID 及名称。把这些放到一起你就可以得到一个可用于你的 ValueKey 了。

代码示例:创建数据键

import org.spongepowered.api.data.DataQuery;
import org.spongepowered.api.data.key.Key;
import org.spongepowered.api.data.key.KeyFactory;
import org.spongepowered.api.data.value.mutable.Value;
import org.spongepowered.api.data.value.mutable.Value;

import com.google.common.reflect.TypeToken;

import org.spongepowered.cookbook.myhomes.data.home.Home;

public static final Key<Value<Home>> DEFAULT_HOME = KeyFactory.makeSingleKey(
        TypeToken.of(Home.class),
        new TypeToken<Value<Home>>() {},
        DataQuery.of("DefaultHome"), "myhomes:default_home", "Default Home");

注解

服务端实现需要用到 TypeToken 以指示你的数据值的泛型类型。你可以选择下面两种方式的一种来完成创建工作:

  • 对于不包含泛型的类型,请使用 TypeToken.of(MyType.class)

  • 如果类型包含泛型,你可以创建一个匿名类的实例: TypeToken<MyGenericType<String>>() {}

数据序列化

如果你希望你的数据可 serializableDataHolder 或者配置文件中,你必须还要实现 DataSerializable#toContainer() 方法。我们十分建议你调用父类的 super.toContainer() ,这样子就会包含 DataSerializable#getContentVersion() 提供的版本号信息。每次数据信息的结构发生变化时你应该提升你的版本号信息,并使用 DataContentUpdater 类 以保证向前兼容。

注解

对于单一数据类型你不需要这么做,因为它已经实现 toContainer() 方法了

代码示例:实现 toContainer 方法

import org.spongepowered.api.data.DataContainer;

import org.spongepowered.cookbook.myhomes.data.Keys;

@Override
public DataContainer toContainer() {
    DataContainer container = super.toContainer();
    // This is the simplest, but use whatever structure you want!
    container.set(Keys.DEFAULT_HOME.getQuery(), this.defaultHome);
    container.set(Keys.HOMES, this.homes);

    return container;
}

单一数据类型

实现单一数据类型(Single Type)需要的工作要少些,因为很多方法都已经被你需要继承实现的 AbstractSingleData 类实现了。

简单数据类型实现起来难度最低,不过只限于以下类型:

  • Boolean

  • Comparable

  • Integer

  • List

  • Map

  • CatalogType

  • Enum

对于所有其他类型,你都需要通过继承实现 AbstractSingleData 类来实现它们。这允许你定义你自己的单一数据类型,而不受数据类型的约束,不过你就需要做较多的工作了。

小技巧

默认的实现类已经在构造方法中把你的对象准备好了。你可以通过 getValue() 或者 getValueGetter() 方法获取到它。

简单数据类型

对于简单数据类型(Simple Single Type)而言,几乎所有的工作都已由对应的抽象类实现完成了。你需要做的只有:

  • 继承相应的抽象类

  • 将该数据操纵器对应的 Key 、对象本身、以及默认的对象(如果对象本身是 null 的话)传入构造方法

AbstractBoundedComparableData (及对应的不可变实现)需要额外的最小值和最大值用于比较,相应的 Comparator 也需要传入。

注解

对于 ListMapped 单一数据类型而言,相应的 ListDataMappedData (及它们的不可变版本)都需要实现。一些专用于 List 和 Map 的数据操作方法都会被额外加入到 DataManipulator 中。

下面三个方法必须由可变数据操纵器实现:

fill(DataHolder, MergeFunction) 方法用于把自己的数据操纵器中的数据,通过 MergeFunction#merge() 方法替换成给定 DataHolder 中的数据。

import org.spongepowered.api.data.DataHolder;
import org.spongepowered.api.data.merge.MergeFunction;

import org.spongepowered.cookbook.myhomes.data.friends.FriendsData;

import java.util.Optional;

@Override
public Optional<FriendsData> fill(DataHolder dataHolder, MergeFunction overlap) {
    FriendsData merged = overlap.merge(this, dataHolder.get(FriendsData.class).orElse(null));
    setValue(merged.friends().get());

    return Optional.of(this);
}

from(DataContainer) 用于基于给定的 DataContainer 覆盖已有数据,如果相应数据不存在则返回 Optional.empty()

import org.spongepowered.api.data.DataContainer;
import org.spongepowered.api.data.DataQuery;

import org.spongepowered.cookbook.myhomes.data.Keys;
import org.spongepowered.cookbook.myhomes.data.friends.FriendsData;
import org.spongepowered.cookbook.myhomes.data.friends.ImmutableFriendsData;

import com.google.common.collect.Maps;

import java.util.Optional;
import java.util.UUID;

@Override
public Optional<FriendsData> from(DataContainer container) {
    if(container.contains(Keys.FRIENDS)) {
        List<UUID> friends = container.getObjectList(Keys.FRIENDS.getQuery(), UUID.class).get();
        return Optional.of(setValue(friends));
    }

    return Optional.empty();
}

copy() ,顾名思义,返回一个当前数据操纵器的副本。

import org.spongepowered.cookbook.myhomes.data.friends.FriendsData;

@Override
public FriendsData copy() {
    return new FriendsDataImpl(getValue());
}

自定义单一数据类型

除了上面提到的方法外,你还需要覆写两个方法:

getValueGetter() 方法的返回值需要返回一个可以代表你的数据的 Value (见上)。

toContainer() 方法需要返回一个代表你的数据的 DataContainer (见上)。

复合数据类型

和只能提供一种数据值的单一数据类型相比,复合数据类型可以提供多个“复合”的数据值。如果多个数据值是强关联的,像 FurnaceData 一样,那么复合数据类型便可大显身手,不过,实现复合数据类型,也就会变得更复杂一些。

首先请定义你的数据操纵器的所有数据值的 Getter。对于每一个数据值,请再创建相应的 裸露在外的 对象的 Getter 和 Setter。你马上就会用到它们。对于不可变数据操纵器来说,只有 Getter 是需要实现的。

注册数据值

然后你需要执行注册操作,这样基于 数据键 的系统才能在其上工作。如欲注册,请分别针对可变和不可变数据操纵器实现 DataManipulator#registerGettersAndSetters()ImmutableDataManipulator#registerGetters() 方法。

对于任何一个数据值,你都需要调用的有:

  • registerKeyValue(Key, Supplier) ,并传入数据键和相应的数据值的 Getter

  • registerFieldGetter(Key, Supplier) ,并传入数据键和相应的 裸露在外的 对象的 Getter

  • registerFieldSetter(Key, Consumer) 并传入数据键和相应的 Setter,如果调用的对象是一个可变的数据操纵器的话

我们十分建议使用 Java 8 的 :: 运算符以方便地传入 SupplierConsumer

代码示例:实现相应的 Getter 和 Setter

import org.spongepowered.cookbook.myhomes.data.Keys

// registerGetters() for immutable implementation
@Override
protected void registerGettersAndSetters() {
    registerKeyValue(Keys.DEFAULT_HOME, this::defaultHome);
    registerKeyValue(Keys.HOMES, this::homes);

    registerFieldGetter(Keys.DEFAULT_HOME, this::getDefaultHome);
    registerFieldGetter(Keys.HOMES, this::getHomes);

    // Only on mutable implementation
    registerFieldSetter(Keys.DEFAULT_HOME, this::setDefaultHome);
    registerFieldSetter(Keys.HOMES, this::setHomes);
}

fill(DataHolder, MergeFunction)from(DataContainer) 两个方法和实现单一数据类型类似,只不过需要考虑保存和加载多个数据值。