Getting started - gkresic/commons-fields GitHub Wiki

In this implementation, proposed design pattern is implemented as several Java interfaces with many default methods giving implementing objects support for working with their own attributes (properties). Attributes of each object are referenced by "fields" where one field references exactly one object's attribute.

Fields are implemented as custom enum classes which implement FieldEnum interface and there should be one such declaration for every type of object. Due to some short-comings of Java language (like enum classes being final) there is some unfortunate boilerplate code needed for every field enum declaration, but fortunately it's just one template that can be safely copy/pasted.

For declaring "field graph" which will define which fields in object and its sub objects we want to reference we'll use FieldGraph - immutable hierarchical set with some useful builder patterns. More on this class later.

Base interface where most functionality is implemented is HasFields. It will require you to provide a field enum class and implement several simple methods: getFieldsClass, getFields, setFields, ref and pull. If your object has any kind of unique identifier from which it can be resolved, then use HasEntityFields fields which will require you to implement one additional getter/setter pair (getId and setId) but in exchange will give you additional methods on top of those present in HasFields.

So, before we dive into examples, let's make a distinction between two types of objects: "data" objects that contain values intended for one-time usage (usually something calculated on demand) and "entity" objects which are identified by some kind of "key" from which the whole object can be resolved. Implement HasFields for "data" objects and HasEntityFields for "entity" objects.

Note that most abstract methods from those two interfaces will be generic in your app, so you can easily extract them to one abstract super class or even simpler use provided FieldsObject and FieldsEntity.

Finally, lets see how to add described functionality to one simple class with three attributes:

  1. number primitive value of type integer

  2. list collection of primitive integers

  3. bar sub object whose attributes may be selected via fields, too

Here it is:

// we'll extend FieldsEntity (implements HasEntityFields) for common implementation of id and fields getters/setters
// note that we are opting for String as our ID type
public class Foo extends FieldsEntity<String, Foo, Foo.Field> {

  // enum for describing fields, we'll use one field per attribute
  public enum Field implements FieldEnum {

     number,
     list,
     bar       (Bar.Field.class);  // fields referencing sub-object should specify sub-object's FieldEnum

     // rest of this enum class is boring, copy-pastable template (Java tends to like those...)

     Field() {
        this(null);
     }

     <F extends Enum<F> & FieldEnum> Field(Class<F> clazz) {
        this.clazz = clazz;
     }

     @Override
     @SuppressWarnings("unchecked")
     public <F extends Enum<F> & FieldEnum> Class<F> getFieldsClass() {
        return (Class<F>) clazz;
     }

     private final Class<?> clazz;

  }

  // default constructor that initializes super class
  public Foo() {
     super(Field.class);
  }

  // pull is the main "working" method on which all other methods depend
  // we'll discuss this in more details below
  @Override
  public Object pull(Field field, Foo other, FieldGraph<Field> graph) {
     switch (field) {
        case number:  return pull(other, other::getNumber,  this::setNumber);
        case list:    return pull(other, other::getList,    this::setList,    ArrayList::new);
        case bar:     return pull(other, other::getBar,     this::setBar,     value -> value.clone(field, graph));
     }
     throw new FieldUnavailableException(field);
  }

  // only other object-specific method that should be implemented is simple factory method that produces "empty" instance
  @Override
  public Foo ref() {
     Foo foo = new Foo();
     foo.setId(getId());
     return foo;
  }

  // And now... getters and setters! But with a twist!

  // NOTE: using fieldGet/fieldSet is completely optional, but helps a lot during development
  
  public Integer getNumber() {
     // fieldGet throws FieldUnavailableException if requested field was not previously set
     return fieldGet(Field.number, number);
  }

  public void setNumber(Integer number) {
     // fieldSet makes sure field corresponding to this attribute is correctly set
     this.number = fieldSet(Field.number, number);
  }

  public List<Integer> getList() {
     return fieldGet(Field.list, list);
  }

  public void setList(List<Integer> list) {
     this.list = fieldSet(Field.list, list);
  }

  public Bar getBar() {
     return fieldGet(Field.bar, bar);
  }

  public void setBar(Bar bar) {
     this.bar = fieldSet(Field.bar, bar);
  }

  private Integer number;
  private List<Integer> list;
  private Bar bar;

}

As you can see, besides Field class which is simple enum + boilerplate (and I'm all ears on suggestions on how to implement this better in Java), only concrete method that objects are supposed to implement is pull.

Requirement from this method is that it should copy ('pull') attribute(s) referenced by specified field from another instance (other) obeying following rules:

  • if other == this, don't copy anything, just return current value
  • if graph == null, pull whole sub-tree for every sub-object

Sounds complicated, but most of this logic is implemented in another (provided) method used in example above:

pull(HasFields, Supplier, Consumer)

or

pull(HasFields, Supplier, Consumer, Function)

Given default methods accept other and supplier/consumer pair for field's attribute, optionally with mapper function (used in above example to deep-clone pulled values). Also, if using mapper function for deep-cloning, consider using specialized clone which know how to extract sub-tree from graph, cloning whole sub-tree if graph is null.

Implementing field selector as switch statement will ensure we'll get warning if we miss any field which is very useful when adding new fields and tracing where in your code base you should add logic specific to that field.

And... that's it! At least as far our model classes are concerned.

⚠️ **GitHub.com Fallback** ⚠️