Stream API 用法的方便复杂性?
Convenient complexity for Stream API usage?
我有 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
,并将 offer
部分的遗漏 properties
设置为 products
。
在 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
首先,您可以为那些链式过滤器创建一个公共 Predicate
s 作为
.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 can be the same for a bunch of products
您可以更新代码以使用合并功能:
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
}));
对于每个会话,所有报价产品的属性将参考最后一个合格的文档产品的属性,对吗?
因为内部流总是独立于当前文档产品评估相同的结果。
因此,在纠正此问题时,我将建议进行以下重构:
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());
}
})
;
}
}
我有 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
,并将 offer
部分的遗漏 properties
设置为 products
。
在 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
首先,您可以为那些链式过滤器创建一个公共 Predicate
s 作为
.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 can be the same for a bunch of products
您可以更新代码以使用合并功能:
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
}));
对于每个会话,所有报价产品的属性将参考最后一个合格的文档产品的属性,对吗?
因为内部流总是独立于当前文档产品评估相同的结果。
因此,在纠正此问题时,我将建议进行以下重构:
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());
}
})
;
}
}