1. 程式人生 > >常用設計模式--外觀模式

常用設計模式--外觀模式

概述

外觀模式提供了一個統一的介面,用來訪問子系統中的一群介面。外觀定義了一個高層介面,讓子系統更容易使用。

飛機駕駛艙不少人都見過,當看到那些密密麻麻的按鈕時,心想要是能一鍵啟動就好了。在程式碼的世界裡,我們也常常遇到一個業務功能需要呼叫很多介面甚至很多系統的情況,就像下圖:

enter image description here

有時候被我們呼叫模組之間還需要互相呼叫,模組之間的關係都可以畫出一張蜘蛛網。在這種情況下,要求開發者需要對每一個模組都有一定的瞭解,還需要了解他們之間的關係,開發一個功能的成本簡直太高了,令人崩潰。

飛機駕駛艙的按鈕由於某些原因不可能做成一鍵啟動,但是我們的程式碼可以:

enter image description here

外觀就是這個一鍵啟動的按鈕,它將多個模組或系統的程式碼進行了整合,而我們只要簡單地呼叫外觀暴露出來的一個介面。

這就是外觀模式(也叫門面模式),其作用顯而易見,就是提供一個簡單介面來呼叫後方一群複雜的介面。

在外觀模式中主要有三個角色:

  1. 子系統:已有模組或子系統,提供了一系列複雜的介面或功能
  2. 外觀(門面):它瞭解子系統,並對外暴露一個簡單的介面
  3. 客戶:呼叫外觀提供的介面來實現功能,無需瞭解複雜的子系統

程式碼示例

下面我們寫一個簡單的電腦啟動的例子。(這個例子在許多設計模式教程中都曾出現,筆者認為這是最好的例子之一,直接借用了)

啟動電腦我們通常只需要按下開機鍵就可以了,但電腦內部實際上啟動了多個模組,如 CPU,硬碟,記憶體等

enter image description here

開機鍵就是一個很好的外觀,讓程式設計師們無需瞭解 CPU,記憶體和硬碟如何啟動。

//CPU
public class CPU {
    public void start(){
        System.out.println("啟動CPU");
    }
}
//硬碟
public class Disk {
    public void start(){
        System.out.println("啟動硬碟");
    }
}
//記憶體
public class Memory {
    public void start(){
        System.out.println("啟動記憶體");
    }
}

如果沒有開機鍵,我們需要這麼做:

new CPU().start();
new Disk().start();
new Memory().start();

有了開機鍵,這些操作都交給開機鍵去做:

//開機鍵
public class StartBtn {

    public void start(){
        new CPU().start();
        new Disk().start();
        new Memory().start();
    }
}

而我們只需要:

new StartBtn().start();

外觀模式不僅為我們提供了一個簡單方便的介面,也讓我們的系統和子系統解耦。

迪米特法則(最少知道原則)

迪米特法則是說每一個類都應該儘量的少知道別的類,外觀模式就是迪米特法則的應用。原本我們需要知道許多的子系統或介面,用了外觀類之後,我們僅僅需要知道外觀類即可。

換句話說就是:知道的太多對你沒好處。

迪米特法則是希望類之間減少耦合,類越獨立越好。有句話叫牽一髮而動全身,如果類之間關係太緊密,與之關聯的類太多,一旦你修改該類,也許會動到無數與之關聯的類。

實際案例

Java 三層結構

用 Java 開發我們經常使用三層結構:

  • controller 控制器層
  • service 服務層
  • dao 資料訪問層

有時候業務很簡單,例如根據使用者ID或者使用者資訊,Service 層這樣寫:

User getUserById(Integer id){
    return userDao.getUserById(id);
}

dao:

User getUserById(Integer id){
    //查詢資料庫
    return user;
}

Service 層直接呼叫了userDao 的 getUserById() , Service 本身並沒有執行什麼額外的程式碼,那麼為什麼不省去 Service 層呢?

