开发者

Best way to create the behavior of an extendable Enum in java

开发者 https://www.devze.com 2023-04-10 05:29 出处:网络
I want to create something that resembles an extendable Enum (understanding extending Enums isn\'t possible in Java 6).

I want to create something that resembles an extendable Enum (understanding extending Enums isn't possible in Java 6).

Here is what im trying to do:

I have many "Model" classes and each of these classes have a set of Fields that are to be associated with it. These Fields are used to index into Maps that contain representations of the data.

I need to be able to access the Fields from an Class OR instance obj as follows:

MyModel.Fields.SOME_FIELD #=> has string value of "diff-from-field-name"

or

开发者_开发问答
myModel.Fields.SOME_FIELD #=> has string value of "diff-from-field-name"

I also need to be able to get a list of ALL the fields for Model

MyModel.Fields.getKeys() #=> List<String> of all the string values ("diff-from-field name")

When defining the "Fields" class for each Model, I would like to be able to keep the definition in the same file as the Model.

public class MyModel {
    public static final Fields extends BaseFields { 
        public static final String SOME_FIELD = "diff-from-field-name";
        public static final String FOO = "bar";
    }

    public Fields Fields = new Fields();

    // Implement MyModel logic
}

I also want to have OtherModel extends MyModel and beable to inherit the Fields from MyModel.Fields and then add its own Fields on top if it ..

public class OtherModel extends MyModel {
   public static final class Fields extends MyModel.Fields { 
        public static final String CAT = "feline";
        ....

Which woulds allow

OtherModel.Fields.CAT #=> feline
OtherModel.Fields.SOME_FIELD #=> diff-from-field-name
OtherModel.Fields.FOO #=> bar
OtherModel.Fields.getKeys() #=> 3 ["feline", "diff-from-field-name", "bar"]

I am trying to make the definition of the "Fields" in the models as clean and simple as possible as a variety of developers will be building out these "Model" objects.

Thanks


I need to be able to access the Fields from an Class OR instance obj as follows:

  MyModel.Fields.SOME_FIELD #=> has string value of "diff-from-field-name"

That is not possible in Java unless you use a real enum or SOME_FIELD is a real field. In either case, the "enum" is not extensible.

The best you can do in Java 6 is to model the enumeration as mapping from String names to int values. That is extensible, but the mapping from names to values incurs a runtime cost ... and the possibility that your code will use a name that is not a member of the enumeration.


The reason that enum types in Java are not extensible is that the extended enum would break the implicit invariants of the original enum and (as a result) could not be substitutable.


I've just tried out some code trying to do what you've just described and it was really cumbersome.

If you have a Fields static inner class somewhere in a model class like this:

public class Model {

  public static class Fields {

    public static final String CAT = "cat";
    protected static final List<String> KEYS = new ArrayList<String>();

    static {
      KEYS.add(CAT);
    }

    protected Fields() {}

    public static List<String> getKeys() {
      return Collections.unmodifiableList(KEYS);
    }
  }
}

and you extend this class like this:

public class ExtendedModel extends Model {

  public static class ExtendedFields extend Model.Fields {

    public static final String DOG = "dog";

    static {
      KEYS.add(DOG);
    }

    protected ExtendedFields() {}
  }
}

then its just wrong. If you call Model.Fields.getKeys() you'd get what you expect: [cat], but if you call ExtendedModel.ExtendedFields.getKeys() you'd get the same: [cat], no dog. The reason: getKeys() is a static member of Model.Fields calling ExtendedModel.ExtendedFields.getKeys() is wrong because you really call Model.Fields.getKeys() there.

So you either operate with instance methods or create a static getKeys() method in all of your Fields subclasses, which is so wrong I can't even describe.


Maybe you can create a Field interface which your clients can implement and plug into your model(s).

public interface Field {
  String value();
}

public class Model {

  public static Field CAT = new Field() { 
    @Override public String value() {
      return "cat";
    }
  };

  protected final List<Field> fields = new ArrayList();

  public Model() {
    fields.add(CAT);
  }

  public List<Field> fields() {
    return Collections.unmodifiableList(fields);
  }      
}

public class ExtendedModel extends Model {

  public static Field DOG= new Field() { 
    @Override public String value() {
      return "dog";
    }
  };

  public ExtendedModel() {
    fields.add(DOG);
  } 
}


I wonder whether you really need a generated enumeration of fields. If you are going to generate a enum of a list the fields based on a model, why not generate a class which lists all the fields and their types? i.e. its not much harder to generate classes than staticly or dynamically generated enums and it much more efficient, flexible, and compiler friendly.

So you could generate from a model something like

class BaseClass { // with BaseField
    String field;
    int number;
}

class ExtendedClass extends BaseClass { // with OtherFields
    String otherField;
    long counter;
}

Is there a real benefit to inventing your own type system?


I was able to come up with a solution using reflection that seems to work -- I haven't gone through the full gamut of testing, this was more me just fooling around seeing what possible options I have.

ActiveField : Java Class which all other "Fields" Classes (which will be inner classes in my Model classes) will extend. This has a non-static method "getKeys()" which looks at "this's" class, and pulled a list of all the Fields from it. It then checks a few things like Modifiers, Field Type and Casing, to ensure that it only looks at Fields that match my convention: all "field keys" must be "public static final" of type String, and the field name must be all UPPERCASE.

public class ActiveField {
private final String key;

protected ActiveField() {
    this.key = null;
}

public ActiveField(String key) {
    System.out.println(key);
    if (key == null) {
        this.key = "key:unknown";
    } else {
        this.key = key;
    }
}

public String toString() {
    return this.key;
}

@SuppressWarnings("unchecked")
public List<String> getKeys() {
    ArrayList<String> keys = new ArrayList<String>();
    ArrayList<String> names = new ArrayList<String>();

    Class cls;
    try {
        cls = Class.forName(this.getClass().getName());
    } catch (ClassNotFoundException e) {
        return keys;
    }

    Field fieldList[] = cls.getFields();

    for (Field fld : fieldList) {
        int mod = fld.getModifiers();

        // Only look at public static final fields
        if(!Modifier.isPublic(mod) || !Modifier.isStatic(mod) || !Modifier.isFinal(mod)) {
            continue;
        }

        // Only look at String fields
        if(!String.class.equals(fld.getType())) {
            continue;
        }

        // Only look at upper case fields
        if(!fld.getName().toUpperCase().equals(fld.getName())) {
            continue;
        }

        // Get the value of the field
        String value = null;
        try {
            value = StringUtils.stripToNull((String) fld.get(this));
        } catch (IllegalArgumentException e) {
            continue;
        } catch (IllegalAccessException e) {
            continue;
        }

        // Do not add duplicate or null keys, or previously added named fields
        if(value == null || names.contains(fld.getName()) || keys.contains(value)) {
            continue;
        }

        // Success! Add key to key list
        keys.add(value);
        // Add field named to process field names list
        names.add(fld.getName());

    }

    return keys;
}

public int size() {
    return getKeys().size();
} 
}

Then in my "Model" classes (which are fancy wrappers around a Map, which can be indexed using the Fields fields)

public class ActiveResource { /** * Base fields for modeling ActiveResource objs - All classes that inherit from * ActiveResource should have these fields/values (unless overridden) */ public static class Fields extends ActiveField { public static final String CREATED_AT = "node:created"; public static final String LAST_MODIFIED_AT = "node:lastModified"; }

public static final Fields Fields = new Fields();

    ... other model specific stuff ...

}

I can then make a class Foo which extends my ActiveResource class

public class Foo extends ActiveResource {

public static class Fields extends ActiveResource.Fields {
    public static final String FILE_REFERENCE = "fileReference";
    public static final String TYPE = "type";
}

public static final Fields Fields = new Fields();

    ... other Foo specific stuff ...

Now, I can do the following:

ActiveResource ar = new ActiveResource().
Foo foo = new Foo();

ar.Fields.size() #=> 2
foo.Fields.size() #=> 4

ar.Fields.getKeys() #=> ["fileReference", "type", "node:created", "node:lastModified"]
foo.Fields.getKeys() #=> ["node:created", "node:lastModified"]


ar.Fields.CREATED_AT #=> "node:created"
foo.Fields.CREATED_AT #=> "node:created"
foo.Fields.TYPE #=> "type"
etc.

I can also access the Fields as a static field off my Model objects

Foo.Fields.size(); Foo.Fields.getKeys(); Foo.Fields.CREATED_AT; Foo.Fields.FILE_REFERENCE;

So far this looks like a pretty nice solution, that will require minimal instruction for building out new Models.


Curses - For some reason my very lengthy response with the solution i came up with did not post.

I will just give a cursory overview and if anyone wants more detail I can re-post when I have more time/patience.

I made a java class (called ActiveField) from which all the inner Fields inherit. Each of the inner field classes have a series of fields defined:

public static class Fields extends ActiveField {  
     public static final String KEY = "key_value";  
}

In the ActiveRecord class i have a non-static method getKeys() which uses reflection to look at the all the fields on this, iterates through, gets their values and returns them as a List.

It seems to be working quite well - let me know if you are interested in more complete code samples.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号