Java functional programming to leverage polymorphism

Photo by Tim Johnson on Unsplash

Java functional programming to leverage polymorphism

How to replace a switch and reduce complexity?

Introducing the factory

Let's consider a factory method that creates a profile based on some user input as follows:

public static Profile create(String profile) {
  switch (profile) {
  case ADMIN:
    return new Admin();
  case HR:
    return new Hr();
  default:
    return new Anonymous();
  }
}

In the default statement, we could throw an exception, however, in this example I decided to return a value.

Although the create method is very simple, each time we introduce a new profile we should add a new statement and a new complexity to the class:

public static Profile create(String profile) {
  switch (profile) {
  case ADMIN:
    return new Admin();
  case HR:
    return new Hr();
  case HRD:
    return new Hrd();
  case VALIDATOR:
    return new Validator();
  case READER:
    return new Reader();
  default:
    return new Anonymous();
  }
}

Lambda and HashMap to the rescue

Instead of using a switch case to find out which class we need to instantiate, we can take full advantage of HashMap and Functional Interfaces; in our previous case, the Supplier Interface:

Since Java 9, we can use Map.of

static Map<String, Supplier<Profile>> profiles = new HashMap<>();

static {
  profiles.put(ADMIN, Admin::new);
  profiles.put(HR, Hr::new);
  profiles.put(HRD, Hrd::new);
  profiles.put(VALIDATOR, Validator::new);
  profiles.put(READER, Reader::new);
}

Therefore, our factory method is reduced to verifying the key or creating the anonymous profile:

public static Profile create(String profile) {
  if (profiles.containsKey(profile)) {
    return profiles.get(profile).get();
  }
  return new Anonymous();
}

The complexity now is steady stable and won't be affected by adding or removing profiles.

Moreover, we can use Function Interface or apply a specific Functional Interface whenever a constructor is required with one or more arguments.

Using Function when one argument is required

static Map<String, Function<Integer, Profile>> profiles = new HashMap<>();

// Filling the hashMap does not change
static {
  profiles.put(ADMIN, Admin::new);
  profiles.put(HR, Hr::new);
  profiles.put(HRD, Hrd::new);
  profiles.put(VALIDATOR, Validator::new);
  profiles.put(READER, Reader::new);
}

public static Profile create(String profile, int arg) {
  if (profiles.containsKey(profile)) {
    return profiles.get(profile).apply(arg);
  }
  return new Anonymous(arg);
}

Using Specific Functional Interface with one or more arguments

First, we create the Functional Interface with the required arguments:

@FunctionalInterface
public interface ProfileFunction {

   Profile apply(int arg1, String arg2);
}

Then, we change the HashMap to accept the ProfileFunction as follows:

static Map<String, ProfileFunction> profiles = new HashMap<>();

// Filling the hashMap does not change
static {
  profiles.put(ADMIN, Admin::new);
  profiles.put(HR, Hr::new);
  profiles.put(HRD, Hrd::new);
  profiles.put(VALIDATOR, Validator::new);
  profiles.put(READER, Reader::new);
}

public static Profile create(String profile, int arg1, String arg2) {
  if (profiles.containsKey(profile)) {
    return profiles.get(profile).apply(arg1, arg2);
  }
  return new Anonymous(arg1, arg2);
}

A great advantage with Enum

In such a use case, we can replace the HashMap and delegate the profile creation to an Enum, which brings a more elegant and efficient design💖:

public enum ProfileEnum {

  ADMIN(Admin::new),
  HR(Hr::new),
  HRD(Hrd::new),
  VALIDATOR(Validator::new);

  private final ProfileFunction profileFunction;

  ProfileEnum(ProfileFunction profileFunction) {
     this.profileFunction = profileFunction;
  }

  public Profile get(int arg1, String arg2) {
     return profileFunction.apply(arg1, arg2);
  }

}

And the factory method would become:

public class ProfileFactory {

  public static Profile create(String profile, int arg1, String arg2) {
     try {
        ProfileEnum constant = ProfileEnum.valueOf(profile.toUpperCase());
        return constant.get(arg1, arg2);
     } catch (IllegalArgumentException iae) {
        return new Anonymous(arg1, arg2);
     }
  }

}

valueOf would throw an IllegalArgumentException if the enum type cannot find a constant with the specified profile name.

Source Code

The complete source code is available at: github.com/elie29/factory-pattern, where each step has its own specific branch.

Thanks for reading! If you enjoyed this post, follow me on Twitter for more content. If you have any feedback or suggestions, leave it in the comments below and I’ll do my best to get back to you 👌