I have the following domain model:
class Holding ------------ Account account; DataSourceProduct dataSourceProduct; class Account ------------- String name; class DataSourceProduct ------------- DataSource dataSource; Product product; // other product data specific to the data source class Product -------------- String id; class DataSource ---------------- String name;
I get a list of Holding
data from three different data sources. I need to show the data in a grid. Each row in the grid has the same product and account and will have columns to show the product data as provided by the three different data sources. Note: One of the data sources may have a holding with a product id of null
.
To accomplish this, I currently break up the list of holdings into several lists where each list contains only holdings with the same product id and account. Also, I key each list by data source. So, in the end, I have the following:
List<Map<DataSource,Holding>>
So, we have something like this:
index 0 in the list
Map:Key Map:Value
datasource1 holding1AccordingToDs1 ----|
datasource2 holding1AccordingToDs2 ----|---> all three holdings have same product and account
datasource3 holding2AccordingToDs3 ----|
index 1 in the list
Map:Key Map:Value
datasource1 holding2AccordingToDs1 ----|
datasource2 holding2AccordingToDs2 ----|---> all three holdings have same product and account
index 2 in the list
Map:Key Map:Value
datasource1 holding3AccordingToDs1 ----|
datasource3 holding3AccordingToDs3 ----|---> all three holdings have
To get from List<Holding>
to List<Map<DataSource,Holding>>
I do the following:
// these holdings could have an null instrument id
// and this data is returned from a different db than other
// holdings
List<Holding> aSourceHoldings = holdingService.getHoldings(date);
List<Holding> unMatchedHoldings = aSourceHoldings .stream()
.filter(h -> h.getDataSourceProduct().getProduct().getId() == null)
.collect(Collectors.toList());
// if null product id, no way to match to other instruments
aSourceHoldings.removeAll(unMatchedHoldings);
// get other data source holdings
List<Holding> toBeMatchedHoldings = holdingRepository.getHoldings(date);
toBeMatchedHoldings.addAll(aSourceHoldings);
// grouping by account and product
Map<Account, Map<Product, List<Holding>>> matchedHoldings =
toBeMatchedHoldings.stream()
.collect(Collectors.groupingBy(Holding::getAccount,
Collectors.groupingBy(h -> h.getDataSourceProduct().getProduct())));
List<Map<DataSource, Holding>> unifiedHoldings = new ArrayList<Map<DataSource, Holding>>();
Map<DataSource, Holding> tmp = new HashMap<DataSource, Holding>();
for (Map<Product, List<Holding>> productHoldings : matchedHoldings.values()) {
for (List<Holding> holdings : productHoldings.values()) {
for (Holding holding : holdings) {
tempMap.put(holding.getDataSourceProduct.getDataSource(), holding);
}
unifiedHoldings.add(tempMap);
}
}
return unifiedHoldings;
Like data sources, the number of accounts is small (4-5) and will remain fairly static over the years. Is it possible to reduce the number of manipulations to group and get the data that I need for the UI grid?
1 Answer 1
Type inference for generic instance creation
You can rely on type inference for generic instance creation when declaring unifiedHoldings
:
List<Map<DataSource, List<Holding>>> unifiedHoldings = new ArrayList<>();
Mutable operations on collections
List<Holding> aSourceHoldings = holdingService.getHoldings(date);
List<Holding> unMatchedHoldings = /* ... */ ;
aSourceHoldings.removeAll(unMatchedHoldings);
List<Holding> toBeMatchedHoldings = holdingRepository.getHoldings(date);
toBeMatchedHoldings.addAll(aSourceHoldings);
Calling mutable operations such as removeAll(Collection)
and addAll(Collection)
may throw UnsupportedOperationException
if the underlying List
does not return a mutable implementation. As such, in these scenarios, you should be creating your own List
given the resulting elements, and then do the necessary removal/addition calls on the new instance.
Account
and Product
usage
Interestingly, it looks like Account
and Product
themselves aren't used in the processing, after kind of using them as 'keys' for grouping. The nested Map
s looks too peculiar, which brings us to the next point...
Grouping your Holding
objects
I currently break up the list of holdings into several lists where each list contains only holdings with the same product id and account. Also, I key each list by data source.
(emphasis mine)
This looks like a good use case for a supplementary 'key' class that will allow us to grouping Holding
objects with the same account, product and data source. For example:
private static final class HoldingKey {
private final Account account;
private final Product product;
HoldingKey(Account account, Product product) {
this.account = Objects.requireNonNull(account);
this.product = Objects.requireNonNull(product);
}
Account getAccount() {
return account;
}
Product getProduct() {
return product;
}
@Override
public boolean equals(Object o) {
return o instanceof HoldingKey
&& getAccount().equals(((HoldingKey) o).getAccount())
&& getProduct().equals(((HoldingKey) o).getProduct());
}
@Override
public int hashCode() {
return Objects.hash(getAccount(), getProduct());
}
static HoldingKey toKey(Holding holding) {
return new HoldingKey(holding.getAccount(),
holding.getDataSourceProduct().getProduct());
}
}
Using the method reference HoldingKey::toKey
to group Holding
objects by, with a dash of Stream.concat(Stream, Stream)
to combine two streams together, we can combine the processing on both the repository and service results with relative ease:
Stream<Holding> fromRepository = holdingRepository.getHoldings(date).stream();
Stream<Holding> fromService = holdingService.getHoldings(date).stream()
.filter(h -> h.getDataSourceProduct().getProduct().getId() != null);
Map<HoldingKey, List<Holding>> map = Stream.concat(fromRepository, fromService)
.collect(Collectors.groupingBy(HoldingKey::toKey));
This simplifies the nested Map
s we have by flattening that multi-key structure into a single HoldingKey
object. Achieving what you require is a simple continuation on map
's entrySet()
, with a mapping function to turn List<Holding>
into your required Map<DataSource, Holding>
:
private static final Function<List<Holding>, Map<DataSource, Holding>> MAPPER =
x -> x.stream().collect(Collectors.toMap(
y -> y.getDataSourceProduct().getDataSource(), Function.identity()));
private static List<Map<DataSource, List<Holding>>> getUnifiedHoldings(Date date) {
Stream<Holding> fromRepository = holdingRepository.getHoldings(date).stream();
Stream<Holding> fromService = holdingService.getHoldings(date).stream()
.filter(h -> h.getDataSourceProduct().getProduct().getId() != null);
return Stream.concat(fromRepository, fromService)
.collect(Collectors.groupingBy(HoldingKey::toKey))
.entrySet()
.stream()
.map(MAPPER.compose(Map.Entry::getValue))
.collect(Collectors.toList());
}
Here, we compose()
our MAPPER
function with the 'extractor' function Map.Entry::getValue
to derive our resulting list elements.
-
\$\begingroup\$ Awesome! +1 Thanks for spending the time to provide a lot detail and cover multiple points. I might be looking at this wrong, but I think the
HoldingKey
needs to be formed by usingAccount
andProduct
(rather thanDataSourceProduct
). Then I suppose we don't have as easy access toDataSource
in thegetDataSource()
method ofHoldingKey
. \$\endgroup\$James– James2016年11月16日 17:47:44 +00:00Commented Nov 16, 2016 at 17:47 -
\$\begingroup\$ After running a few tests, I realize that I have something wrong in the OP. Very sorry about this... but I actually need
getUnifiedHoldings to return a
List<Map<DataSource, Holding>>. I have revised the OP. I think everything you answered still applies except for the last two operations in the return statement for
getUnifiedHoldings` and its return type. Sorry again and thanks for all of your help. \$\endgroup\$James– James2016年11月16日 19:27:12 +00:00Commented Nov 16, 2016 at 19:27 -
\$\begingroup\$ @James updated my answer. \$\endgroup\$h.j.k.– h.j.k.2016年11月17日 17:21:25 +00:00Commented Nov 17, 2016 at 17:21