【結構型模式七】外觀模式(Facade)

1 場景問題#

1.1 生活中的示例##

外觀模式在現實生活中的示例很多,比如:組裝電腦,通常會有兩種方案。

一個方案是去電子市場把自己需要的配件都買回來,然后自己組裝,絕對DIY(Do It Yourself)。這個方案好是好,但是需要對各種配件都要比較熟悉,這樣才能選擇最合適的配件,而且還要考慮配件之間的兼容性。如圖所示:

客戶完全自己組裝電腦

另外一個方案,就是到電子市場,找一家專業裝機的公司,把具體的要求一講,然后就等著拿電腦就好了。當然價格會比自己全部DIY貴一些,但綜合起來這還算是個不錯的選擇,估計也是大多數人的選擇。如圖所示:

找專業的裝機公司組裝電腦

這個專業的裝機公司就相當于本章的主角——Facade。有了它,我們就不用自己去跟眾多賣配件的公司打交道,只需要跟裝機公司交互就可以了,由裝機公司去跟各個賣配件的公司交互,并組裝好電腦返回給我們。

把上面的過程抽象一下,如果把電子市場看成是一個系統,而各個賣配件的公司看成是模塊的話,就類似于出現了這樣一種情況:

客戶端為了完成某個功能,需要去調用某個系統中的多個模塊,把它們稱為是A模塊、B模塊和C模塊吧,對于客戶端而言,那就需要知道A、B、C這三個模塊的功能,還需要知道如何組合這多個模塊提供的功能來實現自己所需要的功能,非常麻煩

要是有一個簡單的方式能讓客戶端去實現相同的功能多好啊,這樣:

客戶端就不用跟系統中的多個模塊交互,而且客戶端也不需要知道那么多模塊的細節功能了,實現這個功能的就是Facade

1.2 代碼生成的應用##

很多公司都有這樣的應用工具,能根據配置生成代碼。一般這種工具都是公司內部使用,較為專有的工具,生成的多是按照公司的開發結構來實現的常見的基礎功能,比如增刪改查。這樣每次在開發實際應用的時候,就可以以很快的速度把基本的增刪改查實現出來,然后把主要的精力都放在業務功能的實現上。

當然這里不可能去實現一個這樣的代碼生成工具,那需要整本書的內容,這里僅用它來說明外觀模式。

假設使用代碼生成出來的每個模塊都具有基本的三層架構,分為:表現層、邏輯層和數據層,那么代碼生成工具里面就應該有相應的代碼生成處理模塊。

另外,代碼生成工具自身還需要一個配置管理的模塊,通過配置來告訴代碼生成工具,每個模塊究竟需要生成哪些層的代碼,比如:通過配置來描述,是只需要生成表現層代碼呢,還是三層都生成。具體的模塊示意如圖所示:

代碼生成工具的模塊示意圖

那么現在客戶端需要使用這個代碼生成工具來生成需要的基礎代碼,該如何實現呢?

1.3 不用模式的解決方案##

有朋友會想,開發一個這樣的工具或許會比較麻煩,但是使用一下,應該不難吧,直接調用不就可以了。

在示范客戶端之前,先來把工具模擬示范出來,為了簡單,每個模塊就寫一個類,而且每個類只是實現一個功能,僅僅做一個示范。

  1. 先看看描述配置的數據Model,示例代碼如下:
/**
   * 示意配置描述的數據Model,真實的配置數據會很多
   */
public class ConfigModel {
      /**
       * 是否需要生成表現層,默認是true
       */
      private boolean needGenPresentation = true;
      /**
       * 是否需要生成邏輯層,默認是true
       */
      private boolean needGenBusiness = true;
      /**
       * 是否需要生成DAO,默認是true
       */
      private boolean needGenDAO = true;
      public boolean isNeedGenPresentation() {
          return needGenPresentation;
      }
      public void setNeedGenPresentation(
          this.needGenPresentation = needGenPresentation;
      }
      public boolean isNeedGenBusiness() {
          return needGenBusiness;
      }
      public void setNeedGenBusiness(boolean needGenBusiness) {
          this.needGenBusiness = needGenBusiness;
      }
      public boolean isNeedGenDAO() {
          return needGenDAO;
      }
      public void setNeedGenDAO(boolean needGenDAO) {
          this.needGenDAO = needGenDAO;
      }
}
  1. 接下來看看配置管理的實現示意,示例代碼如下:
