2
\$\begingroup\$

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?

\$\endgroup\$
1
\$\begingroup\$

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 Maps 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 Maps 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.

\$\endgroup\$
3
  • \$\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 using Account and Product (rather than DataSourceProduct). Then I suppose we don't have as easy access to DataSource in the getDataSource() method of HoldingKey. \$\endgroup\$
    – James
    Nov 16 '16 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
    Nov 16 '16 at 19:27
  • \$\begingroup\$ @James updated my answer. \$\endgroup\$
    – h.j.k.
    Nov 17 '16 at 17:21

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.