0

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.

1

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);
}
|improve this answer|||||
1

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.

|improve this answer|||||
  • Thanks. The loop should also exit if all params have been tried. Then, the restClient is not only way to exit the loop. By the way what does the paramSupplier code look like. Updated OP to include what is meant by generic. – James Jan 23 at 15:56
1

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;
}
|improve this answer|||||

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.