3

问题

我不知道为我的数据建模的最佳方法。我担心我目前的方法变得过于复杂,我现在想在我基于它的任何代码之前纠正它。

要建模的数据

我有包含 50 多个不同数据项的数据集。每个项目包括:

  • 唯一标识符int
  • 一个标签String
  • 验证标准(最小值、最大值、合法字符等)。
  • 一个值Float, Long, Integer, String, 或Date.

每个项目的标签和验证标准在每个数据集中都是相同的只有值是动态的。顺序并不重要。

需要的用法示例

将数据添加到数据集

dataSet.put(itemIdentifier, value);

遍历并验证数据集中所有非空值

for (DataItem item : dataSet.values()) {
    boolean valid = item.validate();
    if (valid) {...}
}

显示给定数据集中的指定项目

public void displayData(List<DataSet> dataSets, int... itemsIdentifiers) {...}

实施尝试

我当前的实现有一个抽象Key类作为地图的“关键”。每种类型都根据自己的验证需求进行子类化。然后,在DataSet课堂上,我有public static每个项目的钥匙。

abstract public class Key {
    public int mId;
    public String mLabel;

    public Key(int id, String label) {...}
    abstract public boolean validate(Object Value);
}

public class FloatKey extends Key {
    private int mMin, mMax;

    public Key(int id, String label, int min, int max) {...}
    public boolean validate(Object Value) {...}
}

// one for each type
...

public class DataSet {
    public static Key ITEM_A = new FloatKey(1, "item A", 0, 100);
    public static Key ITEM_B = new DateKey(2, "item B", "January 1, 1990");
    // ~50 more of these

    private Map<Key, Object> mMap;

    public void put(int itemId, Object value) {...}
    public Set<Object> values() {...};
    ...
}

我不喜欢这样,当我从 中提取值时DataSet,我需要抓住值和键,这样我才能做类似的事情DataSet.ITEM_A.validate(someFloat)instanceof当我遍历集合中的对象时,我还发现自己经常使用和强制转换,因为在某些情况下我需要调用仅子类的方法。


进一步澄清的编辑

  • 数据项及其验证标准将需要偶尔更改,因此维护应该相对容易/轻松。

  • 虽然我可以将Key对象本身用作映射中的键,但有时我需要将这些键放入Bundle(android API 的一部分)中。我宁愿使用labelor id(如果标签相同)来避免让我的Keyclass Parcelable.

4

2 回答 2

1

这种方法怎么样:创建这个接口:

interface Validable {
    boolean isValid();
}

然后,所有数据项都继承以下类并隐含接口::

abstract class DataItem implements Validable {

    public DataItem(int id, String label, int min, int max) {
    }
}

通过构造函数参数配置 DataItem 的每个特定实例,传递公共值和不同值:

class FloatItem extends DataItem {

    public FloatItem(int id, String label, int min, int max, Float value) {
        super(id, label, min, max);
        // set the Float value here
    }

    @Override
    public boolean isValid() {
        // validate here
        return true;
    }
}

class DateItem extends DataItem {

    public DateItem(int id, String label, int min, int max, Date value) {
        super(id, label, min, max);
    }

    @Override
    public boolean isValid() {
        // validate here
        return true;
    }
}

客户端代码将像这样组装对象::

List<Validable> items = Lists.<Validable>newArrayList(new FloatItem(0, "", 0, 0, Float.NaN),
            new DateItem(0, "", 0, 0, new Date()));

(注意谷歌番石榴的用法)

调用代码只需要这样做::

for (Validable validable : items) {
    System.out.println(validable.isValid());
}

请注意,这种方法要求您首先创建“目标”对象,然后询问它们是否有效。换句话说,您通过构造函数传递有效参数,然后询问对象是否有效。对象本身将使用其中的验证标准回答问题......我希望我正确理解了您的问题。

于 2013-10-10T17:44:57.933 回答
0

我不太了解您的设计目标,所以也许并非所有这些都是正确的或对您直接有用,但这是一些可以玩的想法。

首先,我要指出,您显示的代码中有很多字段应该被标记final。例如,Key.mIdKey.mLabelFloatKey.mMinFloatKey.mMax,所有DataSet.ITEM_X, 和DataSet.mMap。对它们进行标记final(1) 可以更好地传达预期的行为,(2) 防止发生诸如密钥mId更改之类的事故,以及 (3) 可能具有边际性能优势。

我想知道为什么您需要每个键/字段的数字 ID?如果它们需要与已经定义了这些 ID 的某些外部应用程序或存储格式进行交互,那是有道理的,但如果它仅用于类似这种方法的内部事物:

public void displayData(List<DataSet> dataSets, int... itemsIdentifiers) {...}

那么使用字符串标签或键对象列表而不是数字 ID 可以更有意义地实现。同样,DataSet.put可能会使用 Key 或 label 而不是 ID。

当我遍历集合中的对象时,我发现自己经常使用 instanceof 和强制转换

制作Key泛型可以消除一些强制转换。(好吧,它们仍然会出现在字节码中,但不会出现在源代码中,因为编译器会处理它。)例如,

abstract public class Key<T> {
    ...
    abstract public boolean validate(T Value);
}

public class FloatKey extends Key<Float> {
    ...
    public boolean validate(Float value) { ... }
}

在该validate方法中,您因此避免了强制转换的需要value

另外,我猜你目前在类上有这样的方法DataSet

public Object get(int itemId) { ... }

如果您使用 Key 而不是数字 ID 来检索值,并使方法通用,您通常可以避免调用者需要转换返回值(尽管转换仍然存在于get方法中):

public <T> T get(Key<T> key) { ... }

当我从 DataSet 中提取值时,我不喜欢这样,我需要保留值和键,这样我才能执行DataSet.ITEM_A.validate(someFloat).

