4

I'm working on an application which writes on a NoSQL database (Elasticsearch to be precise) and I have to manage more than a dozen (the number grows with time) different document classes, that is classes with numerous fields, their getters and setters and methods to convert the class to a JSONObject (each field is annotated with @JsonProperty(PROPERTY_NAME) and we use a JSON parser ).

All these classes have some fields and methods in common, which are all contained in a superclass (let us call it DocZero) but they all have their own custom fields, which brings me to the point.
It is true that these fields are custom, but some are shared between different classes in a non-linear way, that is I have my document classes Doc1, ... DocN and I have some sets of fields (around 10 as of right now) shared by them in a very wild way.

Some examples to best convey the situation:
Doc1 contains Set1 to Set5;
Doc2 contains Set1, Set2 and Set5 to Set8;
Doc3 contains Set6 and Set7;
Doc4 contains Set5 and Set7;
Doc5 contains Set1, Set2, Set5 and Set7 to Set10.

Given that I need to get and set these fields and, from time to time, manipulate a document with them, I made interfaces out of the Set#, each containing (abstract) setters and getters.
As such, when I declare a class

public class DocX implements SetA, SetB, SetC

I get reminded to implement the methods and hence add the required fields, but this means that all the classes implementing the same set will need to have the same parameters and the same methods which means that I need to write the same code many times (sometimes more than getter and setter methods).

Adding all the fields to DocZero foregoing the different Doc# classes is a solution which I am not keen on using, since I prefer to distinguish different document types and since this situation is present, in lower magnitude, in another section of the code, with AnotherDocZero, AnotherDoc# and AnotherSet# for which merging cannot be done due to other constraints and for which I would like a potential solution to work too.
I feel like this is one of those situation where multiple inheritance would solve the issue, but unfortunately Java doesn't allow it.

How could I avoid duplication in a situation like this? Have you got any advice to improve my handling of this issue?

