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:
-
number
primitive value of type integer -
list
collection of primitive integers -
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.