使用Stream API的便捷复杂性?

时间:2018-12-28 12:55:13

标签: java java-8 java-stream code-complexity

我有一个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"
          }
        },

我想遍历products部分中的所有documents,并从properties部分中将错过的products设置为offer

在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 个答案:

答案 0 :(得分: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);

顺便说一句,您最好将其移动到返回boolean的方法中,然后在过滤器中使用它。


就所使用的数据类型和实用程序类而言,并不精确,但为表示起见,您可以执行以下操作:

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
                            }));

答案 1 :(得分: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());
                }
            })
        ;
    }
}