Stream API使用的便利复杂性?

3

我有一个JSON文档,它是对一堆文件进行解析的结果:

{
  "offer": {
    "clientName": "Tom",
    "insuranceCompany": "INSURANCE",
    "address": "GAMLE BONDALSVEGEN 53",
    "renewalDate": "22.12.2018",
    "startDate": "22.12.2017",
    "too_old": false,
    "products": [
      {
        "productType": "TRAVEL",
        "objectName": "Reiseforsikring - Holen, Tom Andre",
        "name": null,
        "value": null,
        "isExclude": false,
        "monthPrice": null,
        "yearPrice": 1637,
        "properties": {}
      }
    ]
  },
  "documents": [
    {
      "clientName": "Tom",
      "insuranceCompany": "INSURANCE",
      "fileName": "insurance_tom.pdf",
      "address": "GAMLE BONDALSVEGEN 53",
      "renewalDate": "22.12.2019",
      "startDate": "22.12.2018",
      "issuedDate": "20.11.2018",
      "policyNumber": "6497777",
      "products": [
        {
          "productType": "TRAVEL",
          "objectName": "Reiseforsikring - Holen, Tom Andre",
          "name": null,
          "value": null,
          "isExclude": false,
          "monthPrice": null,
          "yearPrice": 1921,
          "properties": {
            "TRAVEL_PRODUCT_NAME": "Reise Ekstra",
            "TRAVEL_DURATION_TYPE": "DAYS",
            "TRAVEL_TYPE": "FAMILY",
            "TRAVEL_DURATION": "70",
            "TRAVEL_INSURED_CLIENT_NAME": "Holen, Tom Andre, Familie"
          }
        },

我想要遍历所有来自“documents”部分的“products”,并将缺失的“properties”设置为来自“offer”部分的“products”。
“Offer”和“documents”在JSON中处于相同的深度级别。
使用Stream API实现如下:
private void mergePropertiesToOffer(InsuranceDocumentsSession insuranceSession) {
    Validate.notNull(insuranceSession, "insurance session can't be null");
    if (insuranceSession.getOffer() == null) return;

    log.info("BEFORE_MERGE");
    // merge all properties by `objectName`
    Stream.of(insuranceSession).forEach(session -> session.getDocuments().stream()
            .filter(Objects::nonNull)
            .flatMap(doc -> doc.getProducts().stream())
            .filter(Objects::nonNull)
            .filter(docProduct -> StringUtils.isNotEmpty(docProduct.getObjectName()))
            .filter(docProduct -> MapUtils.isNotEmpty(docProduct.getProperties()))
            .forEach(docProduct -> Stream.of(session.getOffer())
                    .flatMap(offer -> offer.getProducts().stream())
                    .filter(Objects::nonNull)
                    .filter(offerProduct -> MapUtils.isEmpty(offerProduct.getProperties()))
                    .filter(offerProduct -> StringUtils.isNotEmpty(offerProduct.getObjectName()))
                    .filter(offerProduct -> offerProduct.getObjectName().equals(docProduct.getObjectName()))
                    .forEach(offerProduct -> {
                        try {
                            ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
                            log.info("BEFORE_PRODUCT: {}", mapper.writeValueAsString(offerProduct));
                            offerProduct.setProperties(docProduct.getProperties());
                            log.info("UPDATED_PRODUCT: {}", mapper.writeValueAsString(offerProduct));
                        } catch (JsonProcessingException e) {
                            log.error("Error converting product to offer: {}", e.getCause());
                        }
                    })));
    log.info("AFTER_MERGE");
}

它工作得很好。然而,未来实施比维护要快得多。

我两次使用Stream.of()工厂方法获取不同级别的2个实体的流。此外,尽可能使用flatMap(),以及所有的空值检查。

问题不是这个实现太难了吗?

应该将其重构并分成较小的部分吗?如果是,如何使用良好的编程原则?

解决方案:

非常感谢 nullpointer 的答案。
最终解决方案如下:

Map<Integer, InsuranceProductDto> offerProductMap = session.getOffer().getProducts()
    .stream()
    .filter(this::validateOfferProduct)
    .collect(Collectors.toMap(InsuranceProductDto::getYearPrice, Function.identity(), (first, second) -> first));

Map<Integer, InsuranceProductDto> documentsProductMap = session.getDocuments()
    .stream()
    .flatMap(d -> d.getProducts().stream())
    .filter(this::validateDocumentProduct)
    .collect(Collectors.toMap(InsuranceProductDto::getYearPrice, Function.identity(), (first, second) -> first));

documentsProductMap.forEach((docPrice, docProduct) -> {
    if (offerProductMap.containsKey(docPrice)) {
        offerProductMap.compute(docPrice, (s, offerProduct) -> {
            setProductProperties(offerProduct, docProduct);
            return offerProduct;
        });
    }
}); 
// after finishing execution `offerProductMap` contains updated products

2
实现似乎太困难了,但它可以重构。 - Sagar P. Ghagare
@SagarP.Ghagare 有什么建议如何实现吗? - catch23
2个回答

3

首先,您可以创建一个通用的Predicate来作为这些链接过滤器的基础。

.filter(offerProduct -> MapUtils.isEmpty(offerProduct.getProperties()))
.filter(offerProduct -> StringUtils.isNotEmpty(offerProduct.getObjectName()))
.filter(offerProduct -> offerProduct.getObjectName().equals(docProduct.getObjectName()))

您可以编写一个Predicate,例如

Predicate<OfferProduct> offerProductSelection = offerProduct -> MapUtils.isEmpty(offerProduct.getProperties())
                                    && StringUtils.isNotEmpty(offerProduct.getObjectName())
                                    && offerProduct.getObjectName().equals(docProduct.getObjectName());

然后只需将其用作单个过滤器即可。
.filter(offerProductSelection);

顺便提一下,最好将其移动到返回布尔值的方法中,然后在过滤器中使用该方法。
为了表示,数据类型和实用类可能不是精确的,但你可以做类似这样的事情:
private void mergePropertiesToOffer(InsuranceDocumentsSession insuranceSession) {
    Validate.notNull(insuranceSession, "insurance session can't be null");
    if (insuranceSession.getOffer() == null) return;
    Map<String, InsuranceProductDto> offerProductMap = insuranceSession.getOffer().getProducts()
            .stream()
            .filter(this::validateOfferProduct)
            .collect(Collectors.toMap(InsuranceProductDto::getObjectName, Function.identity())); // assuming 'objectName' to be unique

    Map<String, InsuranceProductDto> documentsProductMap = insuranceSession.getDocuments()
            .stream()
            .filter(Objects::nonNull)
            .flatMap(d -> d.getProducts().stream())
            .filter(this::validateDocumentProduct)
            .collect(Collectors.toMap(InsuranceProductDto::getObjectName, Function.identity())); // assuming 'objectName' to be unique

    Map<String, Product> productsToProcess = new HashMap<>(documentsProductMap);
    productsToProcess.forEach((k, v) -> {
        if (offerProductMap.containsKey(k)) {
            offerProductMap.compute(k, (s, product) -> {
                Objects.requireNonNull(product).setProperties(v.getProperties());
                return product;
            });
        }
    });

    // now the values of 'offerProductMap' is what you can set as an updated product list under offer
}


private boolean validateDocumentProduct(InsuranceProductDto product) {
    return Objects.nonNull(product)
            && MapUtils.isNotEmpty(product.getProperties())
            && StringUtils.isNotEmpty(product.getObjectName());
}

private boolean validateOfferProduct(InsuranceProductDto offerProduct) {
    return Objects.nonNull(offerProduct)
            && MapUtils.isEmpty(offerProduct.getProperties())
            && StringUtils.isNotEmpty(offerProduct.getObjectName());
}

编辑: 根据评论所述,

objectName 可以为一组产品相同

您可以更新代码使用合并函数如下:

Map<String, InsuranceProductDto> offerProductMap = insuranceSession.getOffer().getProducts()
        .stream()
        .filter(this::validateOfferProduct)
        .collect(Collectors.toMap(InsuranceProductDto::getObjectName, Function.identity(), 
                     (a,b) -> {// logic to merge and return value for same keys
                            }));

@nazar_art 我的错,我指的是将相关逻辑抽象成一个特定于任务的单个函数。例如,filter的两个链。然后应该使用封装这些术语。 - Naman
1
好的解决方案。只是我想补充一点,objectName可以是一组产品中相同的。因此,它会失败并显示java.lang.IllegalStateException: Duplicate key。是否有可能找到一些解决方法? - catch23
@nazar_art,你可以在Collectors.toMap重载中使用合并函数。请参见编辑。 - Naman
验证器有一些不同。对于“offer”产品,它应该检查MapUtils.isEmpty(offerProduct.getProperties())。对于文档产品,应该相反。 - catch23
@nazar_art明白了,已经回滚更新了。另一方面,我希望你现在知道我最初的建议是什么了。 - Naman

1
针对每个会话,所有优惠产品属性将参照最后一个符合条件的文档产品的属性,是吗?
因为内部流将始终独立于当前文档产品而评估为相同结果。
因此,在修正此问题时,我建议进行以下重构:
final class ValueWriter
{
    private final static ObjectMapper mapper = new ObjectMapper();

    static
    {
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
    }

    static String writeValue(final Object value) throws JsonProcessingException
    {
        return mapper.writeValueAsString(value);
    }
}

private Optional<Product> firstQualifiedDocumentProduct(final InsuranceDocumentsSession insuranceSession)
{
    return insuranceSession.getDocuments().stream()
        .filter(Objects::notNull)
        .map(Document::getProducts)
        .flatMap(Collection::stream)
        .filter(docProduct -> StringUtils.isNotEmpty(docProduct.getObjectName()))
        .filter(docProduct -> MapUtils.isNotEmpty(docProduct.getProperties()))
        .findFirst()
    ;
}

private void mergePropertiesToOffer(final InsuranceDocumentsSession insuranceSession)
{
    Validate.notNull(insuranceSession, "insurance session can't be null");

    if(insuranceSession.getOffer() == null) return;

    log.info("BEFORE_MERGE");

    final Optional<Product> qualifiedDocumentProduct = firstQualifiedDocumentProduct(insuranceSession);

    if (qualifiedDocumentProduct.isPresent())
    {
        insuranceSession.getOffer().getProducts().stream()
            .filter(Objects::nonNull)
            .filter(offerProduct -> MapUtils.isEmpty(offerProduct.getProperties()))
            .filter(offerProduct -> StringUtils.isNotEmpty(offerProduct.getObjectName()))
            .filter(offerProduct -> offerProduct.getObjectName().equals(qualifiedDocumentProduct.get().getObjectName()))
            .forEach(offerProduct ->
            {
                try
                {
                    log.info("BEFORE_PRODUCT: {}", ValueWriter.writeValueAsString(offerProduct));
                    offerProduct.setProperties(qualifiedDocumentProduct.get().getProperties());
                    log.info("BEFORE_PRODUCT: {}", ValueWriter.writeValueAsString(offerProduct));
                }
                catch (final JsonProcessingException e)
                {
                    log.error("Error converting product to offer: {}", e.getCause());
                }
            })
        ;
    }
}

你的解决方案忽略了一些重要的地方。我不仅需要从文档的产品中获取第一个产品,我需要所有产品并将它们与报价产品中的“objectName”进行比较。如果报价产品的属性为空或为null,则从具有相同对象名称的文档中设置它。 - catch23
@nazar_art,文档和报价产品的对象名称是唯一的吗? - HPH
它们对于“产品”是独一无二的。然而,它们可以来自不同文档的重复。 - catch23

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接