Populate values in a map from a series of function calls
https://softwareengineering.stackexchange.com/questions/404142
Question
I have the following common pattern that I need to use in an application:
- 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.
- 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.
- 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
.
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;
}