「補課」進行時:設計模式(16)——簡單又實用的門面模式

極客挖掘機發表於2020-12-03

1. 前文彙總

「補課」進行時:設計模式系列

2. 從銀行轉賬說起

當我們在銀行進行轉賬操作的時候,整個操作流程我們可以簡化為賬戶 A 扣費,然後賬戶 B 增加餘額,最後轉賬操作成功。

這兩個操作缺一不可,同時又不能顛倒順序。

簡單定義一個轉賬的介面 ITransfer

public interface ITransfer {
    // 首先發起轉賬
    void start(String amount);
    // 賬戶 A 進行扣費
    void subtractionA();
    // 賬戶 B 增加金額
    void addB();
    // 轉賬完成
    void end();
}

然後增加一個介面實現類:

public class TransferImpl implements ITransfer {
    @Override
    public void start(String amount) {
        System.out.println(String.format("賬戶 A 開始向賬戶 B 進行轉賬: %s 元。", amount));
    }

    @Override
    public void subtractionA() {
        System.out.println("賬戶 A 扣費成功");
    }

    @Override
    public void addB() {
        System.out.println("賬戶 B 餘額增加成功");
    }

    @Override
    public void end() {
        System.out.println("轉賬完成");
    }
}

來一個測試類:

public class Test {
    public static void main(String[] args) {
        ITransfer transfer = new TransferImpl();
        transfer.start("1000");
        transfer.subtractionA();
        transfer.addB();
        transfer.end();
    }
}

最後執行的結果如下:

賬戶 A 開始向賬戶 B 進行轉賬: 1000 元。
賬戶 A 扣費成功
賬戶 B 餘額增加成功
轉賬完成

我們回過頭來看看這個過程,它與高內聚的要求相差甚遠,更不要說迪米特法則、介面隔離原則了。

如果我們要進行轉賬操作,那麼我們必須要知道這幾個步驟,而且還要知道它們的順序,一旦出錯,轉賬操作就無法完成,這在物件導向的程式設計中是極度地不適合,它根本就沒有完成一個類所具有的單一職責。

那怎麼辦呢?這時候銀行櫃檯出現了,我們只需要把需求告訴銀行櫃檯,櫃檯會直接幫我們完成轉賬操作。

銀行櫃檯:

public class BankCounter {
    private ITransfer transfer = new TransferImpl();
    // 轉賬操作一體化
    public void transferAmount(String amount) {
        transfer.start(amount);
        transfer.subtractionA();
        transfer.addB();
        transfer.end();
    }
}

接下來修改一下測試類:

public class Test1 {
    public static void main(String[] args) {
        BankCounter counter = new BankCounter();
        counter.transferAmount("1000");
    }
}

和剛才的執行結果一樣,但是整個測試類卻簡化了很多,只要關心和銀行櫃檯進行互動就行,完全不用自己操心之前的賬戶 A 扣費,再給賬戶 B 加餘額,但是,每次轉賬就這麼直接轉賬有點不大安全,假如賬戶 A 的餘額根本不足轉賬的費用,那麼就不應該轉賬成功。

增加一個餘額校驗類 Balance 對賬戶餘額進行校驗:

public class Balance {
    Boolean checkBalance() {
        System.out.println("賬戶餘額校驗成功");
        return true;
    }
}

這時候,測試類無需改動,只需修改銀行櫃檯類就可以:

public class BankCounter {
    private ITransfer transfer = new TransferImpl();
    private Balance balance = new Balance();
    // 轉賬操作一體化
    public void transferAmount(String amount) {
        transfer.start(amount);
        transfer.subtractionA();
        // 增加餘額校驗
        if (balance.checkBalance()) {
            transfer.addB();
            transfer.end();
        }
    }
}

這裡只增加了一個餘額校驗類,並且對轉賬的過程進行了修改,這個過程對於我們來講是完全透明的,我們完全不需要關心轉賬的過程,這個過程由銀行櫃檯全部幫我們辦好了。

高層模組沒有任何改動,但是賬戶的餘額已經被檢查過了,不改變子系統對外暴露的介面、方法,只改變內部的處理邏輯,其他兄弟模組的呼叫產生了不同的結果。

是不是非常簡單,沒錯,這就是門面模式或者說外觀模式。

3. 門面模式

3.1 定義

門面模式(Facade Pattern)也叫做外觀模式,是一種比較常用的封裝模式,其定義如下:

Provide a unified interface to a set of interfaces in a subsystem.Facadedefines a higher-level interface that makes the subsystem easier to use.(要求一個子系統的外部與其內部的通訊必須通過一個統一的物件進行。門面模式提供一個高層次的介面,使得子系統更易於使用。)

