Question

I have the following common pattern that I need to use in an application:

  1. Given a list of keys, call a function with a parameter to find values for the keys. The function may return null for a given key where no value can be found.
  2. For those keys missing a value, go to step 1 supplying only the keys missing values and using a different value for the function parameter. If no keys are missing a value or no parameter values are left to try, go to the next step.
  3. Return a map that contains all keys with their value (or null if not found) as found in the above steps.

Here's a specific instance of this behavior:

public Map<String, BigDecimal> getValues(List<String> keys) {

    Map<String, BigDecimal> map = restClient.get(keys, "paramFirstTry");

    List<String> missingKeys = getKeysMissingValues(map);

    if (!missingKeys.isEmpty()) {
        Map<String, BigDecimal> mapSecondTry = restClient.get(missingKeys "paramSecondTry");
        map.putAll(mapSecondTry);

        missingKeys = getKeysMissingValues(mapSecondTry);

        if (!missingKeys.isEmpty()) {

            Map<String, BigDecimal> mapThirdTry = restClient.get(missingKeys, "paramThirdTry");     
            map.putAll(mapThirdTry);
        }
    }

    return map;
}

private List<String> getMissingValues(Map<String, BigDecimal> map) {        
    return map
        .entrySet()
        .stream()
        .filter(entry -> entry.getValue() == null)
        .map(Entry::getKey)
        .collect(Collectors.toList());
}

I'll have many instances of this behavior albeit with a different number of attempts and String parameter values. How would I make this code generic to handle the behavior described above? I'm using Java 8.

Update

By generic, I mean the restClient may be of different types and may have a different method name. The method should always take a List for its first parameter and the second parameter will always take a String. I should have seen the loop. I was looking at this similar post that is a more simple case of just trying to get the first non-null String.

Was it helpful?

Solution

Personally, I would pass in the API call (restClient.get()) as a list of functions (Function<String, Map<String, BigDecimal>) and then simply loop over the missing keys and supply functions until either all keys are found or no more lookup functions are available:

public Map<String, BigDecimal> getValues(List<String> keys, Iterator<Function<String, Map<String, BigDecimal>> valueFactoryIterator) {
    var values = new HashMap<String, BigDecimal>(); // or whatever type of map
    var missingKeys = List.copyOf(keys); // defensive copy 
    // var missingKeys = keys; // alternative without defensive copy

    while (!missingKeys.isEmpty() && valueFactoryIterator.hasNext()) {
        var additionalValues = valueFactoryIterator.next().apply(missingKeys);
        values.putAll(additionalValues);
        missingKeys = findMissingKeys(values, keys);
    }
    if (!missingKeys.isEmpty() && !valueFactoryIterator.hasNext()) {
        logger.log("Could not resolve values for keys: {0}", missingKeys);
    }
    return values;
}

public Map<String, BigDecimal> getValues(List<String> keys, Iterable<Function<String, Map<String, BigDecimal>> valueFactories) {
     return getValues(keys, valueFactories.iterator());
}

You can then call the function in this way:

List<Function<String, Map<String, BigDecimal>> supplyChain = List.of(
    keys -> restClient.get(keys, "param1stTry),
    keys -> restClient.get(keys, "param2ndTry),
    keys -> restClient.get(keys, "param3rdTry)
);
var values = getValues(keys, supplyChain);

You can have the supply chain stored as function you call, or as some field, or whatever. Doesn't need to be a list, can be any Iterable or Iterator.

You could also consider passing a Supplier<Map<String, BigDecimal>> parameter to the function and constructing the map from that, then you could decide the type of map from the outside, e.g. by passing HashMap::new.

The method would then look this way:

public Map<String, BigDecimal> getValues(List<String> keys, 
    Iterator<Function<String, Map<String, BigDecimal>> valueFactoryIterator, 
    Supplier<Map<String, BigDecimal>> mapFactory) {

    var values = mapFactory.get();
    var missingKeys = List.copyOf(keys); // defensive copy 
    // var missingKeys = keys; // alternative without defensive copy

    while (!missingKeys.isEmpty() && valueFactoryIterator.hasNext()) {
        var additionalValues = valueFactoryIterator.next().apply(missingKeys);
        values.putAll(additionalValues);
        missingKeys = findMissingKeys(values, keys);
    }
    if (!missingKeys.isEmpty() && !valueFactoryIterator.hasNext()) {
        logger.log("Could not resolve values for keys: {0}", missingKeys);
    }
    return values;
}

public Map<String, BigDecimal> resolveValues() {
     List<Function<String, Map<String, BigDecimal>> supplyChain = List.of(
        keys -> restClient.get(keys, "param1stTry),
        keys -> restClient.get(keys, "param2ndTry),
        keys -> restClient.get(keys, "param3rdTry)
    );
    var values = getValues(keys, supplyChain, HashMap::new);
}

OTHER TIPS

Not a 100% sure what you mean by 'generic' but you can get rid of this codes repetitive nature by rolling it up in a loop.

public Map<String, BigDecimal> getValues(List<String> keys) {

    Map<String, BigDecimal> map = new Map<>(); 

    while (!keys.isEmpty()) {
        Map<String, BigDecimal> mapTry = restClient.get(keys, paramSupplier());
        map.putAll(mapTry);
        keys = getKeysMissingValues(mapTry);
    }

    return map;
}

Of course this is putting a lot of trust in your restClient to ensure this doesn't go off into an infinite loop so you way want more than one way to exit this loop.

By using a good old loop.

Pseudocode:

public void outerCaller() {
   List<String> params = Arrays.asList("try1", "try2", "try3");
   conductTries(params, Arrays.asList("val1", "val2", "val3"));
}

private static Map<String, BigDecimal> conductTries(List<String> params, List<String> allKeys) {
   Map<String, BigDecimal> result = new HashMap<String, BigDecimal>();
   for (String param : params) {
      List<String> missingKeys = findMissingKeys(result, allKeys);
      if (!missingKeys.isEmpty()) {
         Map<String, BigDecimal> thisCallsResult = doRestCall(param, missingKeys);
         result.putAll(thisCallsResult);
      }
   }
   return result;
}

PS/edit: if this is about different rest-calls, just add a BiFunction<String, List<String>, Map<String, BigDecimal>> as an additional parameter, which replaces the doRestCall(). Slightly modified code to demonstrate this:

public void outerCaller() {
   List<String> params = Arrays.asList("try1", "try2", "try3");
   List<String> keys = Arrays.asList("val1", "val2", "val3");
   BiFunction<String, List<String>, Map<String, BigDecimal>> call = (param, keys) -> restClient.get(keys, param);

   conductTries(params, keys, call);
}

private static Map<String, BigDecimal> conductTries(List<String> params, List<String> allKeys, BiFunction<String, List<String>, Map<String, BigDecimal>> call) {
   Map<String, BigDecimal> result = new HashMap<String, BigDecimal>();
   for (String param : params) {
      List<String> missingKeys = findMissingKeys(result, allKeys);
      if (!missingKeys.isEmpty()) {
         Map<String, BigDecimal> thisCallsResult = call.apply(param, missingKeys);
         result.putAll(thisCallsResult);
      }
   }
   return result;
}
Licensed under: CC-BY-SA with attribution
scroll top