其實三層結構也蘊含了外觀模式的思想在內。假如 Service 有一個轉賬方法:

public boolean transMoney(Integer user1,Integer user2,Float money){
    //使用者1加錢
    userDao.addMoney(user1,money);
    //使用者2扣錢
    userDao.decMoney(user2,money);
    //轉賬日誌
    logDao.addLog(user1,user2,money);
}

作為呼叫方來說,並不想知道轉賬操作具體要呼叫哪些 Dao,一行程式碼 transMoney() 就能搞定豈不是皆大歡喜。

因此 Service 是很有必要的,一般在業務系統中,Service 層的類不僅僅是簡單的呼叫 dao,而是作為外觀,給 Controller 提供了更方便好用的介面。

不過無論多複雜的系統,總會有 Service 直接呼叫 dao 的getUserById() 的情況 ,我們是否可以偷懶直接在 Controller 呼叫 Dao 呢?

理論上是沒問題的,但是強烈建議不要這麼幹,因為這樣會導致層侵入,三層結構的層級混亂。

除非你的業務真的簡單到極致,那麼幹脆直接捨棄 Service 層。只要你有Service 層,就請不要跨層呼叫。

Tomcat中的外觀模式

在做 Servlet 開發時,我們經常用的兩個物件就是HttpServletRequest 和 HttpServletResponse ,但我們拿到的這兩個物件其實是被 Tomcat 經過了外觀包裹的物件,那麼 Tomcat 為什麼要這麼做呢?

首先我們先來了解一下HttpServletRequest ,通過原始碼可以發現,HttpServletRequest 是一個介面,有兩個類 RequestFacade 和 Request 實現了 HttpServletRequest :

enter image description here

Facade 命名的,毫無疑問是用了外觀模式,下面給出一部分原始碼:

Request類,繼承 HttpServletRequest :

public class Request implements HttpServletRequest {

}

當我們需要使用 Request 物件時,Tomcat 傳給我們的其實並不是 Request物件,而是 RequestFacade

RequestFacade 類:

package org.apache.catalina.connector;

public class RequestFacade implements HttpServletRequest {

    protected Request request = null;

    public RequestFacade(Request request) {
        this.request = request;
    }

    public Object getAttribute(String name) {
        if (this.request == null) {
            throw new IllegalStateException(sm.getString("requestFacade.nullRequest"));
        } else {
            return this.request.getAttribute(name);
        }
    }


    public String getProtocol() {
        if (this.request == null) {
            throw new IllegalStateException(sm.getString("requestFacade.nullRequest"));
        } else {
            return this.request.getProtocol();
        }
    }

}

從上面 RequestFacade 原始碼中可以看到,當呼叫 getAttribute() , getProtocol() 等方法時,其實還是呼叫了Request 物件的getAttribute() 方法。

既然如此,為什麼要多次一舉弄個 RequestFacade 呢 ,其實是為了安全,Tomcat 不想把過多的方法暴露給別人。

Tomcat 內部有很多元件,元件之間經常需要通訊,有些方法不得不定義為Public,這樣才能被其他元件所呼叫。

但是有些方法只希望內部通訊用,並不想暴露給 Web 開發者,否則會有安全問題。所以定義一個外觀類,只實現想要暴露給外部的方法。

所以 Tomcat 要傳 Request 給我們的時候,其實是這麼做的:

return new RequestFacade(request);

從這個案例中可以看出外觀模式不僅僅用於將複雜的介面包裝為一個簡單的介面,也可以用於隱藏一些不想暴露給別人的方法或介面。

總結

外觀模式主要使用場景:

  • 包裝多個複雜的子系統,提供一個簡單的介面
  • 重新包裝系統,隱藏不想暴露的介面

優缺點

將複雜的介面簡單化,減少了客戶端與介面之間的耦合,提高了安全性。可能產生大量的中間類(外觀類),一定程度上增加了系統的複雜度。