Java 8函数式编程模式:不要使用巨长的Stream流

18-11-24 banq
              

假设你已经使用了lambdas流,巨长的Stream的代码如下:

public List<Product> getFrequentOrderedProducts(List<Order> orders) {
        return orders.stream()
                        .filter(o -> o.getCreationDate().isAfter(LocalDate.now().minusYears(1)))
                        .flatMap(o -> o.getOrderLines().stream())
                        .collect(groupingBy(OrderLine::getProduct, summingInt(OrderLine::getItemCount)))
                        .entrySet()
                        .stream()
                        .filter(e -> e.getValue() >= 10)
                        .map(Entry::getKey)
                        .filter(p -> !p.isDeleted())
                        .filter(p -> !productRepo.getHiddenProductIds().contains(p.getId()))
                        .collect(toList());

以上实现功能是:计算上一年订购产品的次数。现在,只接受频繁订购的产品(> = 10)并返回它们,前提是如果它们没有被逻辑删除或显式隐藏在数据库中。

你写完这段代码很快乐地回家了...

但我们会找到你的!管理层无法解雇你,谁可以读懂这堆代码?!谁愿意和你合作?

这段代码最糟糕的是每行返回不同的类型。除非您在IDE将鼠标悬停其中,否则你将看不到这些类型。

清洁代码最重要的规则之一是:小方法。所以,让我们通过查看我们.collect(..)后面看到的代码,将这个长链分成两个方法.stream()。既然你Collect了一个集合中的项目,为什么我们不通过提取一个好的方法名来解释那个集合是什么?

public List<Product> getFrequentOrderedProducts(List<Order> orders) {
          return getProductCountsOverTheLastYear(orders).entrySet().stream()
                             .filter(e -> e.getValue() >= 10)
                             .map(Entry::getKey)
                             .filter(Product::isNotDeleted)
                             .filter(p -> !productRepo.getHiddenProductIds().contains(p.getId()))
                             .collect(toList());
}
private Map<Product, Integer> getProductCountsOverTheLastYear(List<Order> orders) {
          return orders.stream()
                             .filter(o -> o.getCreationDate().isAfter(LocalDate.now().minusYears(1)))
                             .flatMap(o -> o.getOrderLines().stream())
                             .collect(groupingBy(OrderLine::getProduct, summingInt(OrderLine::getItemCount)));
}

但是,只有这样我们才注意到在第6行,我们可能会在循环中查询外部系统!我的天啊!这是你永远不应该做的事情。

让我们开始流之前先获得hiddenProductIds 列表,我们甚至可以进一步检查产品是否隐藏在Predicate局部变量中:

public List<Product> getFrequentOrderedProducts(List<Order> orders) {
        List<Long> hiddenProductIds = productRepo.getHiddenProductIds();
        Predicate<Product> productIsNotHidden = p -> !hiddenProductIds.contains(p.getId());
        return getProductCountsOverTheLastYear(orders).entrySet().stream()
                        .filter(e -> e.getValue() >= 10)
                        .map(Entry::getKey)
                        .filter(Product::isNotDeleted)
                        .filter(productIsNotHidden)
                        .collect(toList());

还有一件事我们可以做:我们可以命名被频繁订购的产品的流,并使其成为Stream类型的变量。众所周知,这些Stream项目实际上并未在此时进行计算评估,而是仅在结束时.collect()进行计算评估。但是,Stream<>有时不鼓励使用变量,因为粗心的开发人员可能会尝试重新使用它(重新遍历它),因此在执行此操作之前,请确保您的团队完全了解这种常见情况。

public List<Product> getFrequentOrderedProducts(List<Order> orders) {
        List<Long> hiddenProductIds = productRepo.getHiddenProductIds();
        Predicate<Product> productIsNotHidden = p -> !hiddenProductIds.contains(p.getId());
        Stream<Product> frequentProducts = getProductCountsOverTheLastYear(orders).entrySet().stream()
                        .filter(e -> e.getValue() >= 10)
                        .map(Entry::getKey);
        return frequentProducts
                        .filter(Product::isNotDeleted)
                        .filter(productIsNotHidden)
                        .collect(toList());
}
<p>[...]

这里的想法是通过引入解释变量来避免过多的方法链。这意味着 提取方法甚至使用函数或Stream类型的变量,以使代码尽可能清晰地显示给读者。