这种方式是将可选的数据结构组合成值对象的最佳方式吗?

3

我有一个值对象(Test2),它将保存许多可选的数据结构(实现PropertySet)。我不能将它们全部组合到Test2中,因为会有很多变体,实现所有排列会导致类的扩散。我想出了以下解决方案:

public class Test2
{

    static interface PropertySet
    {

    }

    static class LocationInfo implements PropertySet
    {
        String lat;
        String lng;

        public LocationInfo(String lat, String lng)
        {
            this.lat = lat;
            this.lng = lng;
        }

    }

    private Map<Class<? extends PropertySet>, PropertySet> propertySets = new HashMap<>();

    @SuppressWarnings("unchecked")
    public <T extends PropertySet> T fetchPropertySet(Class<? extends T> propertySetType)
    {
        T result = (T) propertySets.get(propertySetType);
        return result;
    }

    public LocationInfo getLocationInfo()
    {
        return this.<LocationInfo> fetchPropertySet(LocationInfo.class);
    }

    public static void main(String[] args)
    {
        Test2 test = new Test2();
        test.propertySets.put(LocationInfo.class, new LocationInfo("1", "1"));

        LocationInfo locationInfo = test.<LocationInfo> fetchPropertySet(LocationInfo.class);
        System.out.println(locationInfo.lat + ", " + locationInfo.lng);

        LocationInfo locationInfo2 = test.getLocationInfo();
        System.out.println(locationInfo2.lat + ", " + locationInfo2.lng);
    }
}

我的问题是,这种解决方案是否被认为是这种问题的良好实践?

请注意,我不能使用外部库,如Guava,但使用了Java 8。

3个回答

2
如果您有很多可选属性的类型,使用映射是一种足够的方式。然而,问题在于您使用属性的类型作为键。与用名称和类型区分的普通字段/属性相比,即可以具有相同类型的多个属性。你的LocationInfo示例显示了这一点。它表示没有特定含义的位置。 完全可以想象存在两个类型为LocationInfo的属性的实体,例如 startLocationendLocation
因此,您不应混淆使用类型定义属性和表示值的类型。也就是说,对于interface PropertySet,没有理由存在。它对类型没有任何价值,只是一个不必要的限制。
为了解决这个问题,使用同时包含属性名称和类型的键。
public class Test2
{
    static class LocationInfo
    {
        String lat;
        String lng;

        public LocationInfo(String lat, String lng)
        {
            this.lat = lat;
            this.lng = lng;
        }
    }

    private static final class PropKey {
        final Class<?> type;
        final String name;

        public PropKey(Class<?> type, String name) {
            this.type = Objects.requireNonNull(type);
            this.name = Objects.requireNonNull(name);
        }
        @Override
        public int hashCode() {
            return Objects.hash(name, type);
        }
        @Override
        public boolean equals(Object obj) {
            if(obj==this) return true;
            if (obj == null || !(obj instanceof PropKey)) return false;
            final PropKey other = (PropKey) obj;
            return type==other.type && name.equals(other.name);
        }
    }
    private final Map<PropKey, Object> properties = new HashMap<>();

    public <T> T fetchProperty(Class<T> type, String name)
    {
        return type.cast(properties.get(new PropKey(type, name)));
    }
    // your decision whether this should be public
    <T> void putProperty(Class<T> type, String name, T value)
    {
        Objects.requireNonNull(value);
        properties.put(new PropKey(type, name), value);
    }

    public LocationInfo getPosition()
    {
        return fetchProperty(LocationInfo.class, "position");
    }

    public static void main(String[] args)
    {
        Test2 test = new Test2();
        test.putProperty(LocationInfo.class, "position", new LocationInfo("1", "1"));

        LocationInfo locationInfo = test.fetchProperty(LocationInfo.class, "position");
        System.out.println(locationInfo.lat + ", " + locationInfo.lng);

        LocationInfo locationInfo2 = test.getPosition();
        System.out.println(locationInfo2.lat + ", " + locationInfo2.lng);

        test.putProperty(String.class, "debugInfo", "hello world");
        System.out.println(test.fetchProperty(String.class, "debugInfo"));
    }
}

正如已经提到的那样,实体和其属性类型之间不应该有紧密的联系,因此您可以将 LocationInfo 类转换为所有包含纬度/经度对的用例的顶级类型。强烈建议使用不可变值类型模式来处理此类,因为您要存储在地图中的所有属性类型都需要使用此模式,而通用方法无法创建防御性副本以保护多个 Test2 实例之间的可变对象的错误共享。
另一个需要考虑的事情是,由于属性是可选的,getter 方法可能会返回一个 Optional<PropertyType> 而不是使用 null 来编码属性的缺失。

我理解你提到的多个实例的观点。由于这将是MVP的一部分,我将尽可能简单地完成它。关于“Optional”的评论确实非常好,谢谢你指出来。我还没有习惯新的Java特性。 :) - Adam Arold
2
顺便提一下,注意 type.cast(…) 如何使 @SuppressWarnings("unchecked") 变得不必要。 - Holger

0

Guava有一个ClassToInstanceMap,您可以使用它来存储类->该类的实例的映射:

ClassToInstanceMap<PropertySet> map = MutableClassToInstanceMap.create();
map.putInstance(LocationInfo.class, new LocationInfo("1", "1"));

LocationInfo li = map.getInstance(LocationInfo.class);

你能在答案中举一个非常简单的例子吗,这样我就可以接受它了吗? - Adam Arold

0

我建议

public <T extends PropertySet> T fetchPropertySet(Class<T> propertySetType)

(消除通配符)

这样你就不需要显式指定泛型参数,也可能不需要像getLocationInfo这样的包装器。而且你仍然可以将返回的T实例用作T的超类。

关于你的意图有一些问题:你是否需要同一个类的多个实例?对于子类和超类,你打算怎么处理?目前的情况是,每个类都会有自己的实例。你可能需要为所有超类(或其中一些超类)提供最派生类的单个实例。


该类将仅有一个实例,且没有子类计划。这基本上是用于分组数据组合。 - Adam Arold

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接