Identifiable.java

package com.github.dakusui.pcond.core.identifieable;

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;

import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;

/**
 * An interface that represents an object which can be identified based on objects
 * used on instantiation without overriding the {@link Object#hashCode} and {@link Object#equals(Object)}.
 * In order to minimize effort to implement this interface, you can extend
 * {@link Identifiable.Base} class.
 */
public interface Identifiable {
  static Optional<Object> creatorOf(Object object) {
    Optional<Object> ret = Optional.empty();
    if (object instanceof Identifiable)
      ret = Optional.of(((Identifiable) object).creator());
    return ret;
  }

  static List<Object> argsOf(Object object) {
    List<Object> ret = singletonList(object);
    if (object instanceof Identifiable)
      ret = ((Identifiable) object).args();
    return ret;
  }

  static <T> String formatObjectName(Object object) {
    return "noname:" + (object == null ? "null" : object.toString());
  }

  Object creator();

  /**
   * This method is designed to be called from the {@code hashCode()} method of a class
   * which implements {@link Identifiable} identity interface.
   *
   * @return A proper value to be returned from the {@link Object#hashCode()} method.
   */
  default int defaultHashCode() {
    return identityObject().hashCode();
  }

  /**
   * This method is designed to be called from the {@code defaultEquals()} method of a class
   * which implements {@link Identifiable} identity interface.
   *
   * @return A proper value to be returned from the {@link Object#equals(Object)} method.
   */
  default boolean defaultEquals(Object anotherObject) {
    if (this == anotherObject)
      return true;
    if (!this.getClass().isInstance(anotherObject))
      return false;
    return Objects.equals(this.identityObject(), ((Identifiable) anotherObject).identityObject());
  }

  default Object createIdentity() {
    return Stream.concat(
        Stream.of(creator()),
        args().stream()).collect(toList());
  }

  /**
   * Typically, implementation of this method should return a final field value to which the
   * value returned by {@link Identifiable#createIdentity()} method is assigned in the constructor.
   *
   * @return The identity object.
   */
  Object identityObject();

  List<Object> args();

  class Base implements Identifiable {
    private final Object       creator;
    private final List<Object> args;
    private final Object       identity;

    protected Base(Object creator, List<Object> args) {
      this.creator = Objects.requireNonNull(creator);
      this.args = Objects.requireNonNull(args);
      this.identity = createIdentity();
    }

    @Override
    public Object identityObject() {
      return this.identity;
    }

    @Override
    public Object creator() {
      return this.creator;
    }

    @Override
    public List<Object> args() {
      return args;
    }

    @Override
    public int hashCode() {
      return defaultHashCode();
    }

    // Done in a method to which the operation is delegated.
    @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
    @Override
    public boolean equals(Object obj) {
      return defaultEquals(obj);
    }
  }
}