/**
   * 示意配置管理,就是負責讀取配置文件,
   * 并把配置文件的內容設置到配置Model中去,是個單例
   */
public class ConfigManager {
    private static ConfigManager manager = null;
    private static ConfigModel cm = null;
    private ConfigManager(){
        //
    }
    public static ConfigManager getInstance(){
        if(manager == null){
            manager = new ConfigManager();
            cm = new ConfigModel();
            //讀取配置文件,把值設置到ConfigModel中去,這里省略了
        }
        return manager;
    }
    /**
     * 獲取配置的數據
     * @return 配置的數據
     */
    public ConfigModel getConfigData(){
        return cm;
    }
}
  1. 接下來就來看看各個生成代碼的模塊,在示意中,它們的實現類似,就是獲取配置文件的內容,然后按照配置來生成相應的代碼。分別看看它們的示意實現,先看生成表現層的示意實現,示例代碼如下:
/**
   * 示意生成表現層的模塊
   */
public class Presentation {
    public void generate(){
        //1:從配置管理里面獲取相應的配置信息
        ConfigModel cm = ConfigManager.getInstance().getConfigData();
        if(cm.isNeedGenPresentation()){
            //2:按照要求去生成相應的代碼,并保存成文件
            System.out.println("正在生成表現層代碼文件");
        }
    }
}

/**
   * 示意生成邏輯層的模塊
   */
public class Business {
    public void generate(){
        ConfigModel cm = ConfigManager.getInstance().getConfigData();
        if(cm.isNeedGenBusiness()){
            System.out.println("正在生成邏輯層代碼文件");
        }
    }
}

/**
   * 示意生成數據層的模塊
   */
public class DAO {
    public void generate(){
        ConfigModel cm = ConfigManager.getInstance().getConfigData();
        if(cm.isNeedGenDAO()){
            System.out.println("正在生成數據層代碼文件");
        }
    }
}
  1. 此時的客戶端實現,就應該自行去調用這多個模塊了,示例代碼如下:
public class Client {
    public static void main(String[] args) {
        //現在沒有配置文件,就直接使用默認的配置,通常情況下,三層都應該生成,
        //也就是說客戶端必須對這些模塊都有了解,才能夠正確使用它們
        new Presentation().generate();
        new Business().generate();
        new DAO().generate();
    }
}

1.4 有何問題##

仔細查看上面的實現,會發現其中有一個問題:那就是客戶端為了使用生成代碼的功能,需要與生成代碼子系統內部的多個模塊交互

這對于客戶端而言,是個麻煩,使得客戶端不能簡單的使用生成代碼的功能。而且,如果其中的某個模塊發生了變化,還可能會引起客戶端也需要跟著變化。那么如何實現,才能讓子系統外部的客戶端在使用子系統的時候,既能簡單的使用這些子系統內部的模塊功能,而又不用客戶端去與子系統內的多個模塊交互呢?

2 解決方案#

2.1 外觀模式來解決##

用來解決上述問題的一個合理的解決方案就是外觀模式。那么什么是外觀模式呢?

  1. 外觀模式定義
外觀模式定義

這里先對兩個詞進行一下說明,一個是界面,一個是接口

一提到界面,估計很多朋友的第一反應就是圖形界面(GUI)。其實在這里提到的界面,主要指的是從一個組件外部來看這個組件,能夠看到什么,這就是這個組件的界面,也就是所說的外觀

比如:你從一個類外部來看這個類,那么這個類的public方法就是這個類的外觀,因為你從類外部來看這個類,就能看到這些。

