Stream流收集器的購物車DDD聚合真實示例 - foojay

banq發表於2021-05-11

Java Stream的Collectors方法適合大多數用例。它們允許返回aCollection或標量。對於前者,使用一種toXXX()方法,對於後者,使用一種方法reducing()。

讓我們想象一個實現購物車的電子商務平臺。該購物車的建模如下:

Stream流收集器的購物車DDD聚合真實示例 -  foojay
這非常類似DDD設計中聚合,Cart作為一個聚合根實體。

public class Product {

    private final Long id;                           // 1
    private final String label;                      // 1
    private final BigDecimal price;                  // 1

    public Product(Long id, String label, BigDecimal price) {
        this.id = id;
        this.label = label;
        this.price = price;
    }

    @Override
    public boolean equals(Object object ) { ... }    // 2

    @Override
    public int hashCode() { ... }                    // 2
}
public class Cart {

    private final Map<Product, Integer> products = new HashMap<>(); // 1

    public void add(Product product) {
        add(product, 1);
    }

    public void add(Product product, int quantity) {
        products.merge(product, quantity, Integer::sum);
    }

    public void remove(Product product) {
        products.remove(product);
    }

    public void setQuantity(Product product, int quantity) {
        products.put(product, quantity);
    }

    public Map<Product, Integer> getProducts() {
        return Collections.unmodifiableMap(products);               // 2
    }
}


定義瞭如何在記憶體中儲存資料後,我們需要設計如何在螢幕上顯示購物車。我們知道,結帳螢幕需要顯示兩個不同的資訊位:
  • 行的列表,其中每一行的價格,即每種產品的價格乘以數量。
  • 購物車的整體價格。

相應程式碼:

public record CartRow(Product product, int quantity) {                // 1

    public CartRow(Map.Entry<Product, Integer> entry) {
        this(entry.getKey(), entry.getValue());
    }

    public BigDecimal getRowPrice() {
        return product.getPrice().multiply(new BigDecimal(quantity));
    }
}
var rows = cart.getProducts()
    .entrySet()
    .stream()
    .map(CartRow::new)
    .collect(Collectors.toList());                                    // 1

var price = cart.getProducts()
    .entrySet()
    .stream()
    .map(CartRow::new)
    .map(CartRow::getRowPrice)                                        // 2
    .reduce(BigDecimal.ZERO, BigDecimal::add);                        // 3




Java流的主要限制之一是隻能使用一次。原因是流物件不一定是不變的(儘管它們可以是不變的)。因此,兩次執行相同的流可能不是冪等的。
因此,要獲取行和價格,我們需要從購物車建立兩個流。從一個流中,我們將獲得行,而從另一流中,將獲得價格。
如果我們想從單個流中收集行和價格。我們需要一個可Collector在一次透過中將兩個物件都作為單個物件返回的自定義。

public class PriceAndRows {

    private BigDecimal price;                              // 1
    private final List<CartRow> rows = new ArrayList<>();  // 2

    PriceAndRows(BigDecimal price, List<CartRow> rows) {
        this.price = price;
        this.rows.addAll(rows);
    }

    PriceAndRows() {
        this(BigDecimal.ZERO, new ArrayList<>());
    }
}

這是Collector介面的摘要。有關更多詳細資訊,請檢查此以前的帖子

Stream流收集器的購物車DDD聚合真實示例 -  foojay

  • supplier()    提供基礎物件以開始
  • accumulator()    描述如何將當前流式專案累積到容器中
  • combiner()    如果流是並行的,請描述如何合併它們
  • finisher()    如果可變容器型別不是返回的型別,請描述如何將前者轉換為後者
  • characteristics()    提供後設資料以最佳化流


鑑於此,我們可以相應地實現Collector:

private static class PriceAndRowsCollector
    implements Collector<Map.Entry<Product, Integer>, PriceAndRows, PriceAndRows> {

    @Override
    public Supplier<PriceAndRows> supplier() {
        return PriceAndRows::new;                                                // 1
    }

    @Override
    public BiConsumer<PriceAndRows, Map.Entry<Product, Integer>> accumulator() {
        return (priceAndRows, entry) -> {                                        // 2
            var row = new CartRow(entry);
            priceAndRows.price = priceAndRows.price.add(row.getRowPrice());
            priceAndRows.rows.add(row);
        };
    }

    @Override
    public BinaryOperator<PriceAndRows> combiner() {
        return (c1, c2) -> {                                                     // 3
            c1.price = c1.price.add(c2.price);
            var rows = new ArrayList<>(c1.rows);
            rows.addAll(c2.rows);
            return new PriceAndRows(c1.price, rows);
        };
    }

    @Override
    public Function<PriceAndRows, PriceAndRows> finisher() {
        return Function.identity();                                              // 4
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Set.of(Characteristics.IDENTITY_FINISH);                          // 4
    }
}


設計Collector涉及一些工作,但是使用自定義收集器很容易:

var priceAndRows = cart.getProducts()
                       .entrySet()
                       .stream()
                       .collect(new PriceAndRowsCollector());
 


您可以使用Collectors該類中提供的即用型收集器來解決大多數用例。但是,有些需要實現自定義Collector,例如,當您需要收集多個單個集合或單個標量時,則需要實現一個custom 。
如果您以前從未開發過它,可能看起來很複雜,但事實並非如此。您只需要一點練習即可。希望這篇文章對您有所幫助。
您可以在GitHub上以Maven格式找到此帖子的原始碼。

相關文章