我不太了解您的设计目标,所以也许并非所有这些都是正确的或对您直接有用,但这是一些可以玩的想法。
首先,我要指出,您显示的代码中有很多字段应该被标记final
。例如,Key.mId
,Key.mLabel
,FloatKey.mMin
,FloatKey.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 = value
为itemY = value
,但如果您只需要编写一次它可能是可以接受的。验证代码可以放在 Value 类或 DataSet 类上。