函數語言程式設計讓你忘記設計模式

javaadu發表於2019-07-07

本文是一篇《Java 8實戰》的閱讀筆記,閱讀大約需要5分鐘。

有點標題黨,但是這確實是我最近使用Lambda表示式的感受。設計模式是過去的一些好的經驗和套路的總結,但是好的語言特性可以讓開發者不去考慮這些設計模式。物件導向常見的設計模式有策略模式、模板方法、觀察者模式、責任鏈模式以及工廠模式,使用Lambda表示式(函數語言程式設計思維)有助於避免物件導向開發中的那些固定程式碼。下面我們挑選了策略模式和職責鏈模式兩個案例進行分析。

案例1:策略模式

函數語言程式設計讓你忘記設計模式

當我們解決一個問題有不同的解法的時候,又不希望客戶感知到這些解法的細節,這種情況下適合使用策略模式。策略模式包括三個部分:

  • 解決問題的演算法(上圖中的Strategy);
  • 一個或多個該類演算法的具體實現(上圖中的ConcreteStrategyA、ConcreteStrategyB和ConcreteStrategyC)
  • 一個或多個客戶使用場景(上圖中的ClientContext)

物件導向思路

首先定義策略介面,表示排序策略:

public interface ValidationStrategy {
    boolean execute(String s);
}

然後定義具體的實現類(即不同的排序演算法):

public class IsAllLowerCase implements ValidationStrategy {
    @Override
    public boolean execute(String s) {
        return s.matches("[a-z]+");
    }
}

public class IsNumberic implements ValidationStrategy {
    @Override
    public boolean execute(String s) {
        return s.matches("\\d+");
    }
}

最後定義客戶使用場景,程式碼如下圖所示。Validator是為客戶提供服務時使用的上下文環境,每個Valiator物件中都封裝了具體的Strategy物件,在實際工作中,我們可以通過更換具體的Strategy物件來進行客戶服務的升級,而且不需要讓客戶進行升級。

public class Validator {

    private final ValidationStrategy strategy;

    public Validator(ValidationStrategy strategy) {
        this.strategy = strategy;
    }

    /**
     * 給客戶的介面
     */
    public boolean validate(String s) {
        return strategy.execute(s);
    }
}

public class ClientTestDrive {

    public static void main(String[] args) {
        Validator numbericValidator = new Validator(new IsNumberic());
        boolean res1 = numbericValidator.validate("7780");
        System.out.println(res1);

        Validator lowerCaseValidator = new Validator(new IsAllLowerCase());
        boolean res2 = lowerCaseValidator.validate("aaaddd");
        System.out.println(res2);
    }
}

函數語言程式設計思路

如果使用Lambda表示式考慮,你會發現ValidationStrategy就是一個函式介面(還與Predicate具有同樣的函式描述),那麼就不需要定義上面那些實現類了,可以直接用下面的程式碼替換,原因是Lambda表示式內部已經對這些類進行了一定的封裝。

public class ClientTestDrive {

    public static void main(String[] args) {
        Validator numbericValidator = new Validator((String s) -> s.matches("\\d+"));
        boolean res1 = numbericValidator.validate("7789");
        System.out.println(res1);

        Validator lowerCaseValidator = new Validator((String s) -> s.matches("[a-z]+"));
        boolean res2 = lowerCaseValidator.validate("aaaddd");
        System.out.println(res2);
    }
}

案例2:責任鏈模式

在某些場景下,需要對一個物件做一系列的工作,這些工作分別是由不同的類完成的,這時候就比較適合使用責任鏈模式。責任鏈模式的主要組成部分包括三個:

  • 管理操作序列的抽象類,在該抽象類裡有會有一個物件記錄當前物件的後繼操作物件;
  • 一些具體的操作物件,這些操作物件會以一個連結串列的形式組織起來
  • 一個使用該模式的客戶端元件,該元件只需要跟一個元件打交道就好,不需要跟很多個操作物件耦合在一起。