再比如:你從一個模塊外部來看這個模塊,那么這個模塊對外的接口就是這個模塊的外觀,因為你就只能看到這些接口,其它的模塊內部實現的東西是被接口封裝隔離了的。

一提到接口,做Java的朋友的第一反應就是interface。其實在這里提到的接口,主要是指的外部和內部交互的這么一個通道,通常是指的一些方法,可以是類的方法,也可以是interface的方法。也就是說,這里說的接口,并不等價于interface,也有可能是一個類。

  1. 應用外觀模式來解決的思路

仔細分析上面的問題,客戶端想要操作更簡單點,那就根據客戶端的需要來給客戶端定義一個簡單的接口,然后讓客戶端調用這個接口,剩下的事情就不用客戶端管,這樣客戶端就變得簡單了。

當然,這里所說的接口就是客戶端和被訪問的系統之間的一個通道,并不一定是指Java的interface。事實上,這里所說的接口,在外觀模式里面,通常指的是類,這個類被稱為“外觀”。

外觀模式就是通過引入這么一個外觀類,在這個類里面定義客戶端想要的簡單的方法,然后在這些方法的實現里面,由外觀類再去分別調用內部的多個模塊來實現功能,從而讓客戶端變得簡單,這樣一來,客戶端就只需要和外觀類交互就可以了。

2.2 模式結構和說明##

外觀模式的結構如圖所示:

外觀模式結構示意圖

Facade:定義子系統的多個模塊對外的高層接口,通常需要調用內部多個模塊,從而把客戶的請求代理給適當的子系統對象。

模塊:接受Facade對象的委派,真正實現功能,各個模塊之間可能有交互。但是請注意,Facade對象知道各個模塊,但是各個模塊不應該知道Facade對象

2.3 外觀模式示例代碼##

由于外觀模式的結構圖過于抽象,因此把它稍稍具體點,假設子系統內有三個模塊,分別是AModule、BModule和CModule,它們分別有一個示意的方法,那么此時示例的整體結構如圖所示:

外觀模式示例代碼
  1. 首先定義A模塊的接口,A模塊對外提供功能方法,從抽象的高度去看,可以是任意的功能方法,示例代碼如下:
/**
   * A模塊的接口
   */
public interface AModuleApi {
      /**
       * 示意方法,A模塊對外的一個功能方法
       */
      public void testA();
}
  1. 實現A模塊的接口,簡單示范一下,示例代碼如下:
public class AModuleImpl implements AModuleApi{
      public void testA() {
          System.out.println("現在在A模塊里面操作testA方法");
      } 
}
  1. 同理定義和實現B模塊、C模塊,先看B模塊的接口定義,示例代碼如下:
public interface BModuleApi {
      public void testB();
}

public class BModuleImpl implements BModuleApi{
      public void testB() {
          System.out.println("現在在B模塊里面操作testB方法");
      }
}

public interface CModuleApi {
      public void testC();
}

public class CModuleImpl implements CModuleApi{
      public void testC() {
          System.out.println("現在在C模塊里面操作testC方法");
      }
}
  1. 定義外觀對象,示例代碼如下:
/**
   * 外觀對象
   */
public class Facade {
      /**
       * 示意方法,滿足客戶需要的功能
       */
      public void test(){
          //在內部實現的時候,可能會調用到內部的多個模塊
          AModuleApi a = new AModuleImpl();
          a.testA();
          BModuleApi b = new BModuleImpl();
          b.testB();
          CModuleApi c = new CModuleImpl();
          c.testC();
      }
}
  1. 客戶端如何使用呢,直接使用外觀對象就可以了,示例代碼如下:
public class Client {
      public static void main(String[] args) {
          //使用Facade
          new Facade().test();
      }
}

2.4 使用外觀模式重寫示例##

要使用外觀模式重寫前面的示例,其實非常簡單,只要添加一個Facade的對象,然后在里面實現客戶端需要的功能就可以了。

  1. 新添加一個Facade對象,示例代碼如下:
/**
   * 代碼生成子系統的外觀對象
   */
public class Facade {
      /**
       * 客戶端需要的,一個簡單的調用代碼生成的功能
       */
      public void generate(){
          new Presentation().generate();
          new Business().generate();
          new DAO().generate();
      }
}
  1. 其它的定義和實現都沒有變化,這里就不去贅述了

  2. 看看此時的客戶端怎么實現,不再需要客戶端去調用子系統內部的多個模塊,直接使用外觀對象就可以了,示例代碼如下:

public class Client {
      public static void main(String[] args) {
          //使用Facade
          new Facade().generate();
      }
}

如同上面講述的例子,Facade類其實相當于A、B、C模塊的外觀界面,Facade類也被稱為A、B、C模塊對外的接口,有了這個Facade類,那么客戶端就不需要知道系統內部的實現細節,甚至客戶端都不需要知道A、B、C模塊的存在,客戶端只需要跟Facade類交互就好了,從而更好的實現了客戶端和子系統中A、B、C模塊的解耦,讓客戶端更容易的使用系統。

3 模式講解#

3.1 認識外觀模式##

  1. 外觀模式的目的

外觀模式的目的不是給子系統添加新的功能接口,而是為了讓外部減少與子系統內多個模塊的交互,松散耦合,從而讓外部能夠更簡單的使用子系統。

這點要特別注意,因為外觀是當作子系統對外的接口出現的,雖然也可以在這里定義一些子系統沒有的功能,但不建議這么做。外觀應該是包裝已有的功能,它主要負責組合已有功能來實現客戶需要,而不是添加新的實現

  1. 使用外觀跟不使用相比有何變化

看到Facade的實現,可能有些朋友會說,這不就是把原來在客戶端的代碼搬到Facade里面了嗎?沒有什么大變化啊?

沒錯,說的很對,表面上看就是把客戶端的代碼搬到Facade里面了,但實質是發生了變化的,請思考:Facade到底位于何處呢?是位于客戶端還是在由A、B、C模塊組成的系統這邊呢?

答案肯定是在系統這邊,這有什么不一樣嗎?

當然有了,如果Facade在系統這邊,那么它就相當于屏蔽了外部客戶端和系統內部模塊的交互,從而把A、B、C模塊組合成為一個整體對外,不但方便了客戶端的調用,而且封裝了系統內部的細節功能,也就是說Facade與各個模塊交互的過程已經是內部實現了。這樣一來,如果今后調用模塊的算法發生了變化,比如變化成要先調用B,然后調用A,那么只需要修改Facade的實現就可以了。

另外一個好處,Facade的功能可以被很多個客戶端調用,也就是說Facade可以實現功能的共享,也就是實現復用。同樣的調用代碼就只用在Facade里面寫一次就好了,而不用在多個調用的地方重復寫。

還有一個潛在的好處,對使用Facade的人員來說,Facade大大節省了他們的學習成本,他們只需要了解Facade即可,無需再深入到子系統內部,去了解每個模塊的細節,也不用和這多個模塊交互,從而使得開發簡單,學習也容易。

  1. 有外觀,但是可以不使用

雖然有了外觀,如果有需要,外部還是可以繞開Facade,直接調用某個具體模塊的接口,這樣就能實現兼顧組合功能和細節功能。比如在客戶端就想要使用A模塊的功能,那么就不需要使用Facade,可以直接調用A模塊的接口。示例代碼如下:

public class Client {
      public static void main(String[] args) {
          AModuleApi a = new AModuleImpl();
          a.testA();
      }
}
  1. 外觀提供了缺省的功能實現

現在的系統是越做越大、越來越復雜,對軟件的要求也就更高。為了提高系統的可重用性,通常會把一個大的系統分成很多個子系統,再把一個子系統分成很多更小的子系統,一直分下去,分到一個一個小的模塊,這樣一來,子系統的重用性會得到加強,也更容易對子系統進行定制和使用。