3.2 通用類圖

門面模式注重「統一的物件」,也就是提供一個訪問子系統的介面,除了這個介面不允許有任何訪問子系統的行為發生,其通用類圖:

是的,類圖就這麼簡單,但是它代表的意義可是異常複雜,Subsystem Classes是子系統所有類的簡稱,它可能代表一個類,也可能代表幾十個物件的集合。甭管多少物件,我們把這些物件全部圈入子系統的範疇:

再簡單地說,門面物件是外界訪問子系統內部的唯一通道,不管子系統內部是多麼雜亂無章,只要有門面物件在,就可以做到「金玉其外,敗絮其中」。我們先明確一下門面模式的角色。

  • Facade 門面角色:此角色知曉子系統的所有功能和責任。一般情況下,本角色會將所有從客戶端發來的請求委派到相應的子系統去,也就說該角色沒有實際的業務邏輯,只是一個委託類。
  • subsystem 子系統角色:可以同時有一個或者多個子系統。每一個子系統都不是一個單獨的類,而是一個類的集合。子系統並不知道門面的存在。對於子系統而言,門面僅僅是另外一個客戶端而已。

3.3 通用程式碼

子系統:

// 
public class ClassA {
    public void doSomethingA() {
        // 執行邏輯 A
    }
}

public class ClassB {
    public void doSomethingB() {
        // 執行邏輯 A
    }
}

public class ClassC {
    public void doSomethingC() {
        // 執行邏輯 A
    }
}

門面類:

public class Facade {
    private ClassA classA = new ClassA();
    private ClassB classB = new ClassB();
    private ClassC classC = new ClassC();
    public void methodA() {
        this.classA.doSomethingA();
    }
    public void methodB() {
        this.classB.doSomethingB();
    }
    public void methodC() {
        this.classC.doSomethingC();
    }
}

4. 注意

有一點需要注意的是:門面不參與子系統內的業務邏輯。

這句話怎麼理解?舉一個簡單的例子:

把上面的通用程式碼稍微改一下,在 methodC() 方法上先呼叫 ClassAdoSomethingA() 方法,然後再呼叫 ClassCdoSomethingC() 方法,修改後的門面類如下:

public class Facade {
    private ClassA classA = new ClassA();
    private ClassB classB = new ClassB();
    private ClassC classC = new ClassC();
    public void methodA() {
        this.classA.doSomethingA();
    }
    public void methodB() {
        this.classB.doSomethingB();
    }
    public void methodC() {
        this.classA.doSomethingA();
        this.classC.doSomethingC();
    }
}

非常簡單,只是在 methodC() 方法中增加了 doSomethingA() 方法的呼叫,可以這樣做嗎?

我相信在大多數的日常開發中,我們很多時候都是直接這麼寫了,這麼寫有什麼問題麼?

當然有,因為這種做法讓門面物件參與了業務邏輯,門面物件只是提供一個訪問子系統的一個路徑而已,它不應該也不能參與具體的業務邏輯,否則就會產生一個倒依賴的問題:子系統必須依賴門面才能被訪問。

那麼在這種情況下可以怎麼處理呢?

也很簡單,建立一個封裝類,封裝完畢後提供給門面物件:

public class Context {
    private ClassA classA = new ClassA();
    private ClassC classC = new ClassC();
    // 複雜的業務操作
    public void complexMethod() {
        this.classA.doSomethingA();
        this.classC.doSomethingC();
    }
}

這個封裝類存在的價值就是產生一個複雜的業務規則 complexMethod() ,並且它的生存環境是在子系統內,僅僅依賴兩個相關的物件,門面物件通過對它的訪問完成一個複雜的業務邏輯,最後我們通過門面模式進行呼叫的時候直接呼叫封裝類:

public class Facade1 {
    private ClassA classA = new ClassA();
    private ClassB classB = new ClassB();
    private Context context = new Context();
    public void methodA() {
        this.classA.doSomethingA();
    }
    public void methodB() {
        this.classB.doSomethingB();
    }
    public void methodC() {
        this.context.complexMethod();
    }
}

通過這樣一次封裝後,門面物件又不參與業務邏輯了,在門面模式中,門面角色應該是穩定,它不應該經常變化,一個系統一旦投入執行它就不應該被改變,它是一個系統對外的介面,你變來變去還怎麼保證其他模組的穩定執行呢?但是,業務邏輯是會經常變化的,我們已經把它的變化封裝在子系統內部,無論你如何變化,對外界的訪問者來說,都還是同一個門面,同樣的方法——這才是架構師最希望看到的結構。

相關文章