Ukitinu
  • 53
  • 2
  • 9
  • While I don't know how to avoid duplication of simple getters/setters, duplication of non-trivial parts can be avoided via delegation: https://stackoverflow.com/questions/13245610/what-is-the-difference-between-inheritance-and-delegation-in-java –  Sep 04 '19 at 08:45
  • 1
    You can extract sets of fields in their own types, and compose actual docs of these object, so it is actually modelled as if the `Doc2 contains Set2`, with `Set2` being an actual class instance. – M. Prokhorov Sep 04 '19 at 08:48
  • @dyukha: I should have been more clear when I said "non-trivial parts", but the thing is that, when I'm dealing with a collection, I prefer, instead of setters, to implement `add` methods (and hence first check if the parameter is non-null). Other non-trivial methods are managed elsewhere. – Ukitinu Sep 04 '19 at 09:10
  • @M.Prokhorov: Are you suggesting to create classes with the parameters required by the interface and then using something like `@JsonUnwrap` to still obtain a flat JSON? I remember doing something like this a while ago, when I first tried to tackle the problem without interfaces, but then abandoning it due to other complication (I can't remember what these were). – Ukitinu Sep 04 '19 at 09:27
  • @Phraeq, basically, yes, I was suggesting something along those lines. Having an enclosed object with fields allows you to handle what's required and such in a single place, and make it applicable to all docs. – M. Prokhorov Sep 04 '19 at 09:33
  • @M.Prokhorov: I think I still had code duplication problems, but I can't put my finger on what those were or from where they originated. Could have been bad implementation on my part, I could give this approach another try in the future. – Ukitinu Sep 04 '19 at 09:38
  • 1
    @Phraeq, probably you had dupe problems because you delegated to methods of `SetX` object. I'm suggesting you just have `getSetX()` on your docs. – M. Prokhorov Sep 04 '19 at 09:40
  • @M.Prokhorov, it took me a while to find the old implementation, but it turns out that you were right, I did exactly that. – Ukitinu Sep 06 '19 at 15:15

4 Answers4

3

I strongly suggest to keep your data classes simple even if it does mean that you will need to repeat many fields definitions - POJOs are definitely easier to maintain and understand how the "result" data object looks like if you have all fields in one place - multilevel inheritance will quickly create a mess

For constraints of having proper getters you should use interfaces as you do. You can even create single interface for every getter and group them in another one like

public interface Set1To5 extends Set1, Set2, Set3, Set4, Set5 {}

For avoid duplication of getters/setters you can use some additional lib like lombok or consider not using getters/setters at all (just make all the fields in your data document classes public - but this one of course is not the option if you need to constraint classes with interfaces)

m.antkowicz
  • 13,268
  • 18
  • 37
  • Thank you for the lombok recommendation, I'll check it as soon as I can. As far as "multiple interfaces" go, given the high variability of "`Set#`" interfaces implementation, I would have to create too many "collections" of (not really related) interfaces which is not worth it. – Ukitinu Sep 04 '19 at 09:21
3

If several kinds of fields are often grouped together, that suggests that grouping is a natural part of the domain of your program, and should be represented as such.

So, if you often find this in your classes

   int xCoordinate;
   int yCoordinate;

You should instead introduce

public final class Point ... {
   private final int x;
   private final int y;

   Point(int x, int y) {
      ...
   }

   ...
}

then instead of repeating x and y, write

   Point position;
Raedwald
  • 46,613
  • 43
  • 151
  • 237
  • 1
    That would indeed be the standard pattern for code re-use (composition) to reach for. But in this case, one has to consider the needs of the database, and the layout of the fields in there may be out of our control (and we have to flatten them in the model class). – Thilo Sep 04 '19 at 09:11
  • @Thilo some ORMs support embedded objects, take a look at JPA's `@Embeddable` – Adrian Sep 04 '19 at 09:25
  • The issue with this approach is that all the fields that I could group are already grouped in the "sets" interfaces. It is true that in the example I gave set 1 and 2 are always together, but this is a coincidence more than a pattern, since the fields they represent are completely unrelated. – Ukitinu Sep 04 '19 at 09:34
  • 1
    @Thilo: To flatten them one can use the annotation `@JsonUnwrapped`. I did try this approach in the past (see comment under the question) but I had to abandon it due to some issues I can't remember right now. – Ukitinu Sep 04 '19 at 09:36
  • In the end I implemented a solution similar to this, but keeping interfaces to signal the presence of a field/object. So, despite the solution being very subjective, I feel I should mark as accepted your answer, thank you. – Ukitinu Feb 06 '20 at 15:08
2

There is a pattern to explore. I don't know it already exists or there is a specific name for it.

Consider:

  1. Java 8+ interfaces can have default methods. These methods can use other interface methods to define additional / default logic. The class implementing such an interface automatically get these methods, without having to implement them.

  2. Also, a class can implement multiple interfaces.

The above two can be used to have "easy to compose" types in Java.

Example:

Create a base interface that can store/retrieve data. This can be as simple as:

public interface Document {
    <T> T get(String key);
    void set(String key, Object value);
}

This is the basic capability that will be used by all specific data objects.

Now, define two interfaces that contain nothing but specific field getter/setters using the above interface:

public interface Person extends Document {
    default String getName(){
        return get("name");
    }

    default void setName(String name){
        set("name", name);
    }
}

And another one:

public interface Salaried extends Document {
    default double getSalary(){
        return get("salary");
    }

    default void setSalary(double salary){
        set("salary", salary);
    }
}

Get the idea? This is a simple schema built upon the basic get/set capability. And you might want to define field names as constants in real applications.

But so far, it is all interfaces. It is not linked to something real, like a DB. Hence we must define an implementation to Document that uses a DB storage:

public class DBDoc implements Document {
    private final Map<String,Object> data;

    public DBDoc(HashMap<String, Object> data) {
        this.data = new HashMap<>(data);
    }

    public DBDoc(){
        this.data = new HashMap<>();
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T get(String key) {
        return (T) this.data.get(key);
    }

    @Override
    public void set(String key, Object value) {
        this.data.put(key, value);
    }
}

We have used a simple map for storage, but it might as well be using a db connection or db specific document to get/set data. This is up to what DB or storage you are using.

Finally, we have the capability to compose types out of these interfaces:

public class Employee extends DBDoc implements Person, Salaried { }

And use them:

public static void main(String[] args) {
   Employee employee = new Employee();
   employee.setName("Joe");
   employee.setSalary(1000.00);

   System.out.println(employee.getName());
   System.out.println(employee.getSalary());
}
S.D.
  • 29,290
  • 3
  • 79
  • 130
  • This is a very interesting and thoughtful solution. The sole downside I can see right now is that one should implement exactly one interface per field to be completely clear about the fields available in the class. But, as in a lot of other cases, the true solution is to pick you poison and stick with it. All in all, since your answer does exactly what I was looking for I think I'll wait another day or two and then accept it :-) – Ukitinu Sep 06 '19 at 15:07
  • 1
    One downside here is it exposes generic `get()/set()` methods as well, so anyone can just call `set` with garbage field names, and query non-existing fields. – M. Prokhorov Sep 06 '19 at 15:21
  • @M.Prokhorov I think that's avoidable with package level scoping of Document interface and implementation. – S.D. Sep 06 '19 at 19:11
  • 1
    @S.D. there isn't a way to declare anything on interface as package-scoped, it's members can only be `public`. If you make base interface package-specific, public interfaces such as `Salaried` that extend from the base "re-export" its methods as public in implementations (according to Eclipse compiler, at least). So I don't think there's any tricks to hide generic methods if they are instance methods. – M. Prokhorov Sep 09 '19 at 10:00
  • 1
    @M.Prokhorov Ah, the interface exports methods. That certainly exposes the raw IO methods. – S.D. Sep 09 '19 at 11:00
  • 1
    Since Java 9 (outside of the scope of my question) you can also have private methods in an interfaces, but they cannot be used outside of it. – Ukitinu Sep 11 '19 at 07:14
0

I think default method is an option to go. https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html

nguyentt
  • 649
  • 6
  • 13
  • 3
    If `Set`s are actual fields, default methods do not help. – M. Prokhorov Sep 04 '19 at 08:50
  • @M.Prokhorov: "I get reminded to implement the methods and hence add the required fields, but this means that all the classes implementing the same set will need to have the same parameters and the same methods which means that I need to write the same code many times". If he goes with this solution, default methods help. – nguyentt Sep 04 '19 at 08:53
  • @nguyentt, in any case, your answer lacks details. Please describe exactly how default method helps. –  Sep 04 '19 at 08:56
  • 1
    He's talking about having to write identical code. Default methods help when you need to add no-op methods. In his case best he can do a no-op setter and an always-null getter. And he'll still need the actual implementation in each class, except this time there won't be compiler errors, so he could even miss some instances where he needed to add these fields. In the end he'll have more instances of these methods' implementations, not less. Though default methods aren't true code dupes, that part is true. – M. Prokhorov Sep 04 '19 at 08:56
  • Maybe in my desire to be as abstract as possible I didn't make it clear, but yes, I can't use default methods in interfaces since getters and setters need to access actual non-final parameters, and as far as I know I can't define those in interfaces. Thank you anyway @nguyentt. – Ukitinu Sep 04 '19 at 09:06