Home > Software design >  Java compare class instances using just interface methods
Java compare class instances using just interface methods

Time:06-10

My application uses interfaces as access control in order to mask some database-specific fields from the application. For example, the PublicFoo interface exposes a subset of data from the DatabaseFoo class.

public interface PublicFoo() {
  void setName(String name);
  String getName();
  void setId(String id);
  String getId();
}

public class DatabaseFoo() implements PublicFoo {
  private String name;
  private String id;

  // I want to hide this database-specific field from the PublicFoo interface that gets passed around my app. 
  private OffsetDateTime lastUpdatedTimestamp;
}

My question is, if I have two instances of the class DatabaseFoo that implement the PublicFoo interface, is there a way to compare these two instances using only the attributes exposed by the getters of the interface? I don't want to override the equals() method of DatabaseFoo because there are cases where I want to compare two DatabaseFoo objects using every field. So I think this rules out default interface methods or abstract classes.

I can accomplish what I want by writing a custom method that calls all the getters and checks for equivalency. However I am wondering if there is a more elegant solution. My fear is that if I use this approach I won't remember to update the custom method as the interface evolves in the future.

CodePudding user response:

Much like overriding equals() you could use the Comparable Interface. If your not going to override equals because of potential future breaking you probably don't want to do compareTo() either and maybe you should settle on a more simplified definition of equals for your objects. Perhaps only comparing location/connection if it is a database.

if ( dataBaseFooA.compareTo(dataBaseFooB) ) { ...}

CodePudding user response:

I currently don't have access to an IDE, so I had to trust my memory and an online Java compiler to do this. Here is a demo.

This is an approach using reflection. It is not particularely pretty, but you can hide it with static access.

public class FancyEquals {

private FancyEquals() {}

/**
 * When you ONLY evaluate getters from a shared interface
 */
public static <I, E extends I> boolean reflectiveEquals(E obj1, E obj2, Class<I> clazz) {
  return Stream.of(clazz.getDeclaredMethods())
               .filter(m -> m.getName().startsWith("get"))
               .allMatch(m -> {
                                 try{
                                   return Objects.equals(m.invoke(obj1), m.invoke(obj2));
                                 } catch (Exception e) {
                                    e.printStackTrace(System.err);
                                    return false;
                                 }
                               }
  );
}

/**
 * When you will evaluate ALL getters from the classes
 */
public static <E> boolean reflectiveEquals(E obj1, E obj2) {
  return Stream.of(obj1.getClass().getDeclaredMethods())
               .filter(m -> m.getName().startsWith("get"))
               .allMatch(m -> {
                                try{
                                  return Objects.equals(m.invoke(obj1), m.invoke(obj2));
                                } catch (Exception e) {
                                  e.printStackTrace(System.err);
                                  return false;
                                }
                              }
  );
}

}

Do note that this is an incomplete implementation. Think, for example, that you have a get method with parameters (For whatever reason). The evaluation of Method#invoke will fail due the lack of them, although in both cases it will simply return false and log the error.

I personally don't think of this as a very elegant solution either, but is something you can hide and further tune. You may also use a specific FancyEquals for many special case equals methods (For instance, for the Foo entities, you could use FooEquals#reflective so it's more specific but equally readable. This way you could also generate an Interface for possible XEquals implementations and properly use generics).

  •  Tags:  
  • java
  • Related