您可以为值而不是键创建一个类。例如,

abstract public class Value<T> {
    public final int id;
    public final String label;

    protected Value(int id, String label) {
        this.id = id;
        this.label = label;
    }

    abstract public T get();
    abstract public void set(T value);
}

public class FloatValue extends Value<Float> {
    private final float min, max;
    private float value;

    public FloatValue(int id, String label, float min, float max, float value) {
        super(id, label);
        this.min = min;
        this.max = max;
        set(value);
    }

    public Float get() { return value; }

    public void set(Float value) {
        if (value < min | value > max) throw new IllegalArgumentException();
        this.value = value;
    }
}

public class DataSet {
    public final FloatValue itemA = new FloatValue(1, "item A", 0, 100, 0);
    ...

}

这解决了上述问题,并且还消除了以前每次获取/设置值所需的映射查找。但是,它具有复制标签和数字 ID 存储的副作用,因为值类不再是静态字段。

在这种情况下,要通过标签(或 ID?)访问 DataSet 值,您可以使用反射来构建映射。在数据集类中:

private final Map<String, Value<?>> labelMap = new HashMap<>();
{
    for (Field f : DataSet.class.getFields()) {
        if (Value.class.isAssignableFrom(f.getType())) {
            Value<?> v;
            try {
                v = (Value<?>)f.get(this);
            } catch (IllegalAccessException | IllegalArgumentException e) {
                throw new AssertionError(e); // shouldn't happen
            }
            labelMap.put(v.label, v);
        }
    }
}

这里有一个微妙之处:如果您将 DataSet 子类化以表示不同类型的数据,那么在 DataSet 的初始化程序构建映射时,子类的 Value 字段还没有被初始化。因此,如果您创建 DataSet 的子类,您可能需要init()从子类构造函数调用受保护的方法,以告诉它(重新)构建地图,这有点难看,但它会起作用。

您可以重复使用此映射来提供 DataSet 值的方便迭代:

public Collection<Value<?>> values() {
    return Collections.unmodifiableCollection(labelMap.values());
}

最后一个想法:如果您仍然使用反射,则可以使用普通字段作为值,并使用注释接口来实现它们的行为。

import java.lang.annotation.*;
import java.lang.reflect.*;

public class DataSet {
    @Label("item A") @ValidateFloat(min=0, max=100) public float itemA;
    @Label("item B") public String itemB;

    @Retention(RetentionPolicy.RUNTIME)
    public static @interface Label {
        String value();
    }

    @Retention(RetentionPolicy.RUNTIME)
    public static @interface ValidateFloat {
        float min();
        float max();
    }

    public final class Value {
        public final String label;
        private final Field field;

        protected Value(String label, Field field) {
            this.label = label;
            this.field = field;
        }

        public Object get() {
            try {
                return field.get(DataSet.this);
            } catch (IllegalArgumentException | IllegalAccessException e) {
                throw new AssertionError(e); // shouldn't happen
            }
        }

        public void set(Object value) {
            try {
                field.set(DataSet.this, value);
            } catch (IllegalArgumentException | IllegalAccessException e) {
                throw new AssertionError(e); // shouldn't happen
            }
        }

        public void validate() {
            Object value = get();

            // Test for presence of each validation rule and implement its logic.
            // Ugly but not sure how best to improve this...

            if (field.isAnnotationPresent(ValidateFloat.class)) {
                float floatValue = (float)value;
                ValidateFloat rule = field.getAnnotation(ValidateFloat.class);
                if (floatValue < rule.min() || floatValue > rule.max()) {
                    //throw new Whatever();
                }
            }

            //if (field.isAnnotationPresent(...)) {
            //  ...
            //}
        }
    }

    private final Map<String, Value> labelMap = new HashMap<>();
    {
        for (Field f : DataSet.class.getFields()) {
            if (f.isAnnotationPresent(Label.class)) {
                Value value = new Value(f.getAnnotation(Label.class).value(), f);
                labelMap.put(value.label, value);
            }
        }
    }

    public Collection<Value> values() {
        return Collections.unmodifiableCollection(labelMap.values());
    }
}

这种方法有不同的权衡。确切知道它想要什么字段的代码可以直接访问它。例如,dataSet.itemA代替dataSet.get(DataSet.ITEM_A). 需要迭代多个字段的代码通过 Value 包装器(Property是更好的类名?或Item)来实现,它封装了字段反射代码的丑陋。

我还将验证逻辑放入注释中。如果有很多字段具有非常简单的数字限制,那效果很好。如果它太复杂,您最好使用DataSet.validate直接访问字段的方法。例如,

public void validate() {
    if (itemC < 10 || itemC > itemD) ...
}

好的,还有一个想法:

public class DataSet {
    public float itemA;
    public String itemB;


    public static abstract class Value<T> {
        public final String label;

        protected Value(String label) {
            this.label = label;
        }

        public abstract T get();
        public abstract void set(T value);
    }


    public Value<?>[] values() {
        return new Value[] {
            new Value<Float>("itemA") {
                public Float get() {
                    return itemA;
                }

                public void set(Float value) {
                    itemA = value;
                }
            },

            new Value<String>("itemB") {
                public String get() {
                    return itemB;
                }

                public void set(String value) {
                    itemB = value;
                }
            },
        };
    }
}

这很简单(没有注释或反射),但它是重复的。由于您有“50+”字段,重复性可能并不理想,因为在某些时候复制粘贴很容易滑倒,忘记替换itemX = valueitemY = value,但如果您只需要编写一次它可能是可以接受的。验证代码可以放在 Value 类或 DataSet 类上。

于 2013-10-10T18:37:19.883 回答