函數語言程式設計讓你忘記設計模式

物件導向思路

首先看下我們這裡定義了一個抽象類ProcessingObject,其中successor欄位用於管理該物件的後繼操作物件;handle介面作為對外提供服務的介面;handleWork作為實際處理物件的操作方法。

public abstract class ProcessingObject<T> {

    protected ProcessingObject<T> successor;
    
    public void setSuccessor(ProcessingObject<T> successor) {
        this.successor = successor;
    }

    public T handler(T input) {
        T r = handleWork(input);
        if (successor != null) {
            return successor.handler(r);
        }
        return r;
    }

    abstract protected T handleWork(T input);
}

接下來可以定義兩個具體的操作物件,如下面程式碼所示。PS:這裡《Java 8實戰》書中用的是replaceAll方法是不太合適的,這個點可以參考我們之前的文章——020:舉幾個String的API以及案例

public class HeaderTextProcessing extends ProcessingObject<String> {
    @Override
    protected String handleWork(String input) {
        return "From Raoul, Mario and Alan: " + input;
    }
}

public class SpellCheckerProcessing extends ProcessingObject<String> {
    @Override
    protected String handleWork(String input) {
        return input.replace("labda", "lambda");
    }
}

最後,你就可以在Client中將這上面兩個具體的操作類物件構成一個操作序列,參見下面的程式碼:

public class Client {
    public static void main(String[] args) {
        ProcessingObject<String> p1 = new HeaderTextProcessing();
        ProcessingObject<String> p2 = new SpellCheckerProcessing();

        p1.setSuccessor(p2);

        String result = p1.handler("Aren't labdas really sexy?!!");
        System.out.println(result);
    }
}

函數語言程式設計思路

如果使用函數語言程式設計思維,那麼職責鏈模式就直接了——y=f(x)和z=g(x)這兩個方法都是要對x做處理,那麼如果將這兩個函式組合在一起,就會形成r=f(g(x))的情況,也就是可以使用Lambda表示式中的addThen來串聯起多個處理過程。

public class ClientWithLambda {
    public static void main(String[] args) {
        UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;

        UnaryOperator<String> spellCheckProcessing = (String text) -> text.replace("labda", "lambda");

        Function<String, String> function = headerProcessing.andThen(spellCheckProcessing);

        String result = function.apply("Aren't labdas really sexy?!!");
        System.out.println(result);

        UnaryOperator<String> hhhhhProcessing = (String text) -> text.concat("hhhh");
        Function<String, String> function1 = function.andThen(hhhhhProcessing);
        String result1 = function1.apply("Aren't labdas really sexy?!!");
        System.out.println(result1);
    }
}

上面是利用Java原生的Lambda表示式實現的職責鏈模式,我們也可以使用前面一篇文章——vavr:讓你像寫Scala一樣寫Java中介紹過的vavr庫來實現,程式碼如下所示:

public class ClientWithVavr {
    public static void main(String[] args) {
        Function1<String, String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;
        Function1<String, String> specllCheckProcessing = (String text) -> text.replace("labda", "lambda");

        Function1<String, String> function = headerProcessing.compose(specllCheckProcessing);
        String result = function.apply("Aren't labdas really sexy?!!");
        System.out.println(result);
    }
}

總結

可以看出,函數語言程式設計思維跟物件導向程式設計思維的思考方式是不同的,表達力更強,因此,作為開發者是時候認真學習下函數語言程式設計思維了,作為Java開發者,我準備先從Lambda表示式開始學起,然後嘗試學習下Scala或Kotlin兩門語言中的函數語言程式設計特性。

參考資料

  1. 《Java程式設計實戰》
  2. 《設計模式之禪》

本號專注於後端技術、JVM問題排查和優化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發者的工作和成長經驗,期待你能在這裡有所收穫。javaadu

相關文章