給 Java 造個輪子 - Chain

邊城發表於2021-12-04

在前不久發的「Java 中模擬 C# 的擴充套件方法」一文中給 Java 模擬了擴充套件方法。但畢竟沒有語法支援,使用起來還是有諸多不便,尤其是需要不斷交錯使用不同類中實現的“擴充套件方法”時,切換物件非常繁瑣。前文也提到,之所以想到研究“擴充套件方法”其實只是為了“鏈式呼叫”。

那麼,為什麼不直接從原始需求出發,只解決鏈式呼叫的問題,而不去考慮更寬泛的擴充套件方法呢?前文研究過通過 Builder 模式來實現鏈式呼叫,這種方式需要自己定義擴充套件方法類(也就是 Builder),仍然比較繁瑣。

Chain 雛形

鏈式呼叫的主要特點就是使用了某個物件之後,可以繼續使用該物件 …… 一直使用下去。你看,這裡就兩件事:一是提供一個物件;二是使用這個物件 —— 這不就是 Supplier 和 Consumer 嗎?Java 在 java.util.function 包中正好提供了同名的兩個函式式介面。這樣一來,我們可以定義一個 Chain 類,從一個 Supplier 開始,不斷的“消費”它,這樣一個簡單的 Chain 雛形就出來了:

public class Chain<T> {
    private final T value;

    public Chain(Supplier<? extends T> supplier) {
        this.value = supplier.get();
    }

    public Chain<T> consume(Consumer<? super T> consumer) {
        consumer.accept(this.value);
        return this;
    }
}

現在,假如我們有一個 Person 類,它有一些行為方法:

class Person {
    public void talk() { }
    public void walk(String target) { }
    public void eat() { }
    public void sleep() { }
}

還是前文中那個業務場景:談妥了,出去,吃飯,回來,睡覺。非鏈試呼叫是這樣的:

public static void main(String[] args) {
    var person = new Person();
    person.talk();
    person.walk("飯店");
    person.eat();
    person.walk("家");
    person.sleep();
}

如果用 Chain 串起來就是:

public static void main(String[] args) {
    new Chain<>(Person::new).consume(Person::talk)
        .consume(p -> p.walk("飯店"))
        .consume(Person::eat)
        .consume(p -> p.walk("家"))
        .consume(Person::sleep);
}

上面已經完成了 Chain 封裝,還是蠻簡單的,只不過有兩個小問題:

  1. consume() 字太多,寫起來麻煩。如果改名的話,do 很合適,可惜是關鍵字 …… 不如改成 act 好了;
  2. 連結呼叫往往是用在表示式中,返回有個返回值,所以得加個 Chain::getValue()

完善 Chain

實際上,鏈式呼叫過程中,也不一定就只是“消費”,有可能還需要“轉換”,用程式設計師的話來說,就是 map()—— 將當前物件作為引數傳入,計算完成之後得到另一個物件。可能大家在 java stream 中用到 map() 比較多,不過這裡的場景更像 Optional::map

來看看 Optional::map原始碼

public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent()) {
        return empty();
    } else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

可以看出來這個 map() 的邏輯很簡單,就是把 Function 執行的的結果再封裝成一個 Optional 物件。我們在 Chain 中也可以這麼幹:

public <U> Chain<U> map(Function<? super T, ? extends U> mapper) {
    return new Chain<>(() -> mapper.apply(value));
}

寫到這裡發現,使用 Supplier 的思路雖然沒錯,但要直接從“值”構造 Chain 物件還挺不容易的 —— 當然可以加一個建構函式的過載來解決這個問題,但我想像 Optional 那樣寫兩個靜態方法來實現,同時隱藏建構函式。修改後完整的 Chain 如下:

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public class Chain<T> {
    private final T value;

    public static <T> Chain<T> of(T value) {
        return new Chain<>(value);
    }

    public static <T> Chain<T> from(Supplier<T> supplier) {
        return new Chain<>(supplier.get());
    }

    private Chain(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public Chain<T> act(Consumer<? super T> consumer) {
        consumer.accept(this.value);
        return this;
    }

    public <U> Chain<U> map(Function<? super T, ? extends U> mapper) {
        return Chain.of(mapper.apply(value));
    }
}

繼續改造 Chain

map() 總是會返回一個新的 Chain 物件。如果某次處理中存在很多個 map 步驟,那就會產生很多個 Chain 物件。能不能在一個 Chain 物件中解決呢?

定義 Chain 的時候用到了泛型,而泛型型別在編譯後就會被擦除掉,和我們直接把 value 定義成 Object 沒多大區別。既然如此,map() 的時候,直接把 value 給換掉,而不是產生新的 Chain 物件是否可行呢?—— 確實可行。但是一方面需要繼續使用泛型來約束 consumermapper,另一方面,需要在內部進行強制的型別轉換,還得保證這個轉換不會有問題。

理論上來說,Chain 處理的是鏈式呼叫,一環扣一環,而每一環的結果都儲存在 value 中用於下一環的開始。因此在泛型約束下,不管怎麼變化都是不會出問題的。理論可行,不如實踐一下:

// 因為存在大量的型別轉換(邏輯確認可行),需要忽略掉相關警告
@SuppressWarnings("unchecked")
public class Chain<T> {
    // 把 value 宣告為 Object 型別,以便引用各種型別的值
    // 同時去掉 final 修飾,使之可變
    private Object value;

    public static <T> Chain<T> of(T value) {
        return new Chain<>(value);
    }

    public static <T> Chain<T> from(Supplier<T> supplier) {
        return new Chain<>(supplier.get());
    }

    private Chain(T value) {
        this.value = value;
    }

    public T getValue() {
        // 使用到 value 的地方都需要把 value 轉換為 Chain<> 的泛型引數型別,下同
        return (T) value;
    }

    public Chain<T> act(Consumer<? super T> consumer) {
        consumer.accept((T) this.value);
        return this;
    }

    public <U> Chain<U> map(Function<? super T, ? extends U> mapper) {
        // mapper 的計算結果無所謂是什麼型別都可以給 Object 型別的 value 賦值
        this.value = mapper.apply((T) value);
        // 返回的 Chain 雖然還是自己(就這個物件),但是泛型引數得換成 U 了
        // 換了型別之後,後序的操作才會基於 U 型別來進行
        return (Chain<U>) this;
    }
}

最後那句型別轉換 (Chain<U>) this 很是靈性,Java 中可以這麼幹(因為有型別擦除),C# 中無論如何都做不到!

再寫段程式碼來試驗一下:

public static void main(String[] args) {
    // 注意:String 的操作會產生新的 String 物件,所以要用 map
    Chain.of("     Hello World  ")
        .map(String::trim)
        .map(String::toLowerCase)
        // ↓ 把 String 拆分成 String[],這裡轉換了不相容型別
        .map(s ->s.split("\s+"))
        // ↓ 消費這個 String[],依次列印出來
        .act(ss -> Arrays.stream(ss).forEach(System.out::println));
}

輸出結果正如預期:

hello
world

結語

為了解決鏈式呼叫的問題,我們在上一篇文章中研究了擴充套件方法,研究得有點“過度”。這次迴歸本源,就處理鏈式呼叫。

研究過程中如果有想法,不妨一試。如果發現 JDK 中有類似的處理方式,不防去看看原始碼 —— 畢竟 OpenJDK 是開源的!

還有一點,Java 泛型的型別擦除特性有時候確實會帶來不便,但也有些時候是真的方便!

相關文章