但是這也帶來一個問題,如果用戶不需要對子系統進行定制,僅僅就是想要使用它們來完成一定的功能,那么使用起來會比較麻煩,需要跟這多個模塊交互。

外觀對象就可以為用戶提供一個簡單的、缺省的實現,這個實現對大多數的用戶來說都是已經足夠了的。但是外觀并不限制那些需要更多定制功能的用戶,直接越過外觀去訪問內部的模塊的功能

  1. 外觀模式的調用順序示意圖
外觀模式的調用順序示意圖

3.2 外觀模式的實現##

  1. Facade的實現

對于一個子系統而言,外觀類不需要很多,通常可以實現成為一個單例

也可以直接把外觀中的方法實現成為靜態的方法,這樣就可以不需要創建外觀對象的實例而直接就可以調用,這種實現相當于把外觀類當成一個輔助工具類實現。簡要的示例代碼如下:

public class Facade {
      private Facade() {
           
      }
      public static void test(){
          AModuleApi a = new AModuleImpl();
          a.testA();
          BModuleApi b = new BModuleImpl();
          b.testB();
          CModuleApi c = new CModuleImpl();
          c.testC();
      }
}
  1. Facade可以實現成為interface

雖然Facade通常直接實現成為類,但是也可以把Facade實現成為真正的interface,只是這樣會增加系統的復雜程度,因為這樣會需要一個Facade的實現,還需要一個來獲取Facade接口對象的工廠,此時結構如圖所示:

外觀實現成為接口的結構示意圖
  1. Facade實現成為interface的附帶好處

如果把Facade實現成為接口,還附帶一個功能,就是能夠有選擇性的暴露接口方法,盡量減少模塊對子系統外提供的接口方法

換句話說,一個模塊的接口里面定義的方法可以分成兩部分,一部分是給子系統外部使用的,一部分是子系統內部的模塊間相互調用時使用的。有了Facade接口,那么用于子系統內部的接口功能就不用暴露給子系統外部了。

比如,定義如下的A、B、C模塊的接口:

public interface AModuleApi {
      public void a1();
      public void a2();

      public void a3();
}

同理定義B、C模塊的接口:

public interface BModuleApi {
      //對子系統外部
      public void b1();
      //子系統內部使用
      public void b2();
      //子系統內部使用
      public void b3();
}
public interface CModuleApi {
      //對子系統外部
      public void c1();
      //子系統內部使用
      public void c2();
      //子系統內部使用
      public void c3();
}

定義好了各個模塊的接口,接下來定義Facade的接口:

public interface FacadeApi {
      public void a1();
      public void b1();
      public void c1();
      public void test();
}

這樣定義Facade的話,外部只需要有Facade接口,就不再需要其它的接口了,這樣就能有效地屏蔽內部的細節,免得客戶端去調用A模塊的接口時,發現了一些不需要它知道的接口,這會造成“接口污染”

比如a2、a3方法就不需要讓客戶端知道,否則既暴露了內部的細節,又讓客戶端迷惑。對客戶端來說,他可能還要去思考a2、a3方法用來干什么呢?其實a2、a3方法是對內部模塊之間交互的,原本就不是對子系統外部的,所以干脆就不要讓客戶端知道。

  1. Facade的方法實現

Facade的方法實現中,一般是負責把客戶端的請求轉發給子系統內部的各個模塊進行處理,Facade的方法本身并不進行功能的處理,Facade的方法的實現只是實現一個功能的組合調用

當然在Facade中實現一個邏輯處理也并無不可,但是不建議這樣做,這不是Facade的本意,也超出了Facade的邊界。

3.3 外觀模式的優缺點##

  1. 松散耦合

外觀模式松散了客戶端與子系統的耦合關系,讓子系統內部的模塊能更容易擴展和維護。

  1. 簡單易用

外觀模式讓子系統更加易用,客戶端不再需要了解子系統內部的實現,也不需要跟眾多子系統內部的模塊進行交互,只需要跟外觀交互就可以了,相當于外觀類為外部客戶端使用子系統提供了一站式服務。

  1. 更好的劃分訪問層次

通過合理使用Facade,可以幫助我們更好的劃分訪問的層次。有些方法是對系統外的,有些方法是系統內部使用的。把需要暴露給外部的功能集中到外觀中,這樣既方便客戶端使用,也很好的隱藏了內部的細節。

  1. 過多的或者是不太合理的Facade也容易讓人迷惑,到底是調用Facade好呢,還是直接調用模塊好。

3.4 思考外觀模式##

  1. 外觀模式的本質

外觀模式的本質:封裝交互,簡化調用

Facade封裝了子系統外部和子系統內多個模塊的交互過程,從而簡化外部的調用。通過外觀,子系統為外部提供一些高層的接口,以方便它們的使用。

  1. 對設計原則的體現

外觀模式很好的體現了“最少知識原則”

如果不使用外觀模式,客戶端通常需要和子系統內部的多個模塊交互,也就是說客戶端會有很多的朋友,客戶端和這些模塊之間都有依賴關系,任意一個模塊的變動都可能會引起客戶端的變動。

使用外觀模式過后,客戶端只需要和外觀類交互,也就是說客戶端只有外觀類這一個朋友,客戶端就不需要去關心子系統內部模塊的變動情況了,客戶端只是和這個外觀類有依賴關系。

這樣一來,客戶端不但簡單,而且這個系統會更有彈性。當系統內部多個模塊發生變化的時候,這個變化可以被這個外觀類吸收和消化,并不需要影響到客戶端,換句話說就是:可以在不影響客戶端的情況下,實現系統內部的維護和擴展

  1. 何時選用外觀模式

如果你希望為一個復雜的子系統提供一個簡單接口的時候,可以考慮使用外觀模式,使用外觀對象來實現大部分客戶需要的功能,從而簡化客戶的使用;

如果想要讓客戶程序和抽象類的實現部分松散耦合,可以考慮使用外觀模式,使用外觀對象來將這個子系統與它的客戶分離開來,從而提高子系統的獨立性和可移植性;

如果構建多層結構的系統,可以考慮使用外觀模式,使用外觀對象作為每層的入口,這樣可以簡化層間調用,也可以松散層次之間的依賴關系;

3.5 相關模式##

  1. 外觀模式和中介者模式

這兩個模式非常類似,但是有本質的區別。

中介者模式主要用來封裝多個對象之間相互的交互,多用在系統內部的多個模塊之間;外觀模式封裝的是單向的交互,是從客戶端訪問系統的調用,沒有從系統中來訪問客戶端的調用

中介者模式的實現里面,是需要實現具體的交互功能的;而外觀模式的實現里面,一般是組合調用或是轉調內部實現的功能,通常外觀模式本身并不實現這些功能。

中介者模式的目的主要是松散多個模塊之間的耦合,把這些耦合關系全部放到中介者中去實現;而外觀模式的目的是簡化客戶端的調用,這點和中介者模式也不同。

  1. 外觀模式和單例模式

通常一個子系統只需要一個外觀實例,所以外觀模式可以和單例模式組合使用,把Facade類實現成為單例。當然,也可以跟前面示例的那樣,把外觀類的構造方法私有化,然后把提供給客戶端的方法實現成為靜態的。

  1. 外觀模式和抽象工廠模式

外觀模式的外觀類通常需要和系統內部的多個模塊交互,每個模塊一般都有自己的接口,所以在外觀類的具體實現里面,需要獲取這些接口,然后組合這些接口來完成客戶端的功能

那么怎么獲取這些接口呢?就可以和抽象工廠一起使用,外觀類通過抽象工廠來獲取所需要的接口,而抽象工廠也可以把模塊內部的實現對Facade進行屏蔽,也就是說Facade也僅僅只是知道它從模塊中獲取的它需要的功能,模塊內部的細節,Facade也不